Run DCE after a LoopFlatten test to reduce spurious output [nfc]
[llvm-project.git] / clang / utils / perf-training / perf-helper.py
blob99d6a3333b6ef088a1afbfe67dc5df3e1849dc7e
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
11 import sys
12 import os
13 import subprocess
14 import argparse
15 import time
16 import bisect
17 import shlex
18 import tempfile
20 test_env = {"PATH": os.environ["PATH"]}
23 def findFilesWithExtension(path, extension):
24 filenames = []
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))
29 return filenames
32 def clean(args):
33 if len(args) != 2:
34 print(
35 "Usage: %s clean <path> <extension>\n" % __file__
36 + "\tRemoves all files with extension from <path>."
38 return 1
39 for filename in findFilesWithExtension(args[0], args[1]):
40 os.remove(filename)
41 return 0
44 def merge(args):
45 if len(args) != 3:
46 print(
47 "Usage: %s merge <llvm-profdata> <output> <path>\n" % __file__
48 + "\tMerges all profraw files from path into output."
50 return 1
51 cmd = [args[0], "merge", "-o", args[1]]
52 cmd.extend(findFilesWithExtension(args[2], "profraw"))
53 subprocess.check_call(cmd)
54 return 0
57 def merge_fdata(args):
58 if len(args) != 3:
59 print(
60 "Usage: %s merge-fdata <merge-fdata> <output> <path>\n" % __file__
61 + "\tMerges all fdata files from path into output."
63 return 1
64 cmd = [args[0], "-o", args[1]]
65 cmd.extend(findFilesWithExtension(args[2], "fdata"))
66 subprocess.check_call(cmd)
67 return 0
70 def dtrace(args):
71 parser = argparse.ArgumentParser(
72 prog="perf-helper dtrace",
73 description="dtrace wrapper for order file generation",
75 parser.add_argument(
76 "--buffer-size",
77 metavar="size",
78 type=int,
79 required=False,
80 default=1,
81 help="dtrace buffer size in MB (default 1)",
83 parser.add_argument(
84 "--use-oneshot",
85 required=False,
86 action="store_true",
87 help="Use dtrace's oneshot probes",
89 parser.add_argument(
90 "--use-ustack",
91 required=False,
92 action="store_true",
93 help="Use dtrace's ustack to print function names",
95 parser.add_argument(
96 "--cc1",
97 required=False,
98 action="store_true",
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:]
111 if opts.cc1:
112 cmd = get_cc1_command_for_args(cmd, test_env)
114 if opts.use_oneshot:
115 target = "oneshot$target:::entry"
116 else:
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)'
120 if opts.use_ustack:
121 action = "ustack(1);"
122 else:
123 action = 'printf("dtrace-Symbol: %s\\n", probefunc);'
124 dtrace_script = "%s { %s; %s }" % (predicate, log_timestamp, action)
126 dtrace_args = []
127 if not os.geteuid() == 0:
128 print(
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")
134 dtrace_args.extend(
136 "dtrace",
137 "-xevaltime=exec",
138 "-xbufsize=%dm" % (opts.buffer_size),
139 "-q",
140 "-n",
141 dtrace_script,
142 "-c",
143 " ".join(cmd),
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)
159 return 0
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.
165 cmd = cmd + ["-###"]
166 cc_output = subprocess.check_output(
167 cmd, stderr=subprocess.STDOUT, env=env, universal_newlines=True
168 ).strip()
169 cc_commands = []
170 for ln in cc_output.split("\n"):
171 # Filter out known garbage.
172 if (
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)")
180 or " version " in ln
182 continue
183 cc_commands.append(ln)
185 if len(cc_commands) != 1:
186 print("Fatal error: unable to determine cc1 command: %r" % cc_output)
187 exit(1)
189 cc1_cmd = shlex.split(cc_commands[0])
190 if not cc1_cmd:
191 print("Fatal error: unable to determine cc1 command: %r" % cc_output)
192 exit(1)
194 return cc1_cmd
197 def cc1(args):
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
213 cc1_env = test_env
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)
218 return 0
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
226 return 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):
232 break
233 yield s
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
242 for ln in f:
243 # Drop leading and trailing whitespace.
244 ln = ln.strip()
245 if not ln.startswith("dtrace-"):
246 continue
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():
252 print(
253 "warning: unrecognized timestamp line %r, ignoring" % ln,
254 file=sys.stderr,
256 continue
257 current_timestamp = int(data)
258 continue
259 elif ln.startswith("dtrace-Symbol: "):
261 _, ln = ln.split(": ", 1)
262 if not ln:
263 continue
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).
268 if "`" in ln:
269 yield (current_timestamp, fix_mangling(ln.split("`", 1)[1]))
270 continue
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,
279 # just return it.
280 if not all_symbols_set or symbol in all_symbols_set:
281 yield (current_timestamp, symbol)
282 continue
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:
290 continue
292 # If we found too many possible symbols, ignore this as a prefix.
293 if len(possible_symbols) > 100:
294 print(
295 "warning: ignoring symbol %r " % symbol
296 + "(no match and too many possible suffixes)",
297 file=sys.stderr,
299 continue
301 # Report that we resolved a missing symbol.
302 if opts.show_missing_symbols and symbol not in missing_symbols:
303 print(
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)
314 def uniq(list):
315 seen = set()
316 for item in list:
317 if item not in seen:
318 yield item
319 seen.add(item)
322 def form_by_call_order(symbol_lists):
323 # Simply strategy, just return symbols in order of occurrence, even across
324 # multiple runs.
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
331 # case.
333 # First, uniq all the lists.
334 uniq_lists = [list(uniq(symbols)) for symbols in symbol_lists]
336 # Compute the successors for each list.
337 succs = {}
338 for symbols in uniq_lists:
339 for a, b in zip(symbols[:-1], symbols[1:]):
340 succs[a] = items = succs.get(a, [])
341 if b not in items:
342 items.append(b)
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
349 # probably needed.
350 return uniq(
352 for symbols in symbol_lists
353 for node in symbols
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.
362 counts = {}
363 for symbols in symbol_lists:
364 for a in symbols:
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
386 methods = dict(
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="")
396 parser.add_argument(
397 "--binary",
398 metavar="PATH",
399 type=str,
400 dest="binary_path",
401 help="Path to the binary being ordered (for getting all symbols)",
402 default=None,
404 parser.add_argument(
405 "--output",
406 dest="output_path",
407 help="path to output order file to write",
408 default=None,
409 required=True,
410 metavar="PATH",
412 parser.add_argument(
413 "--show-missing-symbols",
414 dest="show_missing_symbols",
415 help="show symbols which are 'fixed up' to a valid name (requires --binary)",
416 action="store_true",
417 default=None,
419 parser.add_argument(
420 "--output-unordered-symbols",
421 dest="output_unordered_symbols_path",
422 help="write a list of the unordered symbols to PATH (requires --binary)",
423 default=None,
424 metavar="PATH",
426 parser.add_argument(
427 "--method",
428 dest="method",
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))
444 all_symbols.sort()
445 else:
446 all_symbols = []
447 all_symbols_set = set(all_symbols)
449 # Compute the list of input files.
450 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 = [
458 list(
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.
467 symbol_lists = []
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
477 # file.
478 num_ordered_symbols = len(result)
479 if all_symbols:
480 print(
481 "note: order file contains %d/%d symbols (%.2f%%)"
483 num_ordered_symbols,
484 len(all_symbols),
485 100.0 * num_ordered_symbols / len(all_symbols),
487 file=sys.stderr,
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))
498 f.write("\n")
500 return 0
503 commands = {
504 "clean": clean,
505 "merge": merge,
506 "dtrace": dtrace,
507 "cc1": cc1,
508 "gen-order-file": genOrderFile,
509 "merge-fdata": merge_fdata,
513 def main():
514 f = commands[sys.argv[1]]
515 sys.exit(f(sys.argv[2:]))
518 if __name__ == "__main__":
519 main()