blob: aa7e957c50ad3139b836139d9b0829c3ddac2990 [file] [log] [blame]
Edward Lemur59a3b2f2020-01-14 01:50:50 +00001#!/usr/bin/env vpython
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
20> python post_build_ninja_summary.py -C out/Default
21
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
Raul Tambre80ee78e2019-05-06 22:41:05 +000054from __future__ import print_function
55
Daniel Bratella10370c2018-06-11 07:58:59 +000056import argparse
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080057import errno
58import os
59import sys
60
61
62# The number of long build times to report:
63long_count = 10
64# The number of long times by extension to report
65long_ext_count = 5
66
67
68class Target:
69 """Represents a single line read for a .ninja_log file."""
70 def __init__(self, start, end):
Bruce Dawson6be8afd2018-06-11 20:00:05 +000071 """Creates a target object by passing in the start/end times in seconds
72 as a float."""
73 self.start = start
74 self.end = end
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -080075 # A list of targets, appended to by the owner of this object.
76 self.targets = []
77 self.weighted_duration = 0.0
78
79 def Duration(self):
80 """Returns the task duration in seconds as a float."""
81 return self.end - self.start
82
83 def SetWeightedDuration(self, weighted_duration):
84 """Sets the duration, in seconds, passed in as a float."""
85 self.weighted_duration = weighted_duration
86
87 def WeightedDuration(self):
88 """Returns the task's weighted duration in seconds as a float.
89
90 Weighted_duration takes the elapsed time of the task and divides it
91 by how many other tasks were running at the same time. Thus, it
92 represents the approximate impact of this task on the total build time,
93 with serialized or serializing steps typically ending up with much
94 longer weighted durations.
95 weighted_duration should always be the same or shorter than duration.
96 """
97 # Allow for modest floating-point errors
98 epsilon = 0.000002
99 if (self.weighted_duration > self.Duration() + epsilon):
Raul Tambre80ee78e2019-05-06 22:41:05 +0000100 print('%s > %s?' % (self.weighted_duration, self.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800101 assert(self.weighted_duration <= self.Duration() + epsilon)
102 return self.weighted_duration
103
104 def DescribeTargets(self):
105 """Returns a printable string that summarizes the targets."""
106 if len(self.targets) == 1:
107 return self.targets[0]
108 # Some build steps generate dozens of outputs - handle them sanely.
109 # It's a bit odd that if there are three targets we return all three
110 # but if there are more than three we just return two, but this works
111 # well in practice.
112 elif len(self.targets) > 3:
113 return '(%d items) ' % len(self.targets) + (
114 ', '.join(self.targets[:2]) + ', ...')
115 else:
116 return ', '.join(self.targets)
117
118
119# Copied with some modifications from ninjatracing
120def ReadTargets(log, show_all):
121 """Reads all targets from .ninja_log file |log_file|, sorted by duration.
122
123 The result is a list of Target objects."""
124 header = log.readline()
125 assert header == '# ninja log v5\n', \
126 'unrecognized ninja log version %r' % header
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000127 targets_dict = {}
128 last_end_seen = 0.0
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800129 for line in log:
130 parts = line.strip().split('\t')
131 if len(parts) != 5:
132 # If ninja.exe is rudely halted then the .ninja_log file may be
133 # corrupt. Silently continue.
134 continue
135 start, end, _, name, cmdhash = parts # Ignore restat.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000136 # Convert from integral milliseconds to float seconds.
137 start = int(start) / 1000.0
138 end = int(end) / 1000.0
139 if not show_all and end < last_end_seen:
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800140 # An earlier time stamp means that this step is the first in a new
141 # build, possibly an incremental build. Throw away the previous
142 # data so that this new build will be displayed independently.
Bruce Dawson6be8afd2018-06-11 20:00:05 +0000143 # This has to be done by comparing end times because records are
144 # written to the .ninja_log file when commands complete, so end
145 # times are guaranteed to be in order, but start times are not.
146 targets_dict = {}
147 target = None
148 if cmdhash in targets_dict:
149 target = targets_dict[cmdhash]
150 if not show_all and (target.start != start or target.end != end):
151 # If several builds in a row just run one or two build steps then
152 # the end times may not go backwards so the last build may not be
153 # detected as such. However in many cases there will be a build step
154 # repeated in the two builds and the changed start/stop points for
155 # that command, identified by the hash, can be used to detect and
156 # reset the target dictionary.
157 targets_dict = {}
158 target = None
159 if not target:
160 targets_dict[cmdhash] = target = Target(start, end)
161 last_end_seen = end
162 target.targets.append(name)
163 return targets_dict.values()
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800164
165
166def GetExtension(target):
167 """Return the file extension that best represents a target.
168
169 For targets that generate multiple outputs it is important to return a
170 consistent 'canonical' extension. Ultimately the goal is to group build steps
171 by type."""
172 for output in target.targets:
173 # Normalize all mojo related outputs to 'mojo'.
174 if output.count('.mojom') > 0:
175 extension = 'mojo'
176 break
177 # Not a true extension, but a good grouping.
178 if output.endswith('type_mappings'):
179 extension = 'type_mappings'
180 break
181 extension = os.path.splitext(output)[1]
182 if len(extension) == 0:
183 extension = '(no extension found)'
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800184 if extension in ['.pdb', '.dll', '.exe']:
185 extension = 'PEFile (linking)'
186 # Make sure that .dll and .exe are grouped together and that the
187 # .dll.lib files don't cause these to be listed as libraries
188 break
Bruce Dawsone186e502018-02-12 15:41:11 -0800189 if extension in ['.so', '.TOC']:
190 extension = '.so (linking)'
191 # Attempt to identify linking, avoid identifying as '.TOC'
192 break
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800193 return extension
194
195
196def SummarizeEntries(entries):
197 """Print a summary of the passed in list of Target objects."""
198
199 # Create a list that is in order by time stamp and has entries for the
200 # beginning and ending of each build step (one time stamp may have multiple
201 # entries due to multiple steps starting/stopping at exactly the same time).
202 # Iterate through this list, keeping track of which tasks are running at all
203 # times. At each time step calculate a running total for weighted time so
204 # that when each task ends its own weighted time can easily be calculated.
205 task_start_stop_times = []
206
207 earliest = -1
208 latest = 0
209 total_cpu_time = 0
210 for target in entries:
211 if earliest < 0 or target.start < earliest:
212 earliest = target.start
213 if target.end > latest:
214 latest = target.end
215 total_cpu_time += target.Duration()
216 task_start_stop_times.append((target.start, 'start', target))
217 task_start_stop_times.append((target.end, 'stop', target))
218 length = latest - earliest
219 weighted_total = 0.0
220
221 task_start_stop_times.sort()
222 # Now we have all task start/stop times sorted by when they happen. If a
223 # task starts and stops on the same time stamp then the start will come
224 # first because of the alphabet, which is important for making this work
225 # correctly.
226 # Track the tasks which are currently running.
227 running_tasks = {}
228 # Record the time we have processed up to so we know how to calculate time
229 # deltas.
230 last_time = task_start_stop_times[0][0]
231 # Track the accumulated weighted time so that it can efficiently be added
232 # to individual tasks.
233 last_weighted_time = 0.0
234 # Scan all start/stop events.
235 for event in task_start_stop_times:
236 time, action_name, target = event
237 # Accumulate weighted time up to now.
238 num_running = len(running_tasks)
239 if num_running > 0:
240 # Update the total weighted time up to this moment.
241 last_weighted_time += (time - last_time) / float(num_running)
242 if action_name == 'start':
243 # Record the total weighted task time when this task starts.
244 running_tasks[target] = last_weighted_time
245 if action_name == 'stop':
246 # Record the change in the total weighted task time while this task ran.
247 weighted_duration = last_weighted_time - running_tasks[target]
248 target.SetWeightedDuration(weighted_duration)
249 weighted_total += weighted_duration
250 del running_tasks[target]
251 last_time = time
252 assert(len(running_tasks) == 0)
253
254 # Warn if the sum of weighted times is off by more than half a second.
255 if abs(length - weighted_total) > 500:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000256 print('Discrepancy!!! Length = %.3f, weighted total = %.3f' % (
257 length, weighted_total))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800258
259 # Print the slowest build steps (by weighted time).
Raul Tambre80ee78e2019-05-06 22:41:05 +0000260 print(' Longest build steps:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800261 entries.sort(key=lambda x: x.WeightedDuration())
262 for target in entries[-long_count:]:
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000263 print(' %8.1f weighted s to build %s (%.1f s elapsed time)' % (
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800264 target.WeightedDuration(),
Raul Tambre80ee78e2019-05-06 22:41:05 +0000265 target.DescribeTargets(), target.Duration()))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800266
267 # Sum up the time by file extension/type of the output file
268 count_by_ext = {}
269 time_by_ext = {}
270 weighted_time_by_ext = {}
271 # Scan through all of the targets to build up per-extension statistics.
272 for target in entries:
273 extension = GetExtension(target)
274 time_by_ext[extension] = time_by_ext.get(extension, 0) + target.Duration()
275 weighted_time_by_ext[extension] = weighted_time_by_ext.get(extension,
276 0) + target.WeightedDuration()
277 count_by_ext[extension] = count_by_ext.get(extension, 0) + 1
278
Raul Tambre80ee78e2019-05-06 22:41:05 +0000279 print(' Time by build-step type:')
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800280 # Copy to a list with extension name and total time swapped, to (time, ext)
281 weighted_time_by_ext_sorted = sorted((y, x) for (x, y) in
282 weighted_time_by_ext.items())
283 # Print the slowest build target types (by weighted time):
284 for time, extension in weighted_time_by_ext_sorted[-long_ext_count:]:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000285 print(' %8.1f s weighted time to generate %d %s files '
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000286 '(%1.1f s elapsed time sum)' % (time, count_by_ext[extension],
Raul Tambre80ee78e2019-05-06 22:41:05 +0000287 extension, time_by_ext[extension]))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800288
Bruce Dawson0e9afd22019-11-08 18:57:08 +0000289 print(' %.1f s weighted time (%.1f s elapsed time sum, %1.1fx '
290 'parallelism)' % (length, total_cpu_time,
Raul Tambre80ee78e2019-05-06 22:41:05 +0000291 total_cpu_time * 1.0 / length))
292 print(' %d build steps completed, average of %1.2f/s' % (
293 len(entries), len(entries) / (length)))
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800294
295
Daniel Bratella10370c2018-06-11 07:58:59 +0000296def main():
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800297 log_file = '.ninja_log'
Daniel Bratella10370c2018-06-11 07:58:59 +0000298 parser = argparse.ArgumentParser()
299 parser.add_argument('-C', dest='build_directory',
300 help='Build directory.')
301 parser.add_argument('--log-file',
302 help="specific ninja log file to analyze.")
303 args, _extra_args = parser.parse_known_args()
304 if args.build_directory:
305 log_file = os.path.join(args.build_directory, log_file)
306 if args.log_file:
307 log_file = args.log_file
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800308
309 try:
310 with open(log_file, 'r') as log:
311 entries = ReadTargets(log, False)
312 SummarizeEntries(entries)
313 except IOError:
Raul Tambre80ee78e2019-05-06 22:41:05 +0000314 print('Log file %r not found, no build summary created.' % log_file)
Bruce Dawsonffc0c7c2018-02-07 18:00:48 -0800315 return errno.ENOENT
316
317
318if __name__ == '__main__':
Daniel Bratella10370c2018-06-11 07:58:59 +0000319 sys.exit(main())