blob: 04c1bc4809a88eb5d8e268df9d4dba1765586417 [file] [log] [blame]
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -08001# Copyright (c) 2018 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""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
19> python post_build_ninja_summary.py -C out/Default
20
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
Raul Tambre80ee78e2019-05-06 22:41:05 +000053from __future__ import print_function
54
Daniel Bratella10370c2018-06-11 07:58:59 +000055import argparse
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080056import errno
57import 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
64long_ext_count = 5
65
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."""
105 if len(self.targets) == 1:
106 return self.targets[0]
107 # Some build steps generate dozens of outputs - handle them sanely.
108 # It's a bit odd that if there are three targets we return all three
109 # but if there are more than three we just return two, but this works
110 # well in practice.
111 elif len(self.targets) > 3:
112 return '(%d items) ' % len(self.targets) + (
113 ', '.join(self.targets[:2]) + ', ...')
114 else:
115 return ', '.join(self.targets)
116
117
118# Copied with some modifications from ninjatracing
119def ReadTargets(log, show_all):
120 """Reads all targets from .ninja_log file |log_file|, sorted by duration.
121
122 The result is a list of Target objects."""
123 header = log.readline()
124 assert header == '# ninja log v5\n', \
125 'unrecognized ninja log version %r' % header
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000126 targets_dict = {}
127 last_end_seen = 0.0
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800128 for line in log:
129 parts = line.strip().split('\t')
130 if len(parts) != 5:
131 # If ninja.exe is rudely halted then the .ninja_log file may be
132 # corrupt. Silently continue.
133 continue
134 start, end, _, name, cmdhash = parts # Ignore restat.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000135 # Convert from integral milliseconds to float seconds.
136 start = int(start) / 1000.0
137 end = int(end) / 1000.0
138 if not show_all and end < last_end_seen:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800139 # An earlier time stamp means that this step is the first in a new
140 # build, possibly an incremental build. Throw away the previous
141 # data so that this new build will be displayed independently.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000142 # This has to be done by comparing end times because records are
143 # written to the .ninja_log file when commands complete, so end
144 # times are guaranteed to be in order, but start times are not.
145 targets_dict = {}
146 target = None
147 if cmdhash in targets_dict:
148 target = targets_dict[cmdhash]
149 if not show_all and (target.start != start or target.end != end):
150 # If several builds in a row just run one or two build steps then
151 # the end times may not go backwards so the last build may not be
152 # detected as such. However in many cases there will be a build step
153 # repeated in the two builds and the changed start/stop points for
154 # that command, identified by the hash, can be used to detect and
155 # reset the target dictionary.
156 targets_dict = {}
157 target = None
158 if not target:
159 targets_dict[cmdhash] = target = Target(start, end)
160 last_end_seen = end
161 target.targets.append(name)
162 return targets_dict.values()
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800163
164
165def GetExtension(target):
166 """Return the file extension that best represents a target.
167
168 For targets that generate multiple outputs it is important to return a
169 consistent 'canonical' extension. Ultimately the goal is to group build steps
170 by type."""
171 for output in target.targets:
172 # Normalize all mojo related outputs to 'mojo'.
173 if output.count('.mojom') > 0:
174 extension = 'mojo'
175 break
176 # Not a true extension, but a good grouping.
177 if output.endswith('type_mappings'):
178 extension = 'type_mappings'
179 break
180 extension = os.path.splitext(output)[1]
181 if len(extension) == 0:
182 extension = '(no extension found)'
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800183 if extension in ['.pdb', '.dll', '.exe']:
184 extension = 'PEFile (linking)'
185 # Make sure that .dll and .exe are grouped together and that the
186 # .dll.lib files don't cause these to be listed as libraries
187 break
Bruce Dawsone186e502018-02-12 15:41:11 -0800188 if extension in ['.so', '.TOC']:
189 extension = '.so (linking)'
190 # Attempt to identify linking, avoid identifying as '.TOC'
191 break
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800192 return extension
193
194
195def SummarizeEntries(entries):
196 """Print a summary of the passed in list of Target objects."""
197
198 # Create a list that is in order by time stamp and has entries for the
199 # beginning and ending of each build step (one time stamp may have multiple
200 # entries due to multiple steps starting/stopping at exactly the same time).
201 # Iterate through this list, keeping track of which tasks are running at all
202 # times. At each time step calculate a running total for weighted time so
203 # that when each task ends its own weighted time can easily be calculated.
204 task_start_stop_times = []
205
206 earliest = -1
207 latest = 0
208 total_cpu_time = 0
209 for target in entries:
210 if earliest < 0 or target.start < earliest:
211 earliest = target.start
212 if target.end > latest:
213 latest = target.end
214 total_cpu_time += target.Duration()
215 task_start_stop_times.append((target.start, 'start', target))
216 task_start_stop_times.append((target.end, 'stop', target))
217 length = latest - earliest
218 weighted_total = 0.0
219
220 task_start_stop_times.sort()
221 # Now we have all task start/stop times sorted by when they happen. If a
222 # task starts and stops on the same time stamp then the start will come
223 # first because of the alphabet, which is important for making this work
224 # correctly.
225 # Track the tasks which are currently running.
226 running_tasks = {}
227 # Record the time we have processed up to so we know how to calculate time
228 # deltas.
229 last_time = task_start_stop_times[0][0]
230 # Track the accumulated weighted time so that it can efficiently be added
231 # to individual tasks.
232 last_weighted_time = 0.0
233 # Scan all start/stop events.
234 for event in task_start_stop_times:
235 time, action_name, target = event
236 # Accumulate weighted time up to now.
237 num_running = len(running_tasks)
238 if num_running > 0:
239 # Update the total weighted time up to this moment.
240 last_weighted_time += (time - last_time) / float(num_running)
241 if action_name == 'start':
242 # Record the total weighted task time when this task starts.
243 running_tasks[target] = last_weighted_time
244 if action_name == 'stop':
245 # Record the change in the total weighted task time while this task ran.
246 weighted_duration = last_weighted_time - running_tasks[target]
247 target.SetWeightedDuration(weighted_duration)
248 weighted_total += weighted_duration
249 del running_tasks[target]
250 last_time = time
251 assert(len(running_tasks) == 0)
252
253 # Warn if the sum of weighted times is off by more than half a second.
254 if abs(length - weighted_total) > 500:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000255 print('Discrepancy!!! Length = %.3f, weighted total = %.3f' % (
256 length, weighted_total))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800257
258 # Print the slowest build steps (by weighted time).
Raul Tambre80ee78e2019-05-06 22:41:05 +0000259 print(' Longest build steps:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800260 entries.sort(key=lambda x: x.WeightedDuration())
261 for target in entries[-long_count:]:
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000262 print(' %8.1f weighted s to build %s (%.1f s elapsed time)' % (
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800263 target.WeightedDuration(),
Raul Tambre80ee78e2019-05-06 22:41:05 +0000264 target.DescribeTargets(), target.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800265
266 # Sum up the time by file extension/type of the output file
267 count_by_ext = {}
268 time_by_ext = {}
269 weighted_time_by_ext = {}
270 # Scan through all of the targets to build up per-extension statistics.
271 for target in entries:
272 extension = GetExtension(target)
273 time_by_ext[extension] = time_by_ext.get(extension, 0) + target.Duration()
274 weighted_time_by_ext[extension] = weighted_time_by_ext.get(extension,
275 0) + target.WeightedDuration()
276 count_by_ext[extension] = count_by_ext.get(extension, 0) + 1
277
Raul Tambre80ee78e2019-05-06 22:41:05 +0000278 print(' Time by build-step type:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800279 # Copy to a list with extension name and total time swapped, to (time, ext)
280 weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in
281 weighted_time_by_ext.items())
282 # Print the slowest build target types (by weighted time):
283 for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000284 print(' %8.1f s weighted time to generate %d %s files '
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000285 '(%1.1f s elapsed time sum)' % (time, count_by_ext[extension],
Raul Tambre80ee78e2019-05-06 22:41:05 +0000286 extension, time_by_ext[extension]))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800287
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000288 print(' %.1f s weighted time (%.1f s elapsed time sum, %1.1fx '
289 'parallelism)' % (length, total_cpu_time,
Raul Tambre80ee78e2019-05-06 22:41:05 +0000290 total_cpu_time * 1.0 / length))
291 print(' %d build steps completed, average of %1.2f/s' % (
292 len(entries), len(entries) / (length)))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800293
294
Daniel Bratella10370c2018-06-11 07:58:59 +0000295def main():
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800296 log_file = '.ninja_log'
Daniel Bratella10370c2018-06-11 07:58:59 +0000297 parser = argparse.ArgumentParser()
298 parser.add_argument('-C', dest='build_directory',
299 help='Build directory.')
300 parser.add_argument('--log-file',
301 help="specific ninja log file to analyze.")
302 args, _extra_args = parser.parse_known_args()
303 if args.build_directory:
304 log_file = os.path.join(args.build_directory, log_file)
305 if args.log_file:
306 log_file = args.log_file
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800307
308 try:
309 with open(log_file, 'r') as log:
310 entries = ReadTargets(log, False)
311 SummarizeEntries(entries)
312 except IOError:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000313 print('Log file %r not found, no build summary created.' % log_file)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800314 return errno.ENOENT
315
316
317if __name__ == '__main__':
Daniel Bratella10370c2018-06-11 07:58:59 +0000318 sys.exit(main())