blob: c5a868cbf5e1bc12280aa9462d9ffabc8b005bae [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
27[1 processes, 23/23 @ 0.9/s : 26.280s ] LINK(DLL) base.dll base.dll.lib
28 Longest build steps:
29 0.9 weighted s to build obj/base/base_jumbo_17.obj (5.0 s CPU time)
30 1.0 weighted s to build obj/base/base_jumbo_31.obj (13.1 s CPU time)
31 1.2 weighted s to build obj/base/base_jumbo_4.obj (14.7 s CPU time)
32 1.3 weighted s to build obj/base/base_jumbo_32.obj (15.0 s CPU time)
33 1.6 weighted s to build obj/base/base_jumbo_26.obj (17.1 s CPU time)
34 1.7 weighted s to build base.dll, base.dll.lib (1.7 s CPU time)
35 1.7 weighted s to build obj/base/base_jumbo_11.obj (15.9 s CPU time)
36 1.9 weighted s to build obj/base/base_jumbo_12.obj (18.5 s CPU time)
37 3.6 weighted s to build obj/base/base_jumbo_34.obj (20.1 s CPU time)
38 4.3 weighted s to build obj/base/base_jumbo_33.obj (22.3 s CPU time)
39 Time by build-step type:
40 0.1 s weighted time to generate 1 .c files (0.1 s CPU time)
41 0.1 s weighted time to generate 1 .stamp files (0.1 s CPU time)
42 0.2 s weighted time to generate 1 .h files (0.2 s CPU time)
43 1.7 s weighted time to generate 1 PEFile files (1.7 s CPU time)
44 24.3 s weighted time to generate 19 .obj files (233.4 s CPU time)
45 26.3 s weighted time (235.5 s CPU time, 9.0x parallelism)
46 23 build steps completed, average of 0.88/s
47
48If no gn clean has been done then results will be for the last non-NULL
49invocation of ninja. Ideas for future statistics, and implementations are
50appreciated.
51
52The "weighted" time is the elapsed time of each build step divided by the number
53of tasks that were running in parallel. This makes it an excellent approximation
54of how "important" a slow step was. A link that is entirely or mostly serialized
55will have a weighted time that is the same or similar to its elapsed time. A
56compile that runs in parallel with 999 other compiles will have a weighted time
57that is tiny."""
58
Raul Tambre80ee78e2019-05-06 22:41:05 +000059from __future__ import print_function
60
Daniel Bratella10370c2018-06-11 07:58:59 +000061import argparse
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080062import errno
63import os
64import sys
65
66
67# The number of long build times to report:
68long_count = 10
69# The number of long times by extension to report
70long_ext_count = 5
71
72
73class Target:
74 """Represents a single line read for a .ninja_log file."""
75 def __init__(self, start, end):
Bruce Dawson6be8afd2018-06-11 20:00:05 +000076 """Creates a target object by passing in the start/end times in seconds
77 as a float."""
78 self.start = start
79 self.end = end
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080080 # A list of targets, appended to by the owner of this object.
81 self.targets = []
82 self.weighted_duration = 0.0
83
84 def Duration(self):
85 """Returns the task duration in seconds as a float."""
86 return self.end - self.start
87
88 def SetWeightedDuration(self, weighted_duration):
89 """Sets the duration, in seconds, passed in as a float."""
90 self.weighted_duration = weighted_duration
91
92 def WeightedDuration(self):
93 """Returns the task's weighted duration in seconds as a float.
94
95 Weighted_duration takes the elapsed time of the task and divides it
96 by how many other tasks were running at the same time. Thus, it
97 represents the approximate impact of this task on the total build time,
98 with serialized or serializing steps typically ending up with much
99 longer weighted durations.
100 weighted_duration should always be the same or shorter than duration.
101 """
102 # Allow for modest floating-point errors
103 epsilon = 0.000002
104 if (self.weighted_duration > self.Duration() + epsilon):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000105 print('%s > %s?' % (self.weighted_duration, self.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800106 assert(self.weighted_duration <= self.Duration() + epsilon)
107 return self.weighted_duration
108
109 def DescribeTargets(self):
110 """Returns a printable string that summarizes the targets."""
111 if len(self.targets) == 1:
112 return self.targets[0]
113 # Some build steps generate dozens of outputs - handle them sanely.
114 # It's a bit odd that if there are three targets we return all three
115 # but if there are more than three we just return two, but this works
116 # well in practice.
117 elif len(self.targets) > 3:
118 return '(%d items) ' % len(self.targets) + (
119 ', '.join(self.targets[:2]) + ', ...')
120 else:
121 return ', '.join(self.targets)
122
123
124# Copied with some modifications from ninjatracing
125def ReadTargets(log, show_all):
126 """Reads all targets from .ninja_log file |log_file|, sorted by duration.
127
128 The result is a list of Target objects."""
129 header = log.readline()
130 assert header == '# ninja log v5\n', \
131 'unrecognized ninja log version %r' % header
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000132 targets_dict = {}
133 last_end_seen = 0.0
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800134 for line in log:
135 parts = line.strip().split('\t')
136 if len(parts) != 5:
137 # If ninja.exe is rudely halted then the .ninja_log file may be
138 # corrupt. Silently continue.
139 continue
140 start, end, _, name, cmdhash = parts # Ignore restat.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000141 # Convert from integral milliseconds to float seconds.
142 start = int(start) / 1000.0
143 end = int(end) / 1000.0
144 if not show_all and end < last_end_seen:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800145 # An earlier time stamp means that this step is the first in a new
146 # build, possibly an incremental build. Throw away the previous
147 # data so that this new build will be displayed independently.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000148 # This has to be done by comparing end times because records are
149 # written to the .ninja_log file when commands complete, so end
150 # times are guaranteed to be in order, but start times are not.
151 targets_dict = {}
152 target = None
153 if cmdhash in targets_dict:
154 target = targets_dict[cmdhash]
155 if not show_all and (target.start != start or target.end != end):
156 # If several builds in a row just run one or two build steps then
157 # the end times may not go backwards so the last build may not be
158 # detected as such. However in many cases there will be a build step
159 # repeated in the two builds and the changed start/stop points for
160 # that command, identified by the hash, can be used to detect and
161 # reset the target dictionary.
162 targets_dict = {}
163 target = None
164 if not target:
165 targets_dict[cmdhash] = target = Target(start, end)
166 last_end_seen = end
167 target.targets.append(name)
168 return targets_dict.values()
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800169
170
171def GetExtension(target):
172 """Return the file extension that best represents a target.
173
174 For targets that generate multiple outputs it is important to return a
175 consistent 'canonical' extension. Ultimately the goal is to group build steps
176 by type."""
177 for output in target.targets:
178 # Normalize all mojo related outputs to 'mojo'.
179 if output.count('.mojom') > 0:
180 extension = 'mojo'
181 break
182 # Not a true extension, but a good grouping.
183 if output.endswith('type_mappings'):
184 extension = 'type_mappings'
185 break
186 extension = os.path.splitext(output)[1]
187 if len(extension) == 0:
188 extension = '(no extension found)'
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800189 if extension 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
Bruce Dawsone186e502018-02-12 15:41:11 -0800194 if extension in ['.so', '.TOC']:
195 extension = '.so (linking)'
196 # Attempt to identify linking, avoid identifying as '.TOC'
197 break
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800198 return extension
199
200
201def SummarizeEntries(entries):
202 """Print a summary of the passed in list of Target objects."""
203
204 # Create a list that is in order by time stamp and has entries for the
205 # beginning and ending of each build step (one time stamp may have multiple
206 # entries due to multiple steps starting/stopping at exactly the same time).
207 # Iterate through this list, keeping track of which tasks are running at all
208 # times. At each time step calculate a running total for weighted time so
209 # that when each task ends its own weighted time can easily be calculated.
210 task_start_stop_times = []
211
212 earliest = -1
213 latest = 0
214 total_cpu_time = 0
215 for target in entries:
216 if earliest < 0 or target.start < earliest:
217 earliest = target.start
218 if target.end > latest:
219 latest = target.end
220 total_cpu_time += target.Duration()
221 task_start_stop_times.append((target.start, 'start', target))
222 task_start_stop_times.append((target.end, 'stop', target))
223 length = latest - earliest
224 weighted_total = 0.0
225
226 task_start_stop_times.sort()
227 # Now we have all task start/stop times sorted by when they happen. If a
228 # task starts and stops on the same time stamp then the start will come
229 # first because of the alphabet, which is important for making this work
230 # correctly.
231 # Track the tasks which are currently running.
232 running_tasks = {}
233 # Record the time we have processed up to so we know how to calculate time
234 # deltas.
235 last_time = task_start_stop_times[0][0]
236 # Track the accumulated weighted time so that it can efficiently be added
237 # to individual tasks.
238 last_weighted_time = 0.0
239 # Scan all start/stop events.
240 for event in task_start_stop_times:
241 time, action_name, target = event
242 # Accumulate weighted time up to now.
243 num_running = len(running_tasks)
244 if num_running > 0:
245 # Update the total weighted time up to this moment.
246 last_weighted_time += (time - last_time) / float(num_running)
247 if action_name == 'start':
248 # Record the total weighted task time when this task starts.
249 running_tasks[target] = last_weighted_time
250 if action_name == 'stop':
251 # Record the change in the total weighted task time while this task ran.
252 weighted_duration = last_weighted_time - running_tasks[target]
253 target.SetWeightedDuration(weighted_duration)
254 weighted_total += weighted_duration
255 del running_tasks[target]
256 last_time = time
257 assert(len(running_tasks) == 0)
258
259 # Warn if the sum of weighted times is off by more than half a second.
260 if abs(length - weighted_total) > 500:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000261 print('Discrepancy!!! Length = %.3f, weighted total = %.3f' % (
262 length, weighted_total))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800263
264 # Print the slowest build steps (by weighted time).
Raul Tambre80ee78e2019-05-06 22:41:05 +0000265 print(' Longest build steps:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800266 entries.sort(key=lambda x: x.WeightedDuration())
267 for target in entries[-long_count:]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000268 print(' %8.1f weighted s to build %s (%.1f s CPU time)' % (
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800269 target.WeightedDuration(),
Raul Tambre80ee78e2019-05-06 22:41:05 +0000270 target.DescribeTargets(), target.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800271
272 # Sum up the time by file extension/type of the output file
273 count_by_ext = {}
274 time_by_ext = {}
275 weighted_time_by_ext = {}
276 # Scan through all of the targets to build up per-extension statistics.
277 for target in entries:
278 extension = GetExtension(target)
279 time_by_ext[extension] = time_by_ext.get(extension, 0) + target.Duration()
280 weighted_time_by_ext[extension] = weighted_time_by_ext.get(extension,
281 0) + target.WeightedDuration()
282 count_by_ext[extension] = count_by_ext.get(extension, 0) + 1
283
Raul Tambre80ee78e2019-05-06 22:41:05 +0000284 print(' Time by build-step type:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800285 # Copy to a list with extension name and total time swapped, to (time, ext)
286 weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in
287 weighted_time_by_ext.items())
288 # Print the slowest build target types (by weighted time):
289 for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000290 print(' %8.1f s weighted time to generate %d %s files '
291 '(%1.1f s CPU time)' % (time, count_by_ext[extension],
292 extension, time_by_ext[extension]))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800293
Raul Tambre80ee78e2019-05-06 22:41:05 +0000294 print(' %.1f s weighted time (%.1f s CPU time, %1.1fx parallelism)' % (
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800295 length, total_cpu_time,
Raul Tambre80ee78e2019-05-06 22:41:05 +0000296 total_cpu_time * 1.0 / length))
297 print(' %d build steps completed, average of %1.2f/s' % (
298 len(entries), len(entries) / (length)))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800299
300
Daniel Bratella10370c2018-06-11 07:58:59 +0000301def main():
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800302 log_file = '.ninja_log'
Daniel Bratella10370c2018-06-11 07:58:59 +0000303 parser = argparse.ArgumentParser()
304 parser.add_argument('-C', dest='build_directory',
305 help='Build directory.')
306 parser.add_argument('--log-file',
307 help="specific ninja log file to analyze.")
308 args, _extra_args = parser.parse_known_args()
309 if args.build_directory:
310 log_file = os.path.join(args.build_directory, log_file)
311 if args.log_file:
312 log_file = args.log_file
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800313
314 try:
315 with open(log_file, 'r') as log:
316 entries = ReadTargets(log, False)
317 SummarizeEntries(entries)
318 except IOError:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000319 print('Log file %r not found, no build summary created.' % log_file)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800320 return errno.ENOENT
321
322
323if __name__ == '__main__':
Daniel Bratella10370c2018-06-11 07:58:59 +0000324 sys.exit(main())