-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathninjatracing
189 lines (157 loc) · 6.91 KB
/
ninjatracing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
#!/usr/bin/env python
# Copyright 2018 Nico Weber
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Converts one (or several) .ninja_log files into chrome's about:tracing format.
If clang -ftime-trace .json files are found adjacent to generated files they
are embedded.
Usage:
ninja -C $BUILDDIR
ninjatracing $BUILDDIR/.ninja_log > trace.json
"""
from __future__ import print_function
import json
import os
import optparse
import re
import sys
class Target:
"""Represents a single line read for a .ninja_log file. Start and end times
are milliseconds."""
def __init__(self, start, end):
self.start = int(start)
self.end = int(end)
self.targets = []
def read_targets(log, show_all):
"""Reads all targets from .ninja_log file |log_file|, sorted by start
time"""
header = log.readline()
m = re.search(r'^# ninja log v(\d+)\n$', header)
assert m, "unrecognized ninja log version %r" % header
version = int(m.group(1))
assert 5 <= version <= 6, "unsupported ninja log version %d" % version
if version == 6:
# Skip header line
next(log)
targets = {}
last_end_seen = 0
for line in log:
start, end, _, name, cmdhash = line.strip().split('\t') # Ignore restat.
if not show_all and int(end) < last_end_seen:
# An earlier time stamp means that this step is the first in a new
# build, possibly an incremental build. Throw away the previous data
# so that this new build will be displayed independently.
targets = {}
last_end_seen = int(end)
targets.setdefault(cmdhash, Target(start, end)).targets.append(name)
return sorted(targets.values(), key=lambda job: job.end, reverse=True)
class Threads:
"""Tries to reconstruct the parallelism from a .ninja_log"""
def __init__(self):
self.workers = [] # Maps thread id to time that thread is occupied for.
def alloc(self, target):
"""Places target in an available thread, or adds a new thread."""
for worker in range(len(self.workers)):
if self.workers[worker] >= target.end:
self.workers[worker] = target.start
return worker
self.workers.append(target.start)
return len(self.workers) - 1
def read_events(trace, options):
"""Reads all events from time-trace json file |trace|."""
trace_data = json.load(trace)
def include_event(event, options):
"""Only include events if they are complete events, are longer than
granularity, and are not totals."""
return ((event['ph'] == 'X') and
(event['dur'] >= options['granularity']) and
(not event['name'].startswith('Total')))
return [x for x in trace_data['traceEvents'] if include_event(x, options)]
def trace_to_dicts(target, trace, options, pid, tid):
"""Read a file-like object |trace| containing -ftime-trace data and yields
about:tracing dict per eligible event in that log."""
ninja_time = (target.end - target.start) * 1000
for event in read_events(trace, options):
# Check if any event duration is greater than the duration from ninja.
if event['dur'] > ninja_time:
print("Inconsistent timing found (clang time > ninja time). Please"
" ensure that timings are from consistent builds.")
sys.exit(1)
# Set tid and pid from ninja log.
event['pid'] = pid
event['tid'] = tid
# Offset trace time stamp by ninja start time.
event['ts'] += (target.start * 1000)
yield event
def embed_time_trace(ninja_log_dir, target, pid, tid, options):
"""Produce time trace output for the specified ninja target. Expects
time-trace file to be in .json file named based on .o file."""
for t in target.targets:
o_path = os.path.join(ninja_log_dir, t)
json_trace_path = os.path.splitext(o_path)[0] + '.json'
try:
with open(json_trace_path, 'r') as trace:
for time_trace_event in trace_to_dicts(target, trace, options,
pid, tid):
yield time_trace_event
except IOError:
pass
def log_to_dicts(log, pid, options):
"""Reads a file-like object |log| containing a .ninja_log, and yields one
about:tracing dict per command found in the log."""
threads = Threads()
for target in read_targets(log, options['showall']):
tid = threads.alloc(target)
yield {
'name': '%0s' % ', '.join(target.targets), 'cat': 'targets',
'ph': 'X', 'ts': (target.start * 1000),
'dur': ((target.end - target.start) * 1000),
'pid': pid, 'tid': tid, 'args': {},
}
if options.get('embed_time_trace', False):
# Add time-trace information into the ninja trace.
try:
ninja_log_dir = os.path.dirname(log.name)
except AttributeError:
continue
for time_trace in embed_time_trace(ninja_log_dir, target, pid,
tid, options):
yield time_trace
def main(argv):
usage = __doc__
parser = optparse.OptionParser(usage)
parser.add_option('-a', '--showall', action='store_true', dest='showall',
default=False,
help='report on last build step for all outputs. Default '
'is to report just on the last (possibly incremental) '
'build')
parser.add_option('-g', '--granularity', type='int', default=50000,
dest='granularity',
help='minimum length time-trace event to embed in '
'microseconds. Default: %default')
parser.add_option('-e', '--embed-time-trace', action='store_true',
default=False, dest='embed_time_trace',
help='embed clang -ftime-trace json file found adjacent '
'to a target file')
(options, args) = parser.parse_args()
if len(args) == 0:
print('Must specify at least one .ninja_log file')
parser.print_help()
return 1
entries = []
for pid, log_file in enumerate(args):
with open(log_file, 'r') as log:
entries += list(log_to_dicts(log, pid, vars(options)))
json.dump(entries, sys.stdout)
if __name__ == '__main__':
sys.exit(main(sys.argv[1:]))