blob: 99cbc858a2ba829ea9ea4cd5db33a0986b707d72 [file] [log] [blame]
Edward Lemur59a3b2f2020-01-14 01:50:50 +00001#!/usr/bin/env vpython
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -08002# Copyright (c) 2018 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Summarize the last ninja build, invoked with ninja's -C syntax.
7
8This script is designed to be automatically run after each ninja build in
9order to summarize the build's performance. Making build performance information
10more visible should make it easier to notice anomalies and opportunities. To use
Bruce Dawsone186e502018-02-12 15:41:11 -080011this script on Windows just set NINJA_SUMMARIZE_BUILD=1 and run autoninja.bat.
12
13On Linux you can get autoninja to invoke this script using this syntax:
14
15$ NINJA_SUMMARIZE_BUILD=1 autoninja -C out/Default/ chrome
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080016
17You can also call this script directly using ninja's syntax to specify the
18output directory of interest:
19
20> python post_build_ninja_summary.py -C out/Default
21
22Typical output looks like this:
23
24>ninja -C out\debug_component base
25ninja.exe -C out\debug_component base -j 960 -l 48 -d keeprsp
26ninja: Entering directory `out\debug_component'
27[1 processes, 1/1 @ 0.3/s : 3.092s ] Regenerating ninja files
Bruce Dawson0e9afd22019-11-08 18:57:08 +000028Longest build steps:
29 0.1 weighted s to build obj/base/base/trace_log.obj (6.7 s elapsed time)
30 0.2 weighted s to build nasm.exe, nasm.exe.pdb (0.2 s elapsed time)
31 0.3 weighted s to build obj/base/base/win_util.obj (12.4 s elapsed time)
32 1.2 weighted s to build base.dll, base.dll.lib (1.2 s elapsed time)
33Time by build-step type:
34 0.0 s weighted time to generate 6 .lib files (0.3 s elapsed time sum)
35 0.1 s weighted time to generate 25 .stamp files (1.2 s elapsed time sum)
36 0.2 s weighted time to generate 20 .o files (2.8 s elapsed time sum)
37 1.7 s weighted time to generate 4 PEFile (linking) files (2.0 s elapsed
38time sum)
39 23.9 s weighted time to generate 770 .obj files (974.8 s elapsed time sum)
4026.1 s weighted time (982.9 s elapsed time sum, 37.7x parallelism)
41839 build steps completed, average of 32.17/s
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080042
43If no gn clean has been done then results will be for the last non-NULL
44invocation of ninja. Ideas for future statistics, and implementations are
45appreciated.
46
47The "weighted" time is the elapsed time of each build step divided by the number
48of tasks that were running in parallel. This makes it an excellent approximation
49of how "important" a slow step was. A link that is entirely or mostly serialized
50will have a weighted time that is the same or similar to its elapsed time. A
51compile that runs in parallel with 999 other compiles will have a weighted time
52that is tiny."""
53
Raul Tambre80ee78e2019-05-06 22:41:05 +000054from __future__ import print_function
55
Daniel Bratella10370c2018-06-11 07:58:59 +000056import argparse
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080057import errno
Bruce Dawson34d90be2020-03-16 23:08:05 +000058import fnmatch
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080059import os
60import sys
61
62
63# The number of long build times to report:
64long_count = 10
65# The number of long times by extension to report
Peter Wene3a42b22020-04-08 21:39:26 +000066long_ext_count = 10
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080067
68
69class Target:
70 """Represents a single line read for a .ninja_log file."""
71 def __init__(self, start, end):
Bruce Dawson6be8afd2018-06-11 20:00:05 +000072 """Creates a target object by passing in the start/end times in seconds
73 as a float."""
74 self.start = start
75 self.end = end
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080076 # A list of targets, appended to by the owner of this object.
77 self.targets = []
78 self.weighted_duration = 0.0
79
80 def Duration(self):
81 """Returns the task duration in seconds as a float."""
82 return self.end - self.start
83
84 def SetWeightedDuration(self, weighted_duration):
85 """Sets the duration, in seconds, passed in as a float."""
86 self.weighted_duration = weighted_duration
87
88 def WeightedDuration(self):
89 """Returns the task's weighted duration in seconds as a float.
90
91 Weighted_duration takes the elapsed time of the task and divides it
92 by how many other tasks were running at the same time. Thus, it
93 represents the approximate impact of this task on the total build time,
94 with serialized or serializing steps typically ending up with much
95 longer weighted durations.
96 weighted_duration should always be the same or shorter than duration.
97 """
98 # Allow for modest floating-point errors
99 epsilon = 0.000002
100 if (self.weighted_duration > self.Duration() + epsilon):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000101 print('%s > %s?' % (self.weighted_duration, self.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800102 assert(self.weighted_duration <= self.Duration() + epsilon)
103 return self.weighted_duration
104
105 def DescribeTargets(self):
106 """Returns a printable string that summarizes the targets."""
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800107 # Some build steps generate dozens of outputs - handle them sanely.
Bruce Dawson0081a122020-09-26 19:13:23 +0000108 # The max_length was chosen so that it can fit most of the long
109 # single-target names, while minimizing word wrapping.
110 result = ', '.join(self.targets)
111 max_length = 65
112 if len(result) > max_length:
113 result = result[:max_length] + '...'
114 return result
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800115
116
117# Copied with some modifications from ninjatracing
118def ReadTargets(log, show_all):
119 """Reads all targets from .ninja_log file |log_file|, sorted by duration.
120
121 The result is a list of Target objects."""
122 header = log.readline()
123 assert header == '# ninja log v5\n', \
124 'unrecognized ninja log version %r' % header
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000125 targets_dict = {}
126 last_end_seen = 0.0
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800127 for line in log:
128 parts = line.strip().split('\t')
129 if len(parts) != 5:
130 # If ninja.exe is rudely halted then the .ninja_log file may be
131 # corrupt. Silently continue.
132 continue
133 start, end, _, name, cmdhash = parts # Ignore restat.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000134 # Convert from integral milliseconds to float seconds.
135 start = int(start) / 1000.0
136 end = int(end) / 1000.0
137 if not show_all and end < last_end_seen:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800138 # An earlier time stamp means that this step is the first in a new
139 # build, possibly an incremental build. Throw away the previous
140 # data so that this new build will be displayed independently.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000141 # This has to be done by comparing end times because records are
142 # written to the .ninja_log file when commands complete, so end
143 # times are guaranteed to be in order, but start times are not.
144 targets_dict = {}
145 target = None
146 if cmdhash in targets_dict:
147 target = targets_dict[cmdhash]
148 if not show_all and (target.start != start or target.end != end):
149 # If several builds in a row just run one or two build steps then
150 # the end times may not go backwards so the last build may not be
151 # detected as such. However in many cases there will be a build step
152 # repeated in the two builds and the changed start/stop points for
153 # that command, identified by the hash, can be used to detect and
154 # reset the target dictionary.
155 targets_dict = {}
156 target = None
157 if not target:
158 targets_dict[cmdhash] = target = Target(start, end)
159 last_end_seen = end
160 target.targets.append(name)
Bruce Dawson0081a122020-09-26 19:13:23 +0000161 return list(targets_dict.values())
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800162
163
Bruce Dawson34d90be2020-03-16 23:08:05 +0000164def GetExtension(target, extra_patterns):
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800165 """Return the file extension that best represents a target.
166
167 For targets that generate multiple outputs it is important to return a
168 consistent 'canonical' extension. Ultimately the goal is to group build steps
169 by type."""
170 for output in target.targets:
Bruce Dawson34d90be2020-03-16 23:08:05 +0000171 if extra_patterns:
172 for fn_pattern in extra_patterns.split(';'):
173 if fnmatch.fnmatch(output, '*' + fn_pattern + '*'):
174 return fn_pattern
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800175 # Not a true extension, but a good grouping.
176 if output.endswith('type_mappings'):
177 extension = 'type_mappings'
178 break
Peter Wene3a42b22020-04-08 21:39:26 +0000179
180 # Capture two extensions if present. For example: file.javac.jar should be
181 # distinguished from file.interface.jar.
182 root, ext1 = os.path.splitext(output)
183 _, ext2 = os.path.splitext(root)
184 extension = ext2 + ext1 # Preserve the order in the file name.
185
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800186 if len(extension) == 0:
187 extension = '(no extension found)'
Peter Wene3a42b22020-04-08 21:39:26 +0000188
189 if ext1 in ['.pdb', '.dll', '.exe']:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800190 extension = 'PEFile (linking)'
191 # Make sure that .dll and .exe are grouped together and that the
192 # .dll.lib files don't cause these to be listed as libraries
193 break
Peter Wene3a42b22020-04-08 21:39:26 +0000194 if ext1 in ['.so', '.TOC']:
Bruce Dawsone186e502018-02-12 15:41:11 -0800195 extension = '.so (linking)'
196 # Attempt to identify linking, avoid identifying as '.TOC'
197 break
Bruce Dawson1eb16f62020-02-21 16:56:35 +0000198 # Make sure .obj files don't get categorized as mojo files
Peter Wene3a42b22020-04-08 21:39:26 +0000199 if ext1 in ['.obj', '.o']:
200 break
201 # Jars are the canonical output of java targets.
202 if ext1 == '.jar':
Bruce Dawson1eb16f62020-02-21 16:56:35 +0000203 break
204 # Normalize all mojo related outputs to 'mojo'.
205 if output.count('.mojom') > 0:
206 extension = 'mojo'
207 break
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800208 return extension
209
210
Bruce Dawson34d90be2020-03-16 23:08:05 +0000211def SummarizeEntries(entries, extra_step_types):
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800212 """Print a summary of the passed in list of Target objects."""
213
214 # Create a list that is in order by time stamp and has entries for the
215 # beginning and ending of each build step (one time stamp may have multiple
216 # entries due to multiple steps starting/stopping at exactly the same time).
217 # Iterate through this list, keeping track of which tasks are running at all
218 # times. At each time step calculate a running total for weighted time so
219 # that when each task ends its own weighted time can easily be calculated.
220 task_start_stop_times = []
221
222 earliest = -1
223 latest = 0
224 total_cpu_time = 0
225 for target in entries:
226 if earliest < 0 or target.start < earliest:
227 earliest = target.start
228 if target.end > latest:
229 latest = target.end
230 total_cpu_time += target.Duration()
231 task_start_stop_times.append((target.start, 'start', target))
232 task_start_stop_times.append((target.end, 'stop', target))
233 length = latest - earliest
234 weighted_total = 0.0
235
Bruce Dawson0081a122020-09-26 19:13:23 +0000236 # Sort by the time/type records and ignore |target|
237 task_start_stop_times.sort(key=lambda times: times[:2])
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800238 # Now we have all task start/stop times sorted by when they happen. If a
239 # task starts and stops on the same time stamp then the start will come
240 # first because of the alphabet, which is important for making this work
241 # correctly.
242 # Track the tasks which are currently running.
243 running_tasks = {}
244 # Record the time we have processed up to so we know how to calculate time
245 # deltas.
246 last_time = task_start_stop_times[0][0]
247 # Track the accumulated weighted time so that it can efficiently be added
248 # to individual tasks.
249 last_weighted_time = 0.0
250 # Scan all start/stop events.
251 for event in task_start_stop_times:
252 time, action_name, target = event
253 # Accumulate weighted time up to now.
254 num_running = len(running_tasks)
255 if num_running > 0:
256 # Update the total weighted time up to this moment.
257 last_weighted_time += (time - last_time) / float(num_running)
258 if action_name == 'start':
259 # Record the total weighted task time when this task starts.
260 running_tasks[target] = last_weighted_time
261 if action_name == 'stop':
262 # Record the change in the total weighted task time while this task ran.
263 weighted_duration = last_weighted_time - running_tasks[target]
264 target.SetWeightedDuration(weighted_duration)
265 weighted_total += weighted_duration
266 del running_tasks[target]
267 last_time = time
268 assert(len(running_tasks) == 0)
269
270 # Warn if the sum of weighted times is off by more than half a second.
271 if abs(length - weighted_total) > 500:
Bruce Dawson0081a122020-09-26 19:13:23 +0000272 print('Warning: Possible corrupt ninja log, results may be '
273 'untrustworthy. Length = %.3f, weighted total = %.3f' % (
Raul Tambre80ee78e2019-05-06 22:41:05 +0000274 length, weighted_total))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800275
276 # Print the slowest build steps (by weighted time).
Raul Tambre80ee78e2019-05-06 22:41:05 +0000277 print(' Longest build steps:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800278 entries.sort(key=lambda x: x.WeightedDuration())
279 for target in entries[-long_count:]:
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000280 print(' %8.1f weighted s to build %s (%.1f s elapsed time)' % (
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800281 target.WeightedDuration(),
Raul Tambre80ee78e2019-05-06 22:41:05 +0000282 target.DescribeTargets(), target.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800283
284 # Sum up the time by file extension/type of the output file
285 count_by_ext = {}
286 time_by_ext = {}
287 weighted_time_by_ext = {}
288 # Scan through all of the targets to build up per-extension statistics.
289 for target in entries:
Bruce Dawson34d90be2020-03-16 23:08:05 +0000290 extension = GetExtension(target, extra_step_types)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800291 time_by_ext[extension] = time_by_ext.get(extension, 0) + target.Duration()
292 weighted_time_by_ext[extension] = weighted_time_by_ext.get(extension,
293 0) + target.WeightedDuration()
294 count_by_ext[extension] = count_by_ext.get(extension, 0) + 1
295
Raul Tambre80ee78e2019-05-06 22:41:05 +0000296 print(' Time by build-step type:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800297 # Copy to a list with extension name and total time swapped, to (time, ext)
298 weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in
299 weighted_time_by_ext.items())
300 # Print the slowest build target types (by weighted time):
301 for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000302 print(' %8.1f s weighted time to generate %d %s files '
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000303 '(%1.1f s elapsed time sum)' % (time, count_by_ext[extension],
Raul Tambre80ee78e2019-05-06 22:41:05 +0000304 extension, time_by_ext[extension]))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800305
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000306 print(' %.1f s weighted time (%.1f s elapsed time sum, %1.1fx '
307 'parallelism)' % (length, total_cpu_time,
Raul Tambre80ee78e2019-05-06 22:41:05 +0000308 total_cpu_time * 1.0 / length))
309 print(' %d build steps completed, average of %1.2f/s' % (
310 len(entries), len(entries) / (length)))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800311
312
Daniel Bratella10370c2018-06-11 07:58:59 +0000313def main():
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800314 log_file = '.ninja_log'
Daniel Bratella10370c2018-06-11 07:58:59 +0000315 parser = argparse.ArgumentParser()
316 parser.add_argument('-C', dest='build_directory',
317 help='Build directory.')
Bruce Dawson34d90be2020-03-16 23:08:05 +0000318 parser.add_argument(
319 '-s',
320 '--step-types',
321 help='semicolon separated fnmatch patterns for build-step grouping')
Daniel Bratella10370c2018-06-11 07:58:59 +0000322 parser.add_argument('--log-file',
323 help="specific ninja log file to analyze.")
324 args, _extra_args = parser.parse_known_args()
325 if args.build_directory:
326 log_file = os.path.join(args.build_directory, log_file)
327 if args.log_file:
328 log_file = args.log_file
Bruce Dawson34d90be2020-03-16 23:08:05 +0000329 if not args.step_types:
330 # Offer a convenient way to add extra step types automatically, including
331 # when this script is run by autoninja. get() returns None if the variable
332 # isn't set.
333 args.step_types = os.environ.get('chromium_step_types')
334 if args.step_types:
335 # Make room for the extra build types.
336 global long_ext_count
337 long_ext_count += len(args.step_types.split(';'))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800338
339 try:
340 with open(log_file, 'r') as log:
341 entries = ReadTargets(log, False)
Bruce Dawson34d90be2020-03-16 23:08:05 +0000342 SummarizeEntries(entries, args.step_types)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800343 except IOError:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000344 print('Log file %r not found, no build summary created.' % log_file)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800345 return errno.ENOENT
346
347
348if __name__ == '__main__':
Daniel Bratella10370c2018-06-11 07:58:59 +0000349 sys.exit(main())