blob: ecd973ccdd69d2c2f7f58aa26f7ea5563397631b [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
10this script just set NINJA_SUMMARIZE_BUILD=1 and run autoninja.bat.
11
12You can also call this script directly using ninja's syntax to specify the
13output directory of interest:
14
15> python post_build_ninja_summary.py -C out/Default
16
17Typical output looks like this:
18
19>ninja -C out\debug_component base
20ninja.exe -C out\debug_component base -j 960 -l 48 -d keeprsp
21ninja: Entering directory `out\debug_component'
22[1 processes, 1/1 @ 0.3/s : 3.092s ] Regenerating ninja files
23[1 processes, 23/23 @ 0.9/s : 26.280s ] LINK(DLL) base.dll base.dll.lib
24 Longest build steps:
25 0.9 weighted s to build obj/base/base_jumbo_17.obj (5.0 s CPU time)
26 1.0 weighted s to build obj/base/base_jumbo_31.obj (13.1 s CPU time)
27 1.2 weighted s to build obj/base/base_jumbo_4.obj (14.7 s CPU time)
28 1.3 weighted s to build obj/base/base_jumbo_32.obj (15.0 s CPU time)
29 1.6 weighted s to build obj/base/base_jumbo_26.obj (17.1 s CPU time)
30 1.7 weighted s to build base.dll, base.dll.lib (1.7 s CPU time)
31 1.7 weighted s to build obj/base/base_jumbo_11.obj (15.9 s CPU time)
32 1.9 weighted s to build obj/base/base_jumbo_12.obj (18.5 s CPU time)
33 3.6 weighted s to build obj/base/base_jumbo_34.obj (20.1 s CPU time)
34 4.3 weighted s to build obj/base/base_jumbo_33.obj (22.3 s CPU time)
35 Time by build-step type:
36 0.1 s weighted time to generate 1 .c files (0.1 s CPU time)
37 0.1 s weighted time to generate 1 .stamp files (0.1 s CPU time)
38 0.2 s weighted time to generate 1 .h files (0.2 s CPU time)
39 1.7 s weighted time to generate 1 PEFile files (1.7 s CPU time)
40 24.3 s weighted time to generate 19 .obj files (233.4 s CPU time)
41 26.3 s weighted time (235.5 s CPU time, 9.0x parallelism)
42 23 build steps completed, average of 0.88/s
43
44If no gn clean has been done then results will be for the last non-NULL
45invocation of ninja. Ideas for future statistics, and implementations are
46appreciated.
47
48The "weighted" time is the elapsed time of each build step divided by the number
49of tasks that were running in parallel. This makes it an excellent approximation
50of how "important" a slow step was. A link that is entirely or mostly serialized
51will have a weighted time that is the same or similar to its elapsed time. A
52compile that runs in parallel with 999 other compiles will have a weighted time
53that is tiny."""
54
55import errno
56import os
57import sys
58
59
60# The number of long build times to report:
61long_count = 10
62# The number of long times by extension to report
63long_ext_count = 5
64
65
66class Target:
67 """Represents a single line read for a .ninja_log file."""
68 def __init__(self, start, end):
69 """Creates a target object by passing in the start/end times in ms."""
70 # Convert from milliseconds to seconds.
71 self.start = int(start) / 1000.0
72 self.end = int(end) / 1000.0
73 # A list of targets, appended to by the owner of this object.
74 self.targets = []
75 self.weighted_duration = 0.0
76
77 def Duration(self):
78 """Returns the task duration in seconds as a float."""
79 return self.end - self.start
80
81 def SetWeightedDuration(self, weighted_duration):
82 """Sets the duration, in seconds, passed in as a float."""
83 self.weighted_duration = weighted_duration
84
85 def WeightedDuration(self):
86 """Returns the task's weighted duration in seconds as a float.
87
88 Weighted_duration takes the elapsed time of the task and divides it
89 by how many other tasks were running at the same time. Thus, it
90 represents the approximate impact of this task on the total build time,
91 with serialized or serializing steps typically ending up with much
92 longer weighted durations.
93 weighted_duration should always be the same or shorter than duration.
94 """
95 # Allow for modest floating-point errors
96 epsilon = 0.000002
97 if (self.weighted_duration > self.Duration() + epsilon):
98 print '%s > %s?' % (self.weighted_duration, self.Duration())
99 assert(self.weighted_duration <= self.Duration() + epsilon)
100 return self.weighted_duration
101
102 def DescribeTargets(self):
103 """Returns a printable string that summarizes the targets."""
104 if len(self.targets) == 1:
105 return self.targets[0]
106 # Some build steps generate dozens of outputs - handle them sanely.
107 # It's a bit odd that if there are three targets we return all three
108 # but if there are more than three we just return two, but this works
109 # well in practice.
110 elif len(self.targets) > 3:
111 return '(%d items) ' % len(self.targets) + (
112 ', '.join(self.targets[:2]) + ', ...')
113 else:
114 return ', '.join(self.targets)
115
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
125 targets = {}
126 last_end_seen = 0
127 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.
134 if not show_all and int(end) < last_end_seen:
135 # An earlier time stamp means that this step is the first in a new
136 # build, possibly an incremental build. Throw away the previous
137 # data so that this new build will be displayed independently.
138 targets = {}
139 last_end_seen = int(end)
140 targets.setdefault(cmdhash, Target(start, end)).targets.append(name)
141 return targets.values()
142
143
144def GetExtension(target):
145 """Return the file extension that best represents a target.
146
147 For targets that generate multiple outputs it is important to return a
148 consistent 'canonical' extension. Ultimately the goal is to group build steps
149 by type."""
150 for output in target.targets:
151 # Normalize all mojo related outputs to 'mojo'.
152 if output.count('.mojom') > 0:
153 extension = 'mojo'
154 break
155 # Not a true extension, but a good grouping.
156 if output.endswith('type_mappings'):
157 extension = 'type_mappings'
158 break
159 extension = os.path.splitext(output)[1]
160 if len(extension) == 0:
161 extension = '(no extension found)'
162 # AHEM____.TTF fails this check.
163 assert(extension == extension.lower() or extension == '.TTF')
164 if extension in ['.pdb', '.dll', '.exe']:
165 extension = 'PEFile (linking)'
166 # Make sure that .dll and .exe are grouped together and that the
167 # .dll.lib files don't cause these to be listed as libraries
168 break
169 return extension
170
171
172def SummarizeEntries(entries):
173 """Print a summary of the passed in list of Target objects."""
174
175 # Create a list that is in order by time stamp and has entries for the
176 # beginning and ending of each build step (one time stamp may have multiple
177 # entries due to multiple steps starting/stopping at exactly the same time).
178 # Iterate through this list, keeping track of which tasks are running at all
179 # times. At each time step calculate a running total for weighted time so
180 # that when each task ends its own weighted time can easily be calculated.
181 task_start_stop_times = []
182
183 earliest = -1
184 latest = 0
185 total_cpu_time = 0
186 for target in entries:
187 if earliest < 0 or target.start < earliest:
188 earliest = target.start
189 if target.end > latest:
190 latest = target.end
191 total_cpu_time += target.Duration()
192 task_start_stop_times.append((target.start, 'start', target))
193 task_start_stop_times.append((target.end, 'stop', target))
194 length = latest - earliest
195 weighted_total = 0.0
196
197 task_start_stop_times.sort()
198 # Now we have all task start/stop times sorted by when they happen. If a
199 # task starts and stops on the same time stamp then the start will come
200 # first because of the alphabet, which is important for making this work
201 # correctly.
202 # Track the tasks which are currently running.
203 running_tasks = {}
204 # Record the time we have processed up to so we know how to calculate time
205 # deltas.
206 last_time = task_start_stop_times[0][0]
207 # Track the accumulated weighted time so that it can efficiently be added
208 # to individual tasks.
209 last_weighted_time = 0.0
210 # Scan all start/stop events.
211 for event in task_start_stop_times:
212 time, action_name, target = event
213 # Accumulate weighted time up to now.
214 num_running = len(running_tasks)
215 if num_running > 0:
216 # Update the total weighted time up to this moment.
217 last_weighted_time += (time - last_time) / float(num_running)
218 if action_name == 'start':
219 # Record the total weighted task time when this task starts.
220 running_tasks[target] = last_weighted_time
221 if action_name == 'stop':
222 # Record the change in the total weighted task time while this task ran.
223 weighted_duration = last_weighted_time - running_tasks[target]
224 target.SetWeightedDuration(weighted_duration)
225 weighted_total += weighted_duration
226 del running_tasks[target]
227 last_time = time
228 assert(len(running_tasks) == 0)
229
230 # Warn if the sum of weighted times is off by more than half a second.
231 if abs(length - weighted_total) > 500:
232 print 'Discrepancy!!! Length = %.3f, weighted total = %.3f' % (
233 length, weighted_total)
234
235 # Print the slowest build steps (by weighted time).
236 print ' Longest build steps:'
237 entries.sort(key=lambda x: x.WeightedDuration())
238 for target in entries[-long_count:]:
239 print ' %8.1f weighted s to build %s (%.1f s CPU time)' % (
240 target.WeightedDuration(),
241 target.DescribeTargets(), target.Duration())
242
243 # Sum up the time by file extension/type of the output file
244 count_by_ext = {}
245 time_by_ext = {}
246 weighted_time_by_ext = {}
247 # Scan through all of the targets to build up per-extension statistics.
248 for target in entries:
249 extension = GetExtension(target)
250 time_by_ext[extension] = time_by_ext.get(extension, 0) + target.Duration()
251 weighted_time_by_ext[extension] = weighted_time_by_ext.get(extension,
252 0) + target.WeightedDuration()
253 count_by_ext[extension] = count_by_ext.get(extension, 0) + 1
254
255 print ' Time by build-step type:'
256 # Copy to a list with extension name and total time swapped, to (time, ext)
257 weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in
258 weighted_time_by_ext.items())
259 # Print the slowest build target types (by weighted time):
260 for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]:
261 print (' %8.1f s weighted time to generate %d %s files '
262 '(%1.1f s CPU time)') % (time, count_by_ext[extension],
263 extension, time_by_ext[extension])
264
265 print ' %.1f s weighted time (%.1f s CPU time, %1.1fx parallelism)' % (
266 length, total_cpu_time,
267 total_cpu_time * 1.0 / length)
268 print ' %d build steps completed, average of %1.2f/s' % (
269 len(entries), len(entries) / (length))
270
271
272def main(argv):
273 log_file = '.ninja_log'
274 for pid, arg in enumerate(argv):
275 if arg == '-C':
276 log_file = os.path.join(argv[pid+1], log_file)
277
278 try:
279 with open(log_file, 'r') as log:
280 entries = ReadTargets(log, False)
281 SummarizeEntries(entries)
282 except IOError:
283 print 'No log file found, no build summary created.'
284 return errno.ENOENT
285
286
287if __name__ == '__main__':
288 sys.exit(main(sys.argv[1:]))