blob: 8b43cf54d440c4a58057a96a1c723763ad2dd655 [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.
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -08005"""Summarize the last ninja build, invoked with ninja's -C syntax.
6
7This script is designed to be automatically run after each ninja build in
8order to summarize the build's performance. Making build performance information
9more visible should make it easier to notice anomalies and opportunities. To use
Bruce Dawsone186e502018-02-12 15:41:11 -080010this script on Windows just set NINJA_SUMMARIZE_BUILD=1 and run autoninja.bat.
11
12On Linux you can get autoninja to invoke this script using this syntax:
13
14$ NINJA_SUMMARIZE_BUILD=1 autoninja -C out/Default/ chrome
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080015
16You can also call this script directly using ninja's syntax to specify the
17output directory of interest:
18
Takuto Ikuta13466d02021-01-19 00:19:20 +000019> python3 post_build_ninja_summary.py -C out/Default
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080020
21Typical output looks like this:
22
23>ninja -C out\debug_component base
24ninja.exe -C out\debug_component base -j 960 -l 48 -d keeprsp
25ninja: Entering directory `out\debug_component'
26[1 processes, 1/1 @ 0.3/s : 3.092s ] Regenerating ninja files
Bruce Dawson0e9afd22019-11-08 18:57:08 +000027Longest build steps:
28 0.1 weighted s to build obj/base/base/trace_log.obj (6.7 s elapsed time)
29 0.2 weighted s to build nasm.exe, nasm.exe.pdb (0.2 s elapsed time)
30 0.3 weighted s to build obj/base/base/win_util.obj (12.4 s elapsed time)
31 1.2 weighted s to build base.dll, base.dll.lib (1.2 s elapsed time)
32Time by build-step type:
33 0.0 s weighted time to generate 6 .lib files (0.3 s elapsed time sum)
34 0.1 s weighted time to generate 25 .stamp files (1.2 s elapsed time sum)
35 0.2 s weighted time to generate 20 .o files (2.8 s elapsed time sum)
36 1.7 s weighted time to generate 4 PEFile (linking) files (2.0 s elapsed
37time sum)
38 23.9 s weighted time to generate 770 .obj files (974.8 s elapsed time sum)
3926.1 s weighted time (982.9 s elapsed time sum, 37.7x parallelism)
40839 build steps completed, average of 32.17/s
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080041
42If no gn clean has been done then results will be for the last non-NULL
43invocation of ninja. Ideas for future statistics, and implementations are
44appreciated.
45
46The "weighted" time is the elapsed time of each build step divided by the number
47of tasks that were running in parallel. This makes it an excellent approximation
48of how "important" a slow step was. A link that is entirely or mostly serialized
49will have a weighted time that is the same or similar to its elapsed time. A
50compile that runs in parallel with 999 other compiles will have a weighted time
51that is tiny."""
52
Daniel Bratella10370c2018-06-11 07:58:59 +000053import argparse
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080054import errno
Bruce Dawson34d90be2020-03-16 23:08:05 +000055import fnmatch
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080056import os
57import sys
58
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080059# The number of long build times to report:
60long_count = 10
61# The number of long times by extension to report
Peter Wene3a42b22020-04-08 21:39:26 +000062long_ext_count = 10
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080063
64
65class Target:
66 """Represents a single line read for a .ninja_log file."""
67 def __init__(self, start, end):
Bruce Dawson6be8afd2018-06-11 20:00:05 +000068 """Creates a target object by passing in the start/end times in seconds
69 as a float."""
70 self.start = start
71 self.end = end
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080072 # A list of targets, appended to by the owner of this object.
73 self.targets = []
74 self.weighted_duration = 0.0
75
76 def Duration(self):
77 """Returns the task duration in seconds as a float."""
78 return self.end - self.start
79
80 def SetWeightedDuration(self, weighted_duration):
81 """Sets the duration, in seconds, passed in as a float."""
82 self.weighted_duration = weighted_duration
83
84 def WeightedDuration(self):
85 """Returns the task's weighted duration in seconds as a float.
86
87 Weighted_duration takes the elapsed time of the task and divides it
88 by how many other tasks were running at the same time. Thus, it
89 represents the approximate impact of this task on the total build time,
90 with serialized or serializing steps typically ending up with much
91 longer weighted durations.
92 weighted_duration should always be the same or shorter than duration.
93 """
94 # Allow for modest floating-point errors
95 epsilon = 0.000002
96 if (self.weighted_duration > self.Duration() + epsilon):
Mike Frysinger124bb8e2023-09-06 05:48:55 +000097 print('%s > %s?' % (self.weighted_duration, self.Duration()))
98 assert (self.weighted_duration <= self.Duration() + epsilon)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080099 return self.weighted_duration
100
101 def DescribeTargets(self):
102 """Returns a printable string that summarizes the targets."""
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800103 # Some build steps generate dozens of outputs - handle them sanely.
Bruce Dawson0081a122020-09-26 19:13:23 +0000104 # The max_length was chosen so that it can fit most of the long
105 # single-target names, while minimizing word wrapping.
106 result = ', '.join(self.targets)
107 max_length = 65
108 if len(result) > max_length:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000109 result = result[:max_length] + '...'
Bruce Dawson0081a122020-09-26 19:13:23 +0000110 return result
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800111
112
113# Copied with some modifications from ninjatracing
114def ReadTargets(log, show_all):
115 """Reads all targets from .ninja_log file |log_file|, sorted by duration.
116
117 The result is a list of Target objects."""
118 header = log.readline()
Philipp Wollermannc3d210d2023-09-07 16:11:00 +0000119 # TODO: b/298594790 - Siso currently generates empty .ninja_log files.
120 # Handle them gracefully by silently returning an empty list of targets.
121 if not header:
122 return []
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800123 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:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000130 # 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:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000147 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
150 # then the end times may not go backwards so the last build may
151 # not be detected as such. However in many cases there will be a
152 # build step repeated in the two builds and the changed
153 # start/stop points for that command, identified by the hash,
154 # can be used to detect and reset the target dictionary.
155 targets_dict = {}
156 target = None
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000157 if not target:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000158 targets_dict[cmdhash] = target = Target(start, end)
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000159 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):
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000165 """Return the file extension that best represents a target.
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800166
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."""
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000170 for output in target.targets:
171 if extra_patterns:
172 for fn_pattern in extra_patterns.split(';'):
173 if fnmatch.fnmatch(output, '*' + fn_pattern + '*'):
174 return fn_pattern
175 # 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
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000180 # Capture two extensions if present. For example: file.javac.jar should
181 # be 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.
Peter Wene3a42b22020-04-08 21:39:26 +0000185
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000186 if len(extension) == 0:
187 extension = '(no extension found)'
Peter Wene3a42b22020-04-08 21:39:26 +0000188
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000189 if ext1 in ['.pdb', '.dll', '.exe']:
190 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
194 if ext1 in ['.so', '.TOC']:
195 extension = '.so (linking)'
196 # Attempt to identify linking, avoid identifying as '.TOC'
197 break
198 # Make sure .obj files don't get categorized as mojo files
199 if ext1 in ['.obj', '.o']:
200 break
201 # Jars are the canonical output of java targets.
202 if ext1 == '.jar':
203 break
204 # Normalize all mojo related outputs to 'mojo'.
205 if output.count('.mojom') > 0:
206 extension = 'mojo'
207 break
208 return extension
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800209
210
Bruce Dawson58da0c82023-02-23 19:40:12 +0000211def SummarizeEntries(entries, extra_step_types, elapsed_time_sorting):
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:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000226 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))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800233 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:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000252 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
263 # ran.
264 weighted_duration = last_weighted_time - running_tasks[target]
265 target.SetWeightedDuration(weighted_duration)
266 weighted_total += weighted_duration
267 del running_tasks[target]
268 last_time = time
269 assert (len(running_tasks) == 0)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800270
271 # Warn if the sum of weighted times is off by more than half a second.
272 if abs(length - weighted_total) > 500:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000273 print('Warning: Possible corrupt ninja log, results may be '
274 'untrustworthy. Length = %.3f, weighted total = %.3f' %
275 (length, weighted_total))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800276
Bruce Dawson58da0c82023-02-23 19:40:12 +0000277 # Print the slowest build steps:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000278 print(' Longest build steps:')
Bruce Dawson58da0c82023-02-23 19:40:12 +0000279 if elapsed_time_sorting:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000280 entries.sort(key=lambda x: x.Duration())
Bruce Dawson58da0c82023-02-23 19:40:12 +0000281 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000282 entries.sort(key=lambda x: x.WeightedDuration())
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800283 for target in entries[-long_count:]:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000284 print(' %8.1f weighted s to build %s (%.1f s elapsed time)' %
285 (target.WeightedDuration(), target.DescribeTargets(),
286 target.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800287
288 # Sum up the time by file extension/type of the output file
289 count_by_ext = {}
290 time_by_ext = {}
291 weighted_time_by_ext = {}
292 # Scan through all of the targets to build up per-extension statistics.
293 for target in entries:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000294 extension = GetExtension(target, extra_step_types)
295 time_by_ext[extension] = time_by_ext.get(extension,
296 0) + target.Duration()
297 weighted_time_by_ext[extension] = weighted_time_by_ext.get(
298 extension, 0) + target.WeightedDuration()
299 count_by_ext[extension] = count_by_ext.get(extension, 0) + 1
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800300
Raul Tambre80ee78e2019-05-06 22:41:05 +0000301 print(' Time by build-step type:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800302 # Copy to a list with extension name and total time swapped, to (time, ext)
Bruce Dawson58da0c82023-02-23 19:40:12 +0000303 if elapsed_time_sorting:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000304 weighted_time_by_ext_sorted = sorted(
305 (y, x) for (x, y) in time_by_ext.items())
Bruce Dawson58da0c82023-02-23 19:40:12 +0000306 else:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000307 weighted_time_by_ext_sorted = sorted(
308 (y, x) for (x, y) in weighted_time_by_ext.items())
Bruce Dawson58da0c82023-02-23 19:40:12 +0000309 # Print the slowest build target types:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800310 for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000311 print(
312 ' %8.1f s weighted time to generate %d %s files '
313 '(%1.1f s elapsed time sum)' %
314 (time, count_by_ext[extension], extension, time_by_ext[extension]))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800315
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000316 print(' %.1f s weighted time (%.1f s elapsed time sum, %1.1fx '
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000317 'parallelism)' %
318 (length, total_cpu_time, total_cpu_time * 1.0 / length))
319 print(' %d build steps completed, average of %1.2f/s' %
320 (len(entries), len(entries) / (length)))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800321
322
Daniel Bratella10370c2018-06-11 07:58:59 +0000323def main():
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800324 log_file = '.ninja_log'
Daniel Bratella10370c2018-06-11 07:58:59 +0000325 parser = argparse.ArgumentParser()
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000326 parser.add_argument('-C', dest='build_directory', help='Build directory.')
Bruce Dawson34d90be2020-03-16 23:08:05 +0000327 parser.add_argument(
328 '-s',
329 '--step-types',
330 help='semicolon separated fnmatch patterns for build-step grouping')
Bruce Dawson58da0c82023-02-23 19:40:12 +0000331 parser.add_argument(
332 '-e',
333 '--elapsed_time_sorting',
334 default=False,
335 action='store_true',
336 help='Sort output by elapsed time instead of weighted time')
Daniel Bratella10370c2018-06-11 07:58:59 +0000337 parser.add_argument('--log-file',
338 help="specific ninja log file to analyze.")
339 args, _extra_args = parser.parse_known_args()
340 if args.build_directory:
341 log_file = os.path.join(args.build_directory, log_file)
342 if args.log_file:
343 log_file = args.log_file
Bruce Dawson34d90be2020-03-16 23:08:05 +0000344 if not args.step_types:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000345 # Offer a convenient way to add extra step types automatically,
346 # including when this script is run by autoninja. get() returns None if
347 # the variable isn't set.
348 args.step_types = os.environ.get('chromium_step_types')
Bruce Dawson34d90be2020-03-16 23:08:05 +0000349 if args.step_types:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000350 # Make room for the extra build types.
351 global long_ext_count
352 long_ext_count += len(args.step_types.split(';'))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800353
354 try:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000355 with open(log_file, 'r') as log:
356 entries = ReadTargets(log, False)
Philipp Wollermannc3d210d2023-09-07 16:11:00 +0000357 if entries:
358 SummarizeEntries(entries, args.step_types,
359 args.elapsed_time_sorting)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800360 except IOError:
Mike Frysinger124bb8e2023-09-06 05:48:55 +0000361 print('Log file %r not found, no build summary created.' % log_file)
362 return errno.ENOENT
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800363
364
365if __name__ == '__main__':
Daniel Bratella10370c2018-06-11 07:58:59 +0000366 sys.exit(main())