You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
391 lines
16 KiB
Python
391 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# Copyright (c) 2018 The Chromium Authors. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
r"""Summarize the last ninja build, invoked with ninja's -C syntax.
|
|
|
|
This script is designed to be automatically run after each ninja build in
|
|
order to summarize the build's performance. Making build performance information
|
|
more visible should make it easier to notice anomalies and opportunities. To use
|
|
this script on Windows just set NINJA_SUMMARIZE_BUILD=1 and run autoninja.bat.
|
|
|
|
On Linux you can get autoninja to invoke this script using this syntax:
|
|
|
|
$ NINJA_SUMMARIZE_BUILD=1 autoninja -C out/Default/ chrome
|
|
|
|
You can also call this script directly using ninja's syntax to specify the
|
|
output directory of interest:
|
|
|
|
> python3 post_build_ninja_summary.py -C out/Default
|
|
|
|
Typical output looks like this:
|
|
|
|
>ninja -C out\debug_component base
|
|
ninja.exe -C out\debug_component base -j 960 -l 48 -d keeprsp
|
|
ninja: Entering directory `out\debug_component'
|
|
[1 processes, 1/1 @ 0.3/s : 3.092s ] Regenerating ninja files
|
|
Longest build steps:
|
|
0.1 weighted s to build obj/base/base/trace_log.obj (6.7 s elapsed time)
|
|
0.2 weighted s to build nasm.exe, nasm.exe.pdb (0.2 s elapsed time)
|
|
0.3 weighted s to build obj/base/base/win_util.obj (12.4 s elapsed time)
|
|
1.2 weighted s to build base.dll, base.dll.lib (1.2 s elapsed time)
|
|
Time by build-step type:
|
|
0.0 s weighted time to generate 6 .lib files (0.3 s elapsed time sum)
|
|
0.1 s weighted time to generate 25 .stamp files (1.2 s elapsed time sum)
|
|
0.2 s weighted time to generate 20 .o files (2.8 s elapsed time sum)
|
|
1.7 s weighted time to generate 4 PEFile (linking) files (2.0 s elapsed
|
|
time sum)
|
|
23.9 s weighted time to generate 770 .obj files (974.8 s elapsed time sum)
|
|
26.1 s weighted time (982.9 s elapsed time sum, 37.7x parallelism)
|
|
839 build steps completed, average of 32.17/s
|
|
|
|
If no gn clean has been done then results will be for the last non-NULL
|
|
invocation of ninja. Ideas for future statistics, and implementations are
|
|
appreciated.
|
|
|
|
The "weighted" time is the elapsed time of each build step divided by the number
|
|
of tasks that were running in parallel. This makes it an excellent approximation
|
|
of how "important" a slow step was. A link that is entirely or mostly serialized
|
|
will have a weighted time that is the same or similar to its elapsed time. A
|
|
compile that runs in parallel with 999 other compiles will have a weighted time
|
|
that is tiny."""
|
|
|
|
import argparse
|
|
import errno
|
|
import fnmatch
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
THIS_DIR = os.path.dirname(__file__)
|
|
|
|
# The number of long build times to report:
|
|
long_count = 10
|
|
# The number of long times by extension to report
|
|
long_ext_count = 10
|
|
|
|
|
|
class Target:
|
|
"""Represents a single line read for a .ninja_log file."""
|
|
def __init__(self, start, end):
|
|
"""Creates a target object by passing in the start/end times in seconds
|
|
as a float."""
|
|
self.start = start
|
|
self.end = end
|
|
# A list of targets, appended to by the owner of this object.
|
|
self.targets = []
|
|
self.weighted_duration = 0.0
|
|
|
|
def Duration(self):
|
|
"""Returns the task duration in seconds as a float."""
|
|
return self.end - self.start
|
|
|
|
def SetWeightedDuration(self, weighted_duration):
|
|
"""Sets the duration, in seconds, passed in as a float."""
|
|
self.weighted_duration = weighted_duration
|
|
|
|
def WeightedDuration(self):
|
|
"""Returns the task's weighted duration in seconds as a float.
|
|
|
|
Weighted_duration takes the elapsed time of the task and divides it
|
|
by how many other tasks were running at the same time. Thus, it
|
|
represents the approximate impact of this task on the total build time,
|
|
with serialized or serializing steps typically ending up with much
|
|
longer weighted durations.
|
|
weighted_duration should always be the same or shorter than duration.
|
|
"""
|
|
# Allow for modest floating-point errors
|
|
epsilon = 0.000002
|
|
if self.weighted_duration > self.Duration() + epsilon:
|
|
print("%s > %s?" % (self.weighted_duration, self.Duration()))
|
|
assert self.weighted_duration <= self.Duration() + epsilon
|
|
return self.weighted_duration
|
|
|
|
def DescribeTargets(self):
|
|
"""Returns a printable string that summarizes the targets."""
|
|
# Some build steps generate dozens of outputs - handle them sanely.
|
|
# The max_length was chosen so that it can fit most of the long
|
|
# single-target names, while minimizing word wrapping.
|
|
result = ", ".join(self.targets)
|
|
max_length = 65
|
|
if len(result) > max_length:
|
|
result = result[:max_length] + "..."
|
|
return result
|
|
|
|
|
|
# Copied with some modifications from ninjatracing
|
|
def ReadTargets(log, show_all):
|
|
"""Reads all targets from .ninja_log file |log_file|, sorted by duration.
|
|
|
|
The result is a list of Target objects."""
|
|
header = log.readline()
|
|
# Handle empty ninja_log gracefully by silently returning an empty list of
|
|
# targets.
|
|
if not header:
|
|
return []
|
|
assert header in ("# ninja log v5\n", "# ninja log v6\n"), (
|
|
"unrecognized ninja log version %r" % header)
|
|
targets_dict = {}
|
|
last_end_seen = 0.0
|
|
for line in log:
|
|
parts = line.strip().split("\t")
|
|
if len(parts) != 5:
|
|
# If ninja.exe is rudely halted then the .ninja_log file may be
|
|
# corrupt. Silently continue.
|
|
continue
|
|
start, end, _, name, cmdhash = parts # Ignore restat.
|
|
# Convert from integral milliseconds to float seconds.
|
|
start = int(start) / 1000.0
|
|
end = int(end) / 1000.0
|
|
if not show_all and end < last_end_seen:
|
|
# An earlier time stamp means that this step is the first in a new
|
|
# build, possibly an incremental build. Throw away the previous
|
|
# data so that this new build will be displayed independently.
|
|
# This has to be done by comparing end times because records are
|
|
# written to the .ninja_log file when commands complete, so end
|
|
# times are guaranteed to be in order, but start times are not.
|
|
targets_dict = {}
|
|
target = None
|
|
if cmdhash in targets_dict:
|
|
target = targets_dict[cmdhash]
|
|
if not show_all and (target.start != start or target.end != end):
|
|
# If several builds in a row just run one or two build steps
|
|
# then the end times may not go backwards so the last build may
|
|
# not be detected as such. However in many cases there will be a
|
|
# build step repeated in the two builds and the changed
|
|
# start/stop points for that command, identified by the hash,
|
|
# can be used to detect and reset the target dictionary.
|
|
targets_dict = {}
|
|
target = None
|
|
if not target:
|
|
targets_dict[cmdhash] = target = Target(start, end)
|
|
last_end_seen = end
|
|
target.targets.append(name)
|
|
return list(targets_dict.values())
|
|
|
|
|
|
def GetExtension(target, extra_patterns):
|
|
"""Return the file extension that best represents a target.
|
|
|
|
For targets that generate multiple outputs it is important to return a
|
|
consistent 'canonical' extension. Ultimately the goal is to group build
|
|
steps by type."""
|
|
for output in target.targets:
|
|
if extra_patterns:
|
|
for fn_pattern in extra_patterns.split(";"):
|
|
if fnmatch.fnmatch(output, "*" + fn_pattern + "*"):
|
|
return fn_pattern
|
|
# Not a true extension, but a good grouping.
|
|
if output.endswith("type_mappings"):
|
|
extension = "type_mappings"
|
|
break
|
|
|
|
# Capture two extensions if present. For example: file.javac.jar should
|
|
# be distinguished from file.interface.jar.
|
|
root, ext1 = os.path.splitext(output)
|
|
_, ext2 = os.path.splitext(root)
|
|
extension = ext2 + ext1 # Preserve the order in the file name.
|
|
|
|
if len(extension) == 0:
|
|
extension = "(no extension found)"
|
|
|
|
if ext1 in [".pdb", ".dll", ".exe"]:
|
|
extension = "PEFile (linking)"
|
|
# Make sure that .dll and .exe are grouped together and that the
|
|
# .dll.lib files don't cause these to be listed as libraries
|
|
break
|
|
if ext1 in [".so", ".TOC"]:
|
|
extension = ".so (linking)"
|
|
# Attempt to identify linking, avoid identifying as '.TOC'
|
|
break
|
|
# Make sure .obj files don't get categorized as mojo files
|
|
if ext1 in [".obj", ".o"]:
|
|
break
|
|
# Jars are the canonical output of java targets.
|
|
if ext1 == ".jar":
|
|
break
|
|
# Normalize all mojo related outputs to 'mojo'.
|
|
if output.count(".mojom") > 0:
|
|
extension = "mojo"
|
|
break
|
|
return extension
|
|
|
|
|
|
def SummarizeEntries(entries, extra_step_types, elapsed_time_sorting):
|
|
"""Print a summary of the passed in list of Target objects."""
|
|
|
|
# Create a list that is in order by time stamp and has entries for the
|
|
# beginning and ending of each build step (one time stamp may have multiple
|
|
# entries due to multiple steps starting/stopping at exactly the same time).
|
|
# Iterate through this list, keeping track of which tasks are running at all
|
|
# times. At each time step calculate a running total for weighted time so
|
|
# that when each task ends its own weighted time can easily be calculated.
|
|
task_start_stop_times = []
|
|
|
|
earliest = -1
|
|
latest = 0
|
|
total_cpu_time = 0
|
|
for target in entries:
|
|
if earliest < 0 or target.start < earliest:
|
|
earliest = target.start
|
|
if target.end > latest:
|
|
latest = target.end
|
|
total_cpu_time += target.Duration()
|
|
task_start_stop_times.append((target.start, "start", target))
|
|
task_start_stop_times.append((target.end, "stop", target))
|
|
length = latest - earliest
|
|
weighted_total = 0.0
|
|
|
|
# Sort by the time/type records and ignore |target|
|
|
task_start_stop_times.sort(key=lambda times: times[:2])
|
|
# Now we have all task start/stop times sorted by when they happen. If a
|
|
# task starts and stops on the same time stamp then the start will come
|
|
# first because of the alphabet, which is important for making this work
|
|
# correctly.
|
|
# Track the tasks which are currently running.
|
|
running_tasks = {}
|
|
# Record the time we have processed up to so we know how to calculate time
|
|
# deltas.
|
|
last_time = task_start_stop_times[0][0]
|
|
# Track the accumulated weighted time so that it can efficiently be added
|
|
# to individual tasks.
|
|
last_weighted_time = 0.0
|
|
# Scan all start/stop events.
|
|
for event in task_start_stop_times:
|
|
time, action_name, target = event
|
|
# Accumulate weighted time up to now.
|
|
num_running = len(running_tasks)
|
|
if num_running > 0:
|
|
# Update the total weighted time up to this moment.
|
|
last_weighted_time += (time - last_time) / float(num_running)
|
|
if action_name == "start":
|
|
# Record the total weighted task time when this task starts.
|
|
running_tasks[target] = last_weighted_time
|
|
if action_name == "stop":
|
|
# Record the change in the total weighted task time while this task
|
|
# ran.
|
|
weighted_duration = last_weighted_time - running_tasks[target]
|
|
target.SetWeightedDuration(weighted_duration)
|
|
weighted_total += weighted_duration
|
|
del running_tasks[target]
|
|
last_time = time
|
|
assert len(running_tasks) == 0
|
|
|
|
# Warn if the sum of weighted times is off by more than half a second.
|
|
if abs(length - weighted_total) > 500:
|
|
print("Warning: Possible corrupt ninja log, results may be "
|
|
"untrustworthy. Length = %.3f, weighted total = %.3f" %
|
|
(length, weighted_total))
|
|
|
|
# Print the slowest build steps:
|
|
print(" Longest build steps:")
|
|
if elapsed_time_sorting:
|
|
entries.sort(key=lambda x: x.Duration())
|
|
else:
|
|
entries.sort(key=lambda x: x.WeightedDuration())
|
|
for target in entries[-long_count:]:
|
|
print(" %8.1f weighted s to build %s (%.1f s elapsed time)" % (
|
|
target.WeightedDuration(),
|
|
target.DescribeTargets(),
|
|
target.Duration(),
|
|
))
|
|
|
|
# Sum up the time by file extension/type of the output file
|
|
count_by_ext = {}
|
|
time_by_ext = {}
|
|
weighted_time_by_ext = {}
|
|
# Scan through all of the targets to build up per-extension statistics.
|
|
for target in entries:
|
|
extension = GetExtension(target, extra_step_types)
|
|
time_by_ext[extension] = (time_by_ext.get(extension, 0) +
|
|
target.Duration())
|
|
weighted_time_by_ext[extension] = (
|
|
weighted_time_by_ext.get(extension, 0) + target.WeightedDuration())
|
|
count_by_ext[extension] = count_by_ext.get(extension, 0) + 1
|
|
|
|
print(" Time by build-step type:")
|
|
# Copy to a list with extension name and total time swapped, to (time, ext)
|
|
if elapsed_time_sorting:
|
|
weighted_time_by_ext_sorted = sorted(
|
|
(y, x) for (x, y) in time_by_ext.items())
|
|
else:
|
|
weighted_time_by_ext_sorted = sorted(
|
|
(y, x) for (x, y) in weighted_time_by_ext.items())
|
|
# Print the slowest build target types:
|
|
for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]:
|
|
print(" %8.1f s weighted time to generate %d %s files "
|
|
"(%1.1f s elapsed time sum)" % (
|
|
time,
|
|
count_by_ext[extension],
|
|
extension,
|
|
time_by_ext[extension],
|
|
))
|
|
|
|
print(" %.1f s weighted time (%.1f s elapsed time sum, %1.1fx "
|
|
"parallelism)" %
|
|
(length, total_cpu_time, total_cpu_time * 1.0 / length))
|
|
print(" %d build steps completed, average of %1.2f/s" %
|
|
(len(entries), len(entries) / (length)))
|
|
|
|
|
|
def main():
|
|
log_file = ".ninja_log"
|
|
metrics_file = "siso_metrics.json"
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("-C", dest="build_directory", help="Build directory.")
|
|
parser.add_argument(
|
|
"-s",
|
|
"--step-types",
|
|
help="semicolon separated fnmatch patterns for build-step grouping",
|
|
)
|
|
parser.add_argument(
|
|
"-e",
|
|
"--elapsed_time_sorting",
|
|
default=False,
|
|
action="store_true",
|
|
help="Sort output by elapsed time instead of weighted time",
|
|
)
|
|
parser.add_argument("--log-file",
|
|
help="specific ninja log file to analyze.")
|
|
args, _extra_args = parser.parse_known_args()
|
|
if args.build_directory:
|
|
log_file = os.path.join(args.build_directory, log_file)
|
|
metrics_file = os.path.join(args.build_directory, metrics_file)
|
|
if args.log_file:
|
|
log_file = args.log_file
|
|
if not args.step_types:
|
|
# Offer a convenient way to add extra step types automatically,
|
|
# including when this script is run by autoninja. get() returns None if
|
|
# the variable isn't set.
|
|
args.step_types = os.environ.get("chromium_step_types")
|
|
if args.step_types:
|
|
# Make room for the extra build types.
|
|
global long_ext_count
|
|
long_ext_count += len(args.step_types.split(";"))
|
|
|
|
if os.path.exists(metrics_file):
|
|
cmd = [sys.executable, os.path.join(THIS_DIR, "siso.py")]
|
|
cmd.extend(["metrics", "summary"])
|
|
if args.build_directory:
|
|
cmd.extend(["-C", args.build_directory])
|
|
if args.step_types:
|
|
cmd.extend(["--step_types", args.step_types])
|
|
if args.elapsed_time_sorting:
|
|
cmd.append("--elapsed_time_sorting")
|
|
subprocess.run(cmd, check=True)
|
|
return 0
|
|
|
|
try:
|
|
with open(log_file, "r") as log:
|
|
entries = ReadTargets(log, False)
|
|
if entries:
|
|
SummarizeEntries(entries, args.step_types,
|
|
args.elapsed_time_sorting)
|
|
except IOError:
|
|
print("Log file %r not found, no build summary created." % log_file)
|
|
return errno.ENOENT
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|