123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390 |
- #!/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())
|