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"]}
23 def findFilesWithExtension(path
, extension
):
25 for root
, dirs
, files
in os
.walk(path
):
26 for filename
in files
:
27 if filename
.endswith(f
".{extension}"):
28 filenames
.append(os
.path
.join(root
, filename
))
35 "Usage: %s clean <path> <extension>\n" % __file__
36 + "\tRemoves all files with extension from <path>."
39 for filename
in findFilesWithExtension(args
[0], args
[1]):
47 "Usage: %s merge <llvm-profdata> <output> <path>\n" % __file__
48 + "\tMerges all profraw files from path into output."
51 cmd
= [args
[0], "merge", "-o", args
[1]]
52 cmd
.extend(findFilesWithExtension(args
[2], "profraw"))
53 subprocess
.check_call(cmd
)
57 def merge_fdata(args
):
60 "Usage: %s merge-fdata <merge-fdata> <output> <path>\n" % __file__
61 + "\tMerges all fdata files from path into output."
64 cmd
= [args
[0], "-o", args
[1]]
65 cmd
.extend(findFilesWithExtension(args
[2], "fdata"))
66 subprocess
.check_call(cmd
)
71 parser
= argparse
.ArgumentParser(
72 prog
="perf-helper dtrace",
73 description
="dtrace wrapper for order file generation",
81 help="dtrace buffer size in MB (default 1)",
87 help="Use dtrace's oneshot probes",
93 help="Use dtrace's ustack to print function names",
99 help="Execute cc1 directly (don't profile the driver)",
101 parser
.add_argument("cmd", nargs
="*", help="")
103 # Use python's arg parser to handle all leading option arguments, but pass
104 # everything else through to dtrace
105 first_cmd
= next(arg
for arg
in args
if not arg
.startswith("--"))
106 last_arg_idx
= args
.index(first_cmd
)
108 opts
= parser
.parse_args(args
[:last_arg_idx
])
109 cmd
= args
[last_arg_idx
:]
112 cmd
= get_cc1_command_for_args(cmd
, test_env
)
115 target
= "oneshot$target:::entry"
117 target
= "pid$target:::entry"
118 predicate
= '%s/probemod=="%s"/' % (target
, os
.path
.basename(cmd
[0]))
119 log_timestamp
= 'printf("dtrace-TS: %d\\n", timestamp)'
121 action
= "ustack(1);"
123 action
= 'printf("dtrace-Symbol: %s\\n", probefunc);'
124 dtrace_script
= "%s { %s; %s }" % (predicate
, log_timestamp
, action
)
127 if not os
.geteuid() == 0:
129 "Script must be run as root, or you must add the following to your sudoers:"
130 + "%%admin ALL=(ALL) NOPASSWD: /usr/sbin/dtrace"
132 dtrace_args
.append("sudo")
138 "-xbufsize=%dm" % (opts
.buffer_size
),
147 if sys
.platform
== "darwin":
148 dtrace_args
.append("-xmangled")
150 start_time
= time
.time()
152 with
open("%d.dtrace" % os
.getpid(), "w") as f
:
153 f
.write("### Command: %s" % dtrace_args
)
154 subprocess
.check_call(dtrace_args
, stdout
=f
, stderr
=subprocess
.PIPE
)
156 elapsed
= time
.time() - start_time
157 print("... data collection took %.4fs" % elapsed
)
162 def get_cc1_command_for_args(cmd
, env
):
163 # Find the cc1 command used by the compiler. To do this we execute the
164 # compiler with '-###' to figure out what it wants to do.
166 cc_output
= subprocess
.check_output(
167 cmd
, stderr
=subprocess
.STDOUT
, env
=env
, universal_newlines
=True
170 for ln
in cc_output
.split("\n"):
171 # Filter out known garbage.
173 ln
== "Using built-in specs."
174 or ln
.startswith("Configured with:")
175 or ln
.startswith("Target:")
176 or ln
.startswith("Thread model:")
177 or ln
.startswith("InstalledDir:")
178 or ln
.startswith("LLVM Profile Note")
179 or ln
.startswith(" (in-process)")
183 cc_commands
.append(ln
)
185 if len(cc_commands
) != 1:
186 print("Fatal error: unable to determine cc1 command: %r" % cc_output
)
189 cc1_cmd
= shlex
.split(cc_commands
[0])
191 print("Fatal error: unable to determine cc1 command: %r" % cc_output
)
198 parser
= argparse
.ArgumentParser(
199 prog
="perf-helper cc1", description
="cc1 wrapper for order file generation"
201 parser
.add_argument("cmd", nargs
="*", help="")
203 # Use python's arg parser to handle all leading option arguments, but pass
204 # everything else through to dtrace
205 first_cmd
= next(arg
for arg
in args
if not arg
.startswith("--"))
206 last_arg_idx
= args
.index(first_cmd
)
208 opts
= parser
.parse_args(args
[:last_arg_idx
])
209 cmd
= args
[last_arg_idx
:]
211 # clear the profile file env, so that we don't generate profdata
212 # when capturing the cc1 command
214 cc1_env
["LLVM_PROFILE_FILE"] = os
.devnull
215 cc1_cmd
= get_cc1_command_for_args(cmd
, cc1_env
)
217 subprocess
.check_call(cc1_cmd
)
221 def parse_dtrace_symbol_file(path
, all_symbols
, all_symbols_set
, missing_symbols
, opts
):
222 def fix_mangling(symbol
):
223 if sys
.platform
== "darwin":
224 if symbol
[0] != "_" and symbol
!= "start":
225 symbol
= "_" + symbol
228 def get_symbols_with_prefix(symbol
):
229 start_index
= bisect
.bisect_left(all_symbols
, symbol
)
230 for s
in all_symbols
[start_index
:]:
231 if not s
.startswith(symbol
):
235 # Extract the list of symbols from the given file, which is assumed to be
236 # the output of a dtrace run logging either probefunc or ustack(1) and
237 # nothing else. The dtrace -xdemangle option needs to be used.
239 # This is particular to OS X at the moment, because of the '_' handling.
240 with
open(path
) as f
:
241 current_timestamp
= None
243 # Drop leading and trailing whitespace.
245 if not ln
.startswith("dtrace-"):
248 # If this is a timestamp specifier, extract it.
249 if ln
.startswith("dtrace-TS: "):
250 _
, data
= ln
.split(": ", 1)
251 if not data
.isdigit():
253 "warning: unrecognized timestamp line %r, ignoring" % ln
,
257 current_timestamp
= int(data
)
259 elif ln
.startswith("dtrace-Symbol: "):
261 _
, ln
= ln
.split(": ", 1)
265 # If there is a '`' in the line, assume it is a ustack(1) entry in
266 # the form of <modulename>`<modulefunc>, where <modulefunc> is never
267 # truncated (but does need the mangling patched).
269 yield (current_timestamp
, fix_mangling(ln
.split("`", 1)[1]))
272 # Otherwise, assume this is a probefunc printout. DTrace on OS X
273 # seems to have a bug where it prints the mangled version of symbols
274 # which aren't C++ mangled. We just add a '_' to anything but start
275 # which doesn't already have a '_'.
276 symbol
= fix_mangling(ln
)
278 # If we don't know all the symbols, or the symbol is one of them,
280 if not all_symbols_set
or symbol
in all_symbols_set
:
281 yield (current_timestamp
, symbol
)
284 # Otherwise, we have a symbol name which isn't present in the
285 # binary. We assume it is truncated, and try to extend it.
287 # Get all the symbols with this prefix.
288 possible_symbols
= list(get_symbols_with_prefix(symbol
))
289 if not possible_symbols
:
292 # If we found too many possible symbols, ignore this as a prefix.
293 if len(possible_symbols
) > 100:
295 "warning: ignoring symbol %r " % symbol
296 + "(no match and too many possible suffixes)",
301 # Report that we resolved a missing symbol.
302 if opts
.show_missing_symbols
and symbol
not in missing_symbols
:
304 "warning: resolved missing symbol %r" % symbol
, file=sys
.stderr
306 missing_symbols
.add(symbol
)
308 # Otherwise, treat all the possible matches as having occurred. This
309 # is an over-approximation, but it should be ok in practice.
310 for s
in possible_symbols
:
311 yield (current_timestamp
, s
)
322 def form_by_call_order(symbol_lists
):
323 # Simply strategy, just return symbols in order of occurrence, even across
325 return uniq(s
for symbols
in symbol_lists
for s
in symbols
)
328 def form_by_call_order_fair(symbol_lists
):
329 # More complicated strategy that tries to respect the call order across all
330 # of the test cases, instead of giving a huge preference to the first test
333 # First, uniq all the lists.
334 uniq_lists
= [list(uniq(symbols
)) for symbols
in symbol_lists
]
336 # Compute the successors for each list.
338 for symbols
in uniq_lists
:
339 for a
, b
in zip(symbols
[:-1], symbols
[1:]):
340 succs
[a
] = items
= succs
.get(a
, [])
344 # Emit all the symbols, but make sure to always emit all successors from any
345 # call list whenever we see a symbol.
347 # There isn't much science here, but this sometimes works better than the
348 # more naive strategy. Then again, sometimes it doesn't so more research is
352 for symbols
in symbol_lists
354 for s
in ([node
] + succs
.get(node
, []))
358 def form_by_frequency(symbol_lists
):
359 # Form the order file by just putting the most commonly occurring symbols
360 # first. This assumes the data files didn't use the oneshot dtrace method.
363 for symbols
in symbol_lists
:
365 counts
[a
] = counts
.get(a
, 0) + 1
367 by_count
= list(counts
.items())
368 by_count
.sort(key
=lambda __n
: -__n
[1])
369 return [s
for s
, n
in by_count
]
372 def form_by_random(symbol_lists
):
373 # Randomize the symbols.
374 merged_symbols
= uniq(s
for symbols
in symbol_lists
for s
in symbols
)
375 random
.shuffle(merged_symbols
)
376 return merged_symbols
379 def form_by_alphabetical(symbol_lists
):
380 # Alphabetize the symbols.
381 merged_symbols
= list(set(s
for symbols
in symbol_lists
for s
in symbols
))
382 merged_symbols
.sort()
383 return merged_symbols
387 (name
[len("form_by_") :], value
)
388 for name
, value
in locals().items()
389 if name
.startswith("form_by_")
393 def genOrderFile(args
):
394 parser
= argparse
.ArgumentParser("%prog [options] <dtrace data file directories>]")
395 parser
.add_argument("input", nargs
="+", help="")
401 help="Path to the binary being ordered (for getting all symbols)",
407 help="path to output order file to write",
413 "--show-missing-symbols",
414 dest
="show_missing_symbols",
415 help="show symbols which are 'fixed up' to a valid name (requires --binary)",
420 "--output-unordered-symbols",
421 dest
="output_unordered_symbols_path",
422 help="write a list of the unordered symbols to PATH (requires --binary)",
429 help="order file generation method to use",
430 choices
=list(methods
.keys()),
431 default
="call_order",
433 opts
= parser
.parse_args(args
)
435 # If the user gave us a binary, get all the symbols in the binary by
436 # snarfing 'nm' output.
437 if opts
.binary_path
is not None:
438 output
= subprocess
.check_output(
439 ["nm", "-P", opts
.binary_path
], universal_newlines
=True
441 lines
= output
.split("\n")
442 all_symbols
= [ln
.split(" ", 1)[0] for ln
in lines
if ln
.strip()]
443 print("found %d symbols in binary" % len(all_symbols
))
447 all_symbols_set
= set(all_symbols
)
449 # Compute the list of input files.
451 for dirname
in opts
.input:
452 input_files
.extend(findFilesWithExtension(dirname
, "dtrace"))
454 # Load all of the input files.
455 print("loading from %d data files" % len(input_files
))
456 missing_symbols
= set()
457 timestamped_symbol_lists
= [
459 parse_dtrace_symbol_file(
460 path
, all_symbols
, all_symbols_set
, missing_symbols
, opts
463 for path
in input_files
466 # Reorder each symbol list.
468 for timestamped_symbols_list
in timestamped_symbol_lists
:
469 timestamped_symbols_list
.sort()
470 symbol_lists
.append([symbol
for _
, symbol
in timestamped_symbols_list
])
472 # Execute the desire order file generation method.
473 method
= methods
.get(opts
.method
)
474 result
= list(method(symbol_lists
))
476 # Report to the user on what percentage of symbols are present in the order
478 num_ordered_symbols
= len(result
)
481 "note: order file contains %d/%d symbols (%.2f%%)"
485 100.0 * num_ordered_symbols
/ len(all_symbols
),
490 if opts
.output_unordered_symbols_path
:
491 ordered_symbols_set
= set(result
)
492 with
open(opts
.output_unordered_symbols_path
, "w") as f
:
493 f
.write("\n".join(s
for s
in all_symbols
if s
not in ordered_symbols_set
))
495 # Write the order file.
496 with
open(opts
.output_path
, "w") as f
:
497 f
.write("\n".join(result
))
508 "gen-order-file": genOrderFile
,
509 "merge-fdata": merge_fdata
,
514 f
= commands
[sys
.argv
[1]]
515 sys
.exit(f(sys
.argv
[2:]))
518 if __name__
== "__main__":