-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcmake_profile_stat.py
executable file
·267 lines (222 loc) · 9.75 KB
/
cmake_profile_stat.py
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
#!/usr/bin/env python
from __future__ import print_function
import sys
import re
import os
import fileinput
import shelve
import argparse
import textwrap
def process_arguments():
parser = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent('''\
Each trace line in a report has 6 distinct parts:
1) nesting level in square brackets;
2) path to original cmake script (maybe with dots in the middle and
dots at the end if -w option was used) followed by colon;
3) line number of a traced line in the original cmake script
followed by colon;
4) a line of code as it was traced by cmake;
5) cumulative execution time of a traced line in seconds in brackets;
6) percentage of cumulative execution time of a traced line to
whole execution time in brackets.
In short format is as follows:
[nesting]file_path:line_number: cmake_code (seconds) (percentage)
During script execution it can output to stderr lines that it
does not recognize as cmake trace lines. Normally such lines
originate from cmake script's messages and this script outputs
those lines starting with "Ignored: " string.
'''))
parser.add_argument('trace', nargs='?', default=None,
help='cmake trace log or stdin')
parser.add_argument('-f', '--shelve-file', default='cmake.traces',
help='file for shelf container {default: %(default)s}')
parser.add_argument('-t', '--threshold',
default=0, type=float,
help='do not report traces with relative time lower than threshold, '
'for example 0.01 corresponds to 1%% of the whole execution time '
'{default: %(default)s}')
parser.add_argument('-w', '--trace-info-width', default=None, type=int,
help='fixed width in characters of a variable part of cmake trace '
'(file name, line number, nesting) in generated report '
'{default: %(default)s}')
parser.add_argument('-s', '--sort-traces',
default=False, action='store_true',
help='sort subcalls in a trace according to their timings '
'{default: %(default)s}')
parser.add_argument('-r', '--report-only',
default=False, action='store_true',
help='do not collect stats, make a report from shelved stats instead '
'{default: %(default)s}')
parser.add_argument('-1', '--one',
default=False, action='store_true',
help='report only the most expensive stack trace '
'{default: %(default)s}')
args = parser.parse_args()
return args
class CmakeTraceInfo(object):
def __init__(self, cmakeFile, cmakeLine, cmakeCodeLine):
self.cmakeFile = cmakeFile
self.cmakeLine = cmakeLine
self.cmakeCodeLine = cmakeCodeLine
def to_string_adjusted(self, width, nesting):
fileWidth = width - (len(self.cmakeLine) + len(nesting))
cmakeFileLen = len(self.cmakeFile)
adjustedFile = self.cmakeFile
if fileWidth < cmakeFileLen:
assert fileWidth >= 5
halfFileWidth = fileWidth / 2
adjustedFile = ('%s...%s' %
(adjustedFile[:halfFileWidth - 1],
adjustedFile[cmakeFileLen + 2 - halfFileWidth:]))
adjustedFile = adjustedFile.ljust(fileWidth, '.')
return ('[%s]%s:%s: %s' %
(nesting, adjustedFile, self.cmakeLine, self.cmakeCodeLine))
def to_string_plain(self, width, nesting):
assert width is None
return ('[%s]%s:%s: %s' %
(nesting, self.cmakeFile, self.cmakeLine, self.cmakeCodeLine))
def key(self):
# File name is reversed because it makes string comparison faster.
return ('%s%s' % (self.cmakeLine, self.cmakeFile[::-1]))
class CmakeTrace(object):
def __init__(self, duration, ti, parent):
self.duration = duration
self.traceInfo = ti
self.parentTrace = parent
self.traces = []
try:
p = parent
while True:
p.duration = p.duration + duration
p = p.parentTrace
except AttributeError:
assert p is None
def update_trace(newTimeval, prevTimeval, prevTi, nestingGrew,
appendTrace, traces, startTimeRef):
try:
duration = newTimeval - prevTimeval
assert duration >= 0
except TypeError:
# This exception can happen only for the very first trace line.
assert len(startTimeRef) == 0
startTimeRef.append(newTimeval)
return appendTrace
if nestingGrew:
# Switch to the last trace among children of the current trace,
# making the nesting deeper.
appendTrace = appendTrace.traces[-1]
t = CmakeTrace(duration, prevTi, appendTrace)
try:
appendTrace.traces.append(t)
except AttributeError:
assert appendTrace is None
appendTrace = t
return appendTrace
def parent_trace(trace, levels):
for x in xrange(0, levels):
trace = trace.parentTrace
return trace
def add_new_trace(prevNesting, appendTrace, traces, traceKeys):
# -2 is because lowest nesting is 1 and even if we work at nesting 2
# we still hold the trace at level 1 while filling in the level 2.
p = parent_trace(appendTrace, prevNesting - 2)
traces[p.traceInfo.key()] = p
traceKeys[p.traceInfo.key()] = p.duration
assert len(traces) == len(traceKeys) + 2
def collect_stats(traces, traceKeys):
matcher = re.compile('^\(([^)]*)\) \(([^)]*)\) ([^(]*)\(([^)]*)\): (.*)$')
# Contains one element with start time.
startTimeRef = []
prevTi = None
prevTimeval = None
prevNesting = 0
nestingGrew = False
appendTrace = None
for line in fileinput.input():
m = matcher.match(line)
if m is None:
# This will log to stderr all lines that didn't match which will
# provide: 1) control over matching; 2) a filling of progress. :)
print('Ignored: %s' % line, end='', file=sys.stderr)
continue
timeval = float(m.group(1))
nesting = int(m.group(2))
cmakeFile = m.group(3)
cmakeLine = m.group(4)
cmakeCodeLine = m.group(5).rstrip() # Strip possible /r on Windows.
# Make sure that cmake trace doesn't miss any lines when nesting
# increases (if there are bugs in cmake logging). Missing lines when
# nesting decreases is not important at all.
assert ((nesting > prevNesting and nesting == prevNesting + 1) or
nesting <= prevNesting)
appendTrace = update_trace(timeval, prevTimeval, prevTi, nestingGrew,
appendTrace, traces, startTimeRef)
if nesting <= prevNesting:
# Time to finish building current trace and start a new one.
if nesting == 1:
add_new_trace(prevNesting, appendTrace, traces, traceKeys)
# Start a new trace.
appendTrace = None
else:
appendTrace = parent_trace(appendTrace, prevNesting - nesting)
prevTi = CmakeTraceInfo(cmakeFile, cmakeLine, cmakeCodeLine)
nestingGrew = (nesting > 2 and nesting > prevNesting)
prevTimeval = timeval
prevNesting = nesting
# Add the last trace line with the smallest duration for completeness.
# This line shouldn't be important, though.
appendTrace = update_trace(prevTimeval + 10E-7, prevTimeval, prevTi,
nestingGrew, appendTrace, traces, None)
add_new_trace(prevNesting, appendTrace, traces, traceKeys)
return prevTimeval - startTimeRef[0]
def print_trace(args, tiToString, trace, duration, indent):
if trace.duration / duration < args.threshold:
return False
print("%s%s (%fsec)(%f%%)" %
(' ' * indent,
tiToString(trace.traceInfo, args.trace_info_width, str(indent + 1)),
trace.duration, trace.duration / duration * 100))
for t in sorted(trace.traces,
key=lambda x: x.duration if args.sort_traces
else x.traceInfo.cmakeLine,
reverse=True):
print_trace(args, tiToString, t, duration, indent + 1)
return True
TRACE_KEYS_KEY = 'trace.keys'
WHOLE_DURATION_KEY = 'whole.duration'
if __name__ == '__main__':
args = process_arguments()
# Adjust sys.argv for fileinput().
sys.argv = sys.argv[:1]
if args.trace is not None:
sys.argv.append(args.trace)
if os.path.exists(args.shelve_file) and not args.report_only:
os.remove(args.shelve_file)
allTraces = shelve.open(args.shelve_file)
# TODO: allTraceKeys should be a list but for now it's not possible due
# to bugs in cmake logging.
allTraceKeys = allTraces.get(TRACE_KEYS_KEY, {})
wholeDuration = allTraces.get(WHOLE_DURATION_KEY, 0)
allTraces[TRACE_KEYS_KEY] = allTraceKeys
allTraces[WHOLE_DURATION_KEY] = wholeDuration
try:
if not args.report_only:
wholeDuration = collect_stats(allTraces, allTraceKeys)
allTraces[TRACE_KEYS_KEY] = allTraceKeys
allTraces[WHOLE_DURATION_KEY] = wholeDuration
except:
os.remove(args.shelve_file)
raise
traceInfoToString = CmakeTraceInfo.to_string_plain
if args.trace_info_width is not None:
traceInfoToString = CmakeTraceInfo.to_string_adjusted
for (k, d) in sorted(allTraceKeys.iteritems(),
key=lambda x: x[1], reverse=True):
if (not print_trace(args, traceInfoToString,
allTraces[k], wholeDuration, 0) or
args.one):
break
print('')
allTraces.close()