blob: 840563959805eb691ff9b4f2f0ac621bb84001f2 [file] [log] [blame]
Takuto Ikuta13466d02021-01-19 00:19:20 +00001#!/usr/bin/env python3
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
Takuto Ikuta13466d02021-01-19 00:19:20 +000020> python3 post_build_ninja_summary.py -C out/Default
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080021
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
Daniel Bratella10370c2018-06-11 07:58:59 +000054import argparse
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080055import errno
Bruce Dawson34d90be2020-03-16 23:08:05 +000056import fnmatch
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080057import os
58import sys
59
60
61# The number of long build times to report:
62long_count = 10
63# The number of long times by extension to report
Peter Wene3a42b22020-04-08 21:39:26 +000064long_ext_count = 10
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080065
66
67class Target:
68 """Represents a single line read for a .ninja_log file."""
69 def __init__(self, start, end):
Bruce Dawson6be8afd2018-06-11 20:00:05 +000070 """Creates a target object by passing in the start/end times in seconds
71 as a float."""
72 self.start = start
73 self.end = end
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080074 # A list of targets, appended to by the owner of this object.
75 self.targets = []
76 self.weighted_duration = 0.0
77
78 def Duration(self):
79 """Returns the task duration in seconds as a float."""
80 return self.end - self.start
81
82 def SetWeightedDuration(self, weighted_duration):
83 """Sets the duration, in seconds, passed in as a float."""
84 self.weighted_duration = weighted_duration
85
86 def WeightedDuration(self):
87 """Returns the task's weighted duration in seconds as a float.
88
89 Weighted_duration takes the elapsed time of the task and divides it
90 by how many other tasks were running at the same time. Thus, it
91 represents the approximate impact of this task on the total build time,
92 with serialized or serializing steps typically ending up with much
93 longer weighted durations.
94 weighted_duration should always be the same or shorter than duration.
95 """
96 # Allow for modest floating-point errors
97 epsilon = 0.000002
98 if (self.weighted_duration > self.Duration() + epsilon):
Raul Tambre80ee78e2019-05-06 22:41:05 +000099 print('%s > %s?' % (self.weighted_duration, self.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800100 assert(self.weighted_duration <= self.Duration() + epsilon)
101 return self.weighted_duration
102
103 def DescribeTargets(self):
104 """Returns a printable string that summarizes the targets."""
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800105 # Some build steps generate dozens of outputs - handle them sanely.
Bruce Dawson0081a122020-09-26 19:13:23 +0000106 # The max_length was chosen so that it can fit most of the long
107 # single-target names, while minimizing word wrapping.
108 result = ', '.join(self.targets)
109 max_length = 65
110 if len(result) > max_length:
111 result = result[:max_length] + '...'
112 return result
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800113
114
115# Copied with some modifications from ninjatracing
116def ReadTargets(log, show_all):
117 """Reads all targets from .ninja_log file |log_file|, sorted by duration.
118
119 The result is a list of Target objects."""
120 header = log.readline()
121 assert header == '# ninja log v5\n', \
122 'unrecognized ninja log version %r' % header
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000123 targets_dict = {}
124 last_end_seen = 0.0
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800125 for line in log:
126 parts = line.strip().split('\t')
127 if len(parts) != 5:
128 # If ninja.exe is rudely halted then the .ninja_log file may be
129 # corrupt. Silently continue.
130 continue
131 start, end, _, name, cmdhash = parts # Ignore restat.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000132 # Convert from integral milliseconds to float seconds.
133 start = int(start) / 1000.0
134 end = int(end) / 1000.0
135 if not show_all and end < last_end_seen:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800136 # An earlier time stamp means that this step is the first in a new
137 # build, possibly an incremental build. Throw away the previous
138 # data so that this new build will be displayed independently.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000139 # This has to be done by comparing end times because records are
140 # written to the .ninja_log file when commands complete, so end
141 # times are guaranteed to be in order, but start times are not.
142 targets_dict = {}
143 target = None
144 if cmdhash in targets_dict:
145 target = targets_dict[cmdhash]
146 if not show_all and (target.start != start or target.end != end):
147 # If several builds in a row just run one or two build steps then
148 # the end times may not go backwards so the last build may not be
149 # detected as such. However in many cases there will be a build step
150 # repeated in the two builds and the changed start/stop points for
151 # that command, identified by the hash, can be used to detect and
152 # reset the target dictionary.
153 targets_dict = {}
154 target = None
155 if not target:
156 targets_dict[cmdhash] = target = Target(start, end)
157 last_end_seen = end
158 target.targets.append(name)
Bruce Dawson0081a122020-09-26 19:13:23 +0000159 return list(targets_dict.values())
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800160
161
Bruce Dawson34d90be2020-03-16 23:08:05 +0000162def GetExtension(target, extra_patterns):
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800163 """Return the file extension that best represents a target.
164
165 For targets that generate multiple outputs it is important to return a
166 consistent 'canonical' extension. Ultimately the goal is to group build steps
167 by type."""
168 for output in target.targets:
Bruce Dawson34d90be2020-03-16 23:08:05 +0000169 if extra_patterns:
170 for fn_pattern in extra_patterns.split(';'):
171 if fnmatch.fnmatch(output, '*' + fn_pattern + '*'):
172 return fn_pattern
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800173 # Not a true extension, but a good grouping.
174 if output.endswith('type_mappings'):
175 extension = 'type_mappings'
176 break
Peter Wene3a42b22020-04-08 21:39:26 +0000177
178 # Capture two extensions if present. For example: file.javac.jar should be
179 # distinguished from file.interface.jar.
180 root, ext1 = os.path.splitext(output)
181 _, ext2 = os.path.splitext(root)
182 extension = ext2 + ext1 # Preserve the order in the file name.
183
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800184 if len(extension) == 0:
185 extension = '(no extension found)'
Peter Wene3a42b22020-04-08 21:39:26 +0000186
187 if ext1 in ['.pdb', '.dll', '.exe']:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800188 extension = 'PEFile (linking)'
189 # Make sure that .dll and .exe are grouped together and that the
190 # .dll.lib files don't cause these to be listed as libraries
191 break
Peter Wene3a42b22020-04-08 21:39:26 +0000192 if ext1 in ['.so', '.TOC']:
Bruce Dawsone186e502018-02-12 15:41:11 -0800193 extension = '.so (linking)'
194 # Attempt to identify linking, avoid identifying as '.TOC'
195 break
Bruce Dawson1eb16f62020-02-21 16:56:35 +0000196 # Make sure .obj files don't get categorized as mojo files
Peter Wene3a42b22020-04-08 21:39:26 +0000197 if ext1 in ['.obj', '.o']:
198 break
199 # Jars are the canonical output of java targets.
200 if ext1 == '.jar':
Bruce Dawson1eb16f62020-02-21 16:56:35 +0000201 break
202 # Normalize all mojo related outputs to 'mojo'.
203 if output.count('.mojom') > 0:
204 extension = 'mojo'
205 break
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800206 return extension
207
208
Bruce Dawson58da0c82023-02-23 19:40:12 +0000209def SummarizeEntries(entries, extra_step_types, elapsed_time_sorting):
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800210 """Print a summary of the passed in list of Target objects."""
211
212 # Create a list that is in order by time stamp and has entries for the
213 # beginning and ending of each build step (one time stamp may have multiple
214 # entries due to multiple steps starting/stopping at exactly the same time).
215 # Iterate through this list, keeping track of which tasks are running at all
216 # times. At each time step calculate a running total for weighted time so
217 # that when each task ends its own weighted time can easily be calculated.
218 task_start_stop_times = []
219
220 earliest = -1
221 latest = 0
222 total_cpu_time = 0
223 for target in entries:
224 if earliest < 0 or target.start < earliest:
225 earliest = target.start
226 if target.end > latest:
227 latest = target.end
228 total_cpu_time += target.Duration()
229 task_start_stop_times.append((target.start, 'start', target))
230 task_start_stop_times.append((target.end, 'stop', target))
231 length = latest - earliest
232 weighted_total = 0.0
233
Bruce Dawson0081a122020-09-26 19:13:23 +0000234 # Sort by the time/type records and ignore |target|
235 task_start_stop_times.sort(key=lambda times: times[:2])
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800236 # Now we have all task start/stop times sorted by when they happen. If a
237 # task starts and stops on the same time stamp then the start will come
238 # first because of the alphabet, which is important for making this work
239 # correctly.
240 # Track the tasks which are currently running.
241 running_tasks = {}
242 # Record the time we have processed up to so we know how to calculate time
243 # deltas.
244 last_time = task_start_stop_times[0][0]
245 # Track the accumulated weighted time so that it can efficiently be added
246 # to individual tasks.
247 last_weighted_time = 0.0
248 # Scan all start/stop events.
249 for event in task_start_stop_times:
250 time, action_name, target = event
251 # Accumulate weighted time up to now.
252 num_running = len(running_tasks)
253 if num_running > 0:
254 # Update the total weighted time up to this moment.
255 last_weighted_time += (time - last_time) / float(num_running)
256 if action_name == 'start':
257 # Record the total weighted task time when this task starts.
258 running_tasks[target] = last_weighted_time
259 if action_name == 'stop':
260 # Record the change in the total weighted task time while this task ran.
261 weighted_duration = last_weighted_time - running_tasks[target]
262 target.SetWeightedDuration(weighted_duration)
263 weighted_total += weighted_duration
264 del running_tasks[target]
265 last_time = time
266 assert(len(running_tasks) == 0)
267
268 # Warn if the sum of weighted times is off by more than half a second.
269 if abs(length - weighted_total) > 500:
Bruce Dawson0081a122020-09-26 19:13:23 +0000270 print('Warning: Possible corrupt ninja log, results may be '
271 'untrustworthy. Length = %.3f, weighted total = %.3f' % (
Raul Tambre80ee78e2019-05-06 22:41:05 +0000272 length, weighted_total))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800273
Bruce Dawson58da0c82023-02-23 19:40:12 +0000274 # Print the slowest build steps:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000275 print(' Longest build steps:')
Bruce Dawson58da0c82023-02-23 19:40:12 +0000276 if elapsed_time_sorting:
277 entries.sort(key=lambda x: x.Duration())
278 else:
279 entries.sort(key=lambda x: x.WeightedDuration())
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800280 for target in entries[-long_count:]:
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000281 print(' %8.1f weighted s to build %s (%.1f s elapsed time)' % (
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800282 target.WeightedDuration(),
Raul Tambre80ee78e2019-05-06 22:41:05 +0000283 target.DescribeTargets(), target.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800284
285 # Sum up the time by file extension/type of the output file
286 count_by_ext = {}
287 time_by_ext = {}
288 weighted_time_by_ext = {}
289 # Scan through all of the targets to build up per-extension statistics.
290 for target in entries:
Bruce Dawson34d90be2020-03-16 23:08:05 +0000291 extension = GetExtension(target, extra_step_types)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800292 time_by_ext[extension] = time_by_ext.get(extension, 0) + target.Duration()
293 weighted_time_by_ext[extension] = weighted_time_by_ext.get(extension,
294 0) + target.WeightedDuration()
295 count_by_ext[extension] = count_by_ext.get(extension, 0) + 1
296
Raul Tambre80ee78e2019-05-06 22:41:05 +0000297 print(' Time by build-step type:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800298 # Copy to a list with extension name and total time swapped, to (time, ext)
Bruce Dawson58da0c82023-02-23 19:40:12 +0000299 if elapsed_time_sorting:
300 weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in
301 time_by_ext.items())
302 else:
303 weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in
304 weighted_time_by_ext.items())
305 # Print the slowest build target types:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800306 for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000307 print(' %8.1f s weighted time to generate %d %s files '
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000308 '(%1.1f s elapsed time sum)' % (time, count_by_ext[extension],
Raul Tambre80ee78e2019-05-06 22:41:05 +0000309 extension, time_by_ext[extension]))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800310
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000311 print(' %.1f s weighted time (%.1f s elapsed time sum, %1.1fx '
312 'parallelism)' % (length, total_cpu_time,
Raul Tambre80ee78e2019-05-06 22:41:05 +0000313 total_cpu_time * 1.0 / length))
314 print(' %d build steps completed, average of %1.2f/s' % (
315 len(entries), len(entries) / (length)))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800316
317
Daniel Bratella10370c2018-06-11 07:58:59 +0000318def main():
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800319 log_file = '.ninja_log'
Daniel Bratella10370c2018-06-11 07:58:59 +0000320 parser = argparse.ArgumentParser()
321 parser.add_argument('-C', dest='build_directory',
322 help='Build directory.')
Bruce Dawson34d90be2020-03-16 23:08:05 +0000323 parser.add_argument(
324 '-s',
325 '--step-types',
326 help='semicolon separated fnmatch patterns for build-step grouping')
Bruce Dawson58da0c82023-02-23 19:40:12 +0000327 parser.add_argument(
328 '-e',
329 '--elapsed_time_sorting',
330 default=False,
331 action='store_true',
332 help='Sort output by elapsed time instead of weighted time')
Daniel Bratella10370c2018-06-11 07:58:59 +0000333 parser.add_argument('--log-file',
334 help="specific ninja log file to analyze.")
335 args, _extra_args = parser.parse_known_args()
336 if args.build_directory:
337 log_file = os.path.join(args.build_directory, log_file)
338 if args.log_file:
339 log_file = args.log_file
Bruce Dawson34d90be2020-03-16 23:08:05 +0000340 if not args.step_types:
341 # Offer a convenient way to add extra step types automatically, including
342 # when this script is run by autoninja. get() returns None if the variable
343 # isn't set.
344 args.step_types = os.environ.get('chromium_step_types')
345 if args.step_types:
346 # Make room for the extra build types.
347 global long_ext_count
348 long_ext_count += len(args.step_types.split(';'))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800349
350 try:
351 with open(log_file, 'r') as log:
352 entries = ReadTargets(log, False)
Bruce Dawson58da0c82023-02-23 19:40:12 +0000353 SummarizeEntries(entries, args.step_types, args.elapsed_time_sorting)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800354 except IOError:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000355 print('Log file %r not found, no build summary created.' % log_file)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800356 return errno.ENOENT
357
358
359if __name__ == '__main__':
Daniel Bratella10370c2018-06-11 07:58:59 +0000360 sys.exit(main())