1 #===- perf-helper.py - Clang Python Bindings -----------------*- python -*--===#
3 # Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 # See https://llvm.org/LICENSE.txt for license information.
5 # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
7 #===------------------------------------------------------------------------===#
9 from __future__
import absolute_import
, division
, print_function
20 test_env
= { 'PATH' : os
.environ
['PATH'] }
22 def findFilesWithExtension(path
, extension
):
24 for root
, dirs
, files
in os
.walk(path
):
25 for filename
in files
:
26 if filename
.endswith(extension
):
27 filenames
.append(os
.path
.join(root
, filename
))
32 print('Usage: %s clean <path> <extension>\n' % __file__
+
33 '\tRemoves all files with extension from <path>.')
35 for filename
in findFilesWithExtension(args
[0], args
[1]):
41 print('Usage: %s clean <llvm-profdata> <output> <path>\n' % __file__
+
42 '\tMerges all profraw files from path into output.')
44 cmd
= [args
[0], 'merge', '-o', args
[1]]
45 cmd
.extend(findFilesWithExtension(args
[2], "profraw"))
46 subprocess
.check_call(cmd
)
50 parser
= argparse
.ArgumentParser(prog
='perf-helper dtrace',
51 description
='dtrace wrapper for order file generation')
52 parser
.add_argument('--buffer-size', metavar
='size', type=int, required
=False,
53 default
=1, help='dtrace buffer size in MB (default 1)')
54 parser
.add_argument('--use-oneshot', required
=False, action
='store_true',
55 help='Use dtrace\'s oneshot probes')
56 parser
.add_argument('--use-ustack', required
=False, action
='store_true',
57 help='Use dtrace\'s ustack to print function names')
58 parser
.add_argument('--cc1', required
=False, action
='store_true',
59 help='Execute cc1 directly (don\'t profile the driver)')
60 parser
.add_argument('cmd', nargs
='*', help='')
62 # Use python's arg parser to handle all leading option arguments, but pass
63 # everything else through to dtrace
64 first_cmd
= next(arg
for arg
in args
if not arg
.startswith("--"))
65 last_arg_idx
= args
.index(first_cmd
)
67 opts
= parser
.parse_args(args
[:last_arg_idx
])
68 cmd
= args
[last_arg_idx
:]
71 cmd
= get_cc1_command_for_args(cmd
, test_env
)
74 target
= "oneshot$target:::entry"
76 target
= "pid$target:::entry"
77 predicate
= '%s/probemod=="%s"/' % (target
, os
.path
.basename(cmd
[0]))
78 log_timestamp
= 'printf("dtrace-TS: %d\\n", timestamp)'
82 action
= 'printf("dtrace-Symbol: %s\\n", probefunc);'
83 dtrace_script
= "%s { %s; %s }" % (predicate
, log_timestamp
, action
)
86 if not os
.geteuid() == 0:
88 'Script must be run as root, or you must add the following to your sudoers:'
89 + '%%admin ALL=(ALL) NOPASSWD: /usr/sbin/dtrace')
90 dtrace_args
.append("sudo")
93 'dtrace', '-xevaltime=exec',
94 '-xbufsize=%dm' % (opts
.buffer_size
),
95 '-q', '-n', dtrace_script
,
98 if sys
.platform
== "darwin":
99 dtrace_args
.append('-xmangled')
101 start_time
= time
.time()
103 with
open("%d.dtrace" % os
.getpid(), "w") as f
:
104 f
.write("### Command: %s" % dtrace_args
)
105 subprocess
.check_call(dtrace_args
, stdout
=f
, stderr
=subprocess
.PIPE
)
107 elapsed
= time
.time() - start_time
108 print("... data collection took %.4fs" % elapsed
)
112 def get_cc1_command_for_args(cmd
, env
):
113 # Find the cc1 command used by the compiler. To do this we execute the
114 # compiler with '-###' to figure out what it wants to do.
116 cc_output
= subprocess
.check_output(cmd
, stderr
=subprocess
.STDOUT
, env
=env
, universal_newlines
=True).strip()
118 for ln
in cc_output
.split('\n'):
119 # Filter out known garbage.
120 if (ln
== 'Using built-in specs.' or
121 ln
.startswith('Configured with:') or
122 ln
.startswith('Target:') or
123 ln
.startswith('Thread model:') or
124 ln
.startswith('InstalledDir:') or
125 ln
.startswith('LLVM Profile Note') or
126 ln
.startswith(' (in-process)') or
129 cc_commands
.append(ln
)
131 if len(cc_commands
) != 1:
132 print('Fatal error: unable to determine cc1 command: %r' % cc_output
)
135 cc1_cmd
= shlex
.split(cc_commands
[0])
137 print('Fatal error: unable to determine cc1 command: %r' % cc_output
)
143 parser
= argparse
.ArgumentParser(prog
='perf-helper cc1',
144 description
='cc1 wrapper for order file generation')
145 parser
.add_argument('cmd', nargs
='*', help='')
147 # Use python's arg parser to handle all leading option arguments, but pass
148 # everything else through to dtrace
149 first_cmd
= next(arg
for arg
in args
if not arg
.startswith("--"))
150 last_arg_idx
= args
.index(first_cmd
)
152 opts
= parser
.parse_args(args
[:last_arg_idx
])
153 cmd
= args
[last_arg_idx
:]
155 # clear the profile file env, so that we don't generate profdata
156 # when capturing the cc1 command
158 cc1_env
["LLVM_PROFILE_FILE"] = os
.devnull
159 cc1_cmd
= get_cc1_command_for_args(cmd
, cc1_env
)
161 subprocess
.check_call(cc1_cmd
)
164 def parse_dtrace_symbol_file(path
, all_symbols
, all_symbols_set
,
165 missing_symbols
, opts
):
166 def fix_mangling(symbol
):
167 if sys
.platform
== "darwin":
168 if symbol
[0] != '_' and symbol
!= 'start':
169 symbol
= '_' + symbol
172 def get_symbols_with_prefix(symbol
):
173 start_index
= bisect
.bisect_left(all_symbols
, symbol
)
174 for s
in all_symbols
[start_index
:]:
175 if not s
.startswith(symbol
):
179 # Extract the list of symbols from the given file, which is assumed to be
180 # the output of a dtrace run logging either probefunc or ustack(1) and
181 # nothing else. The dtrace -xdemangle option needs to be used.
183 # This is particular to OS X at the moment, because of the '_' handling.
184 with
open(path
) as f
:
185 current_timestamp
= None
187 # Drop leading and trailing whitespace.
189 if not ln
.startswith("dtrace-"):
192 # If this is a timestamp specifier, extract it.
193 if ln
.startswith("dtrace-TS: "):
194 _
,data
= ln
.split(': ', 1)
195 if not data
.isdigit():
196 print("warning: unrecognized timestamp line %r, ignoring" % ln
,
199 current_timestamp
= int(data
)
201 elif ln
.startswith("dtrace-Symbol: "):
203 _
,ln
= ln
.split(': ', 1)
207 # If there is a '`' in the line, assume it is a ustack(1) entry in
208 # the form of <modulename>`<modulefunc>, where <modulefunc> is never
209 # truncated (but does need the mangling patched).
211 yield (current_timestamp
, fix_mangling(ln
.split('`',1)[1]))
214 # Otherwise, assume this is a probefunc printout. DTrace on OS X
215 # seems to have a bug where it prints the mangled version of symbols
216 # which aren't C++ mangled. We just add a '_' to anything but start
217 # which doesn't already have a '_'.
218 symbol
= fix_mangling(ln
)
220 # If we don't know all the symbols, or the symbol is one of them,
222 if not all_symbols_set
or symbol
in all_symbols_set
:
223 yield (current_timestamp
, symbol
)
226 # Otherwise, we have a symbol name which isn't present in the
227 # binary. We assume it is truncated, and try to extend it.
229 # Get all the symbols with this prefix.
230 possible_symbols
= list(get_symbols_with_prefix(symbol
))
231 if not possible_symbols
:
234 # If we found too many possible symbols, ignore this as a prefix.
235 if len(possible_symbols
) > 100:
236 print( "warning: ignoring symbol %r " % symbol
+
237 "(no match and too many possible suffixes)", file=sys
.stderr
)
240 # Report that we resolved a missing symbol.
241 if opts
.show_missing_symbols
and symbol
not in missing_symbols
:
242 print("warning: resolved missing symbol %r" % symbol
, file=sys
.stderr
)
243 missing_symbols
.add(symbol
)
245 # Otherwise, treat all the possible matches as having occurred. This
246 # is an over-approximation, but it should be ok in practice.
247 for s
in possible_symbols
:
248 yield (current_timestamp
, s
)
257 def form_by_call_order(symbol_lists
):
258 # Simply strategy, just return symbols in order of occurrence, even across
260 return uniq(s
for symbols
in symbol_lists
for s
in symbols
)
262 def form_by_call_order_fair(symbol_lists
):
263 # More complicated strategy that tries to respect the call order across all
264 # of the test cases, instead of giving a huge preference to the first test
267 # First, uniq all the lists.
268 uniq_lists
= [list(uniq(symbols
)) for symbols
in symbol_lists
]
270 # Compute the successors for each list.
272 for symbols
in uniq_lists
:
273 for a
,b
in zip(symbols
[:-1], symbols
[1:]):
274 succs
[a
] = items
= succs
.get(a
, [])
278 # Emit all the symbols, but make sure to always emit all successors from any
279 # call list whenever we see a symbol.
281 # There isn't much science here, but this sometimes works better than the
282 # more naive strategy. Then again, sometimes it doesn't so more research is
285 for symbols
in symbol_lists
287 for s
in ([node
] + succs
.get(node
,[])))
289 def form_by_frequency(symbol_lists
):
290 # Form the order file by just putting the most commonly occurring symbols
291 # first. This assumes the data files didn't use the oneshot dtrace method.
294 for symbols
in symbol_lists
:
296 counts
[a
] = counts
.get(a
,0) + 1
298 by_count
= list(counts
.items())
299 by_count
.sort(key
= lambda __n
: -__n
[1])
300 return [s
for s
,n
in by_count
]
302 def form_by_random(symbol_lists
):
303 # Randomize the symbols.
304 merged_symbols
= uniq(s
for symbols
in symbol_lists
306 random
.shuffle(merged_symbols
)
307 return merged_symbols
309 def form_by_alphabetical(symbol_lists
):
310 # Alphabetize the symbols.
311 merged_symbols
= list(set(s
for symbols
in symbol_lists
for s
in symbols
))
312 merged_symbols
.sort()
313 return merged_symbols
315 methods
= dict((name
[len("form_by_"):],value
)
316 for name
,value
in locals().items() if name
.startswith("form_by_"))
318 def genOrderFile(args
):
319 parser
= argparse
.ArgumentParser(
320 "%prog [options] <dtrace data file directories>]")
321 parser
.add_argument('input', nargs
='+', help='')
322 parser
.add_argument("--binary", metavar
="PATH", type=str, dest
="binary_path",
323 help="Path to the binary being ordered (for getting all symbols)",
325 parser
.add_argument("--output", dest
="output_path",
326 help="path to output order file to write", default
=None, required
=True,
328 parser
.add_argument("--show-missing-symbols", dest
="show_missing_symbols",
329 help="show symbols which are 'fixed up' to a valid name (requires --binary)",
330 action
="store_true", default
=None)
331 parser
.add_argument("--output-unordered-symbols",
332 dest
="output_unordered_symbols_path",
333 help="write a list of the unordered symbols to PATH (requires --binary)",
334 default
=None, metavar
="PATH")
335 parser
.add_argument("--method", dest
="method",
336 help="order file generation method to use", choices
=list(methods
.keys()),
337 default
='call_order')
338 opts
= parser
.parse_args(args
)
340 # If the user gave us a binary, get all the symbols in the binary by
341 # snarfing 'nm' output.
342 if opts
.binary_path
is not None:
343 output
= subprocess
.check_output(['nm', '-P', opts
.binary_path
], universal_newlines
=True)
344 lines
= output
.split("\n")
345 all_symbols
= [ln
.split(' ',1)[0]
348 print("found %d symbols in binary" % len(all_symbols
))
352 all_symbols_set
= set(all_symbols
)
354 # Compute the list of input files.
356 for dirname
in opts
.input:
357 input_files
.extend(findFilesWithExtension(dirname
, "dtrace"))
359 # Load all of the input files.
360 print("loading from %d data files" % len(input_files
))
361 missing_symbols
= set()
362 timestamped_symbol_lists
= [
363 list(parse_dtrace_symbol_file(path
, all_symbols
, all_symbols_set
,
364 missing_symbols
, opts
))
365 for path
in input_files
]
367 # Reorder each symbol list.
369 for timestamped_symbols_list
in timestamped_symbol_lists
:
370 timestamped_symbols_list
.sort()
371 symbol_lists
.append([symbol
for _
,symbol
in timestamped_symbols_list
])
373 # Execute the desire order file generation method.
374 method
= methods
.get(opts
.method
)
375 result
= list(method(symbol_lists
))
377 # Report to the user on what percentage of symbols are present in the order
379 num_ordered_symbols
= len(result
)
381 print("note: order file contains %d/%d symbols (%.2f%%)" % (
382 num_ordered_symbols
, len(all_symbols
),
383 100.*num_ordered_symbols
/len(all_symbols
)), file=sys
.stderr
)
385 if opts
.output_unordered_symbols_path
:
386 ordered_symbols_set
= set(result
)
387 with
open(opts
.output_unordered_symbols_path
, 'w') as f
:
388 f
.write("\n".join(s
for s
in all_symbols
if s
not in ordered_symbols_set
))
390 # Write the order file.
391 with
open(opts
.output_path
, 'w') as f
:
392 f
.write("\n".join(result
))
397 commands
= {'clean' : clean
,
401 'gen-order-file' : genOrderFile
}
404 f
= commands
[sys
.argv
[1]]
405 sys
.exit(f(sys
.argv
[2:]))
407 if __name__
== '__main__':