761 lines · python
1# ===- perf-helper.py - Clang Python Bindings -----------------*- python -*--===#2#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-exception6#7# ===------------------------------------------------------------------------===#8 9from __future__ import absolute_import, division, print_function10 11import sys12import os13import subprocess14import argparse15import time16import bisect17import shlex18import tempfile19import re20import shutil21 22test_env = {"PATH": os.environ["PATH"]}23 24 25def findFilesWithExtension(path, extension):26 filenames = []27 for root, dirs, files in os.walk(path):28 for filename in files:29 if filename.endswith(f".{extension}"):30 filenames.append(os.path.join(root, filename))31 return filenames32 33 34def clean(args):35 if len(args) < 2:36 print(37 "Usage: %s clean <paths> <extension>\n" % __file__38 + "\tRemoves all files with extension from <path>."39 )40 return 141 for path in args[0:-1]:42 for filename in findFilesWithExtension(path, args[-1]):43 os.remove(filename)44 return 045 46 47def merge(args):48 parser = argparse.ArgumentParser(49 prog="perf-helper merge",50 description="Merges all profraw files from path(s) into output",51 )52 parser.add_argument("profdata", help="Path to llvm-profdata tool")53 parser.add_argument("output", help="Output filename")54 parser.add_argument(55 "paths", nargs="+", help="Folder(s) containing input profraw files"56 )57 parser.add_argument("--sample", action="store_true", help="Sample profile")58 opts = parser.parse_args(args)59 60 cmd = [opts.profdata, "merge", "-o", opts.output]61 if opts.sample:62 cmd += ["--sample"]63 for path in opts.paths:64 cmd.extend(findFilesWithExtension(path, "profraw"))65 subprocess.check_call(cmd)66 return 067 68 69def merge_fdata(args):70 if len(args) != 3:71 print(72 "Usage: %s merge-fdata <merge-fdata> <output> <path>\n" % __file__73 + "\tMerges all fdata files from path into output."74 )75 return 176 cmd = [args[0], "-o", args[1]]77 cmd.extend(findFilesWithExtension(args[2], "fdata"))78 subprocess.check_call(cmd)79 return 080 81 82def perf(args):83 parser = argparse.ArgumentParser(84 prog="perf-helper perf",85 description="perf wrapper for BOLT/CSSPGO profile collection",86 )87 parser.add_argument(88 "--lbr", action="store_true", help="Use perf with branch stacks"89 )90 parser.add_argument("--csspgo", action="store_true", help="Enable CSSPGO flags")91 parser.add_argument("cmd", nargs=argparse.REMAINDER, help="")92 93 opts = parser.parse_args(args)94 cmd = opts.cmd[1:]95 96 event = "br_inst_retired.near_taken:uppp" if opts.csspgo else "cycles:u"97 perf_args = [98 "perf",99 "record",100 f"--event={event}",101 "--freq=max",102 "--output=%d.perf.data" % os.getpid(),103 ]104 if opts.lbr or opts.csspgo:105 perf_args += ["--branch-filter=any,u"]106 if opts.csspgo:107 perf_args += ["-g", "--call-graph=fp"]108 perf_args.extend(cmd)109 110 start_time = time.time()111 subprocess.check_call(perf_args)112 113 elapsed = time.time() - start_time114 print("... data collection took %.4fs" % elapsed)115 return 0116 117 118def perf2bolt(args):119 parser = argparse.ArgumentParser(120 prog="perf-helper perf2bolt",121 description="perf2bolt conversion wrapper for perf.data files",122 )123 parser.add_argument("bolt", help="Path to llvm-bolt")124 parser.add_argument("path", help="Path containing perf.data files")125 parser.add_argument("binary", help="Input binary")126 parser.add_argument("--lbr", action="store_true", help="Use LBR perf2bolt mode")127 opts = parser.parse_args(args)128 129 p2b_args = [130 opts.bolt,131 opts.binary,132 "--aggregate-only",133 "--profile-format=yaml",134 ]135 if not opts.lbr:136 p2b_args += ["-ba"]137 p2b_args += ["-p"]138 for filename in findFilesWithExtension(opts.path, "perf.data"):139 subprocess.check_call(p2b_args + [filename, "-o", filename + ".fdata"])140 return 0141 142 143def perf2prof(args):144 parser = argparse.ArgumentParser(145 prog="perf-helper perf2prof",146 description="perf to CSSPGO prof conversion wrapper",147 )148 parser.add_argument("profgen", help="Path to llvm-profgen binary")149 parser.add_argument("binary", help="Input binary")150 parser.add_argument("paths", nargs="+", help="Path containing perf.data files")151 opts = parser.parse_args(args)152 153 profgen_args = [opts.profgen, f"--binary={opts.binary}"]154 for path in opts.paths:155 for filename in findFilesWithExtension(path, "perf.data"):156 subprocess.run(157 [158 *profgen_args,159 f"--perfdata={filename}",160 f"--output={filename}.profraw",161 ],162 check=True,163 )164 return 0165 166 167def dtrace(args):168 parser = argparse.ArgumentParser(169 prog="perf-helper dtrace",170 description="dtrace wrapper for order file generation",171 )172 parser.add_argument(173 "--buffer-size",174 metavar="size",175 type=int,176 required=False,177 default=1,178 help="dtrace buffer size in MB (default 1)",179 )180 parser.add_argument(181 "--use-oneshot",182 required=False,183 action="store_true",184 help="Use dtrace's oneshot probes",185 )186 parser.add_argument(187 "--use-ustack",188 required=False,189 action="store_true",190 help="Use dtrace's ustack to print function names",191 )192 parser.add_argument(193 "--cc1",194 required=False,195 action="store_true",196 help="Execute cc1 directly (don't profile the driver)",197 )198 parser.add_argument("cmd", nargs="*", help="")199 200 # Use python's arg parser to handle all leading option arguments, but pass201 # everything else through to dtrace202 first_cmd = next(arg for arg in args if not arg.startswith("--"))203 last_arg_idx = args.index(first_cmd)204 205 opts = parser.parse_args(args[:last_arg_idx])206 cmd = args[last_arg_idx:]207 208 if opts.cc1:209 cmd = get_cc1_command_for_args(cmd, test_env)210 211 if opts.use_oneshot:212 target = "oneshot$target:::entry"213 else:214 target = "pid$target:::entry"215 predicate = '%s/probemod=="%s"/' % (target, os.path.basename(cmd[0]))216 log_timestamp = 'printf("dtrace-TS: %d\\n", timestamp)'217 if opts.use_ustack:218 action = "ustack(1);"219 else:220 action = 'printf("dtrace-Symbol: %s\\n", probefunc);'221 dtrace_script = "%s { %s; %s }" % (predicate, log_timestamp, action)222 223 dtrace_args = []224 if not os.geteuid() == 0:225 print(226 "Script must be run as root, or you must add the following to your sudoers:"227 + "%%admin ALL=(ALL) NOPASSWD: /usr/sbin/dtrace"228 )229 dtrace_args.append("sudo")230 231 dtrace_args.extend(232 (233 "dtrace",234 "-xevaltime=exec",235 "-xbufsize=%dm" % (opts.buffer_size),236 "-q",237 "-n",238 dtrace_script,239 "-c",240 " ".join(cmd),241 )242 )243 244 if sys.platform == "darwin":245 dtrace_args.append("-xmangled")246 247 start_time = time.time()248 249 with open("%d.dtrace" % os.getpid(), "w") as f:250 f.write("### Command: %s" % dtrace_args)251 subprocess.check_call(dtrace_args, stdout=f, stderr=subprocess.PIPE)252 253 elapsed = time.time() - start_time254 print("... data collection took %.4fs" % elapsed)255 256 return 0257 258 259def get_cc1_command_for_args(cmd, env):260 # Find the cc1 command used by the compiler. To do this we execute the261 # compiler with '-###' to figure out what it wants to do.262 cmd = cmd + ["-###"]263 cc_output = subprocess.check_output(264 cmd, stderr=subprocess.STDOUT, env=env, universal_newlines=True265 ).strip()266 cc_commands = []267 for ln in cc_output.split("\n"):268 # Filter out known garbage.269 if (270 ln == "Using built-in specs."271 or ln.startswith("Configured with:")272 or ln.startswith("Target:")273 or ln.startswith("Thread model:")274 or ln.startswith("InstalledDir:")275 or ln.startswith("LLVM Profile Note")276 or ln.startswith(" (in-process)")277 or ln.startswith("Configuration file:")278 or ln.startswith("Build config:")279 or " version " in ln280 ):281 continue282 cc_commands.append(ln)283 284 if len(cc_commands) != 1:285 print("Fatal error: unable to determine cc1 command: %r" % cc_output)286 exit(1)287 288 cc1_cmd = shlex.split(cc_commands[0])289 if not cc1_cmd:290 print("Fatal error: unable to determine cc1 command: %r" % cc_output)291 exit(1)292 293 return cc1_cmd294 295 296def cc1(args):297 parser = argparse.ArgumentParser(298 prog="perf-helper cc1", description="cc1 wrapper for order file generation"299 )300 parser.add_argument("cmd", nargs="*", help="")301 302 # Use python's arg parser to handle all leading option arguments, but pass303 # everything else through to dtrace304 first_cmd = next(arg for arg in args if not arg.startswith("--"))305 last_arg_idx = args.index(first_cmd)306 307 opts = parser.parse_args(args[:last_arg_idx])308 cmd = args[last_arg_idx:]309 310 # clear the profile file env, so that we don't generate profdata311 # when capturing the cc1 command312 cc1_env = test_env313 cc1_env["LLVM_PROFILE_FILE"] = os.devnull314 cc1_cmd = get_cc1_command_for_args(cmd, cc1_env)315 316 subprocess.check_call(cc1_cmd)317 return 0318 319 320def parse_dtrace_symbol_file(path, all_symbols, all_symbols_set, missing_symbols, opts):321 def fix_mangling(symbol):322 if sys.platform == "darwin":323 if symbol[0] != "_" and symbol != "start":324 symbol = "_" + symbol325 return symbol326 327 def get_symbols_with_prefix(symbol):328 start_index = bisect.bisect_left(all_symbols, symbol)329 for s in all_symbols[start_index:]:330 if not s.startswith(symbol):331 break332 yield s333 334 # Extract the list of symbols from the given file, which is assumed to be335 # the output of a dtrace run logging either probefunc or ustack(1) and336 # nothing else. The dtrace -xdemangle option needs to be used.337 #338 # This is particular to OS X at the moment, because of the '_' handling.339 with open(path) as f:340 current_timestamp = None341 for ln in f:342 # Drop leading and trailing whitespace.343 ln = ln.strip()344 if not ln.startswith("dtrace-"):345 continue346 347 # If this is a timestamp specifier, extract it.348 if ln.startswith("dtrace-TS: "):349 _, data = ln.split(": ", 1)350 if not data.isdigit():351 print(352 "warning: unrecognized timestamp line %r, ignoring" % ln,353 file=sys.stderr,354 )355 continue356 current_timestamp = int(data)357 continue358 elif ln.startswith("dtrace-Symbol: "):359 360 _, ln = ln.split(": ", 1)361 if not ln:362 continue363 364 # If there is a '`' in the line, assume it is a ustack(1) entry in365 # the form of <modulename>`<modulefunc>, where <modulefunc> is never366 # truncated (but does need the mangling patched).367 if "`" in ln:368 yield (current_timestamp, fix_mangling(ln.split("`", 1)[1]))369 continue370 371 # Otherwise, assume this is a probefunc printout. DTrace on OS X372 # seems to have a bug where it prints the mangled version of symbols373 # which aren't C++ mangled. We just add a '_' to anything but start374 # which doesn't already have a '_'.375 symbol = fix_mangling(ln)376 377 # If we don't know all the symbols, or the symbol is one of them,378 # just return it.379 if not all_symbols_set or symbol in all_symbols_set:380 yield (current_timestamp, symbol)381 continue382 383 # Otherwise, we have a symbol name which isn't present in the384 # binary. We assume it is truncated, and try to extend it.385 386 # Get all the symbols with this prefix.387 possible_symbols = list(get_symbols_with_prefix(symbol))388 if not possible_symbols:389 continue390 391 # If we found too many possible symbols, ignore this as a prefix.392 if len(possible_symbols) > 100:393 print(394 "warning: ignoring symbol %r " % symbol395 + "(no match and too many possible suffixes)",396 file=sys.stderr,397 )398 continue399 400 # Report that we resolved a missing symbol.401 if opts.show_missing_symbols and symbol not in missing_symbols:402 print(403 "warning: resolved missing symbol %r" % symbol, file=sys.stderr404 )405 missing_symbols.add(symbol)406 407 # Otherwise, treat all the possible matches as having occurred. This408 # is an over-approximation, but it should be ok in practice.409 for s in possible_symbols:410 yield (current_timestamp, s)411 412 413def uniq(list):414 seen = set()415 for item in list:416 if item not in seen:417 yield item418 seen.add(item)419 420 421def form_by_call_order(symbol_lists):422 # Simply strategy, just return symbols in order of occurrence, even across423 # multiple runs.424 return uniq(s for symbols in symbol_lists for s in symbols)425 426 427def form_by_call_order_fair(symbol_lists):428 # More complicated strategy that tries to respect the call order across all429 # of the test cases, instead of giving a huge preference to the first test430 # case.431 432 # First, uniq all the lists.433 uniq_lists = [list(uniq(symbols)) for symbols in symbol_lists]434 435 # Compute the successors for each list.436 succs = {}437 for symbols in uniq_lists:438 for a, b in zip(symbols[:-1], symbols[1:]):439 succs[a] = items = succs.get(a, [])440 if b not in items:441 items.append(b)442 443 # Emit all the symbols, but make sure to always emit all successors from any444 # call list whenever we see a symbol.445 #446 # There isn't much science here, but this sometimes works better than the447 # more naive strategy. Then again, sometimes it doesn't so more research is448 # probably needed.449 return uniq(450 s451 for symbols in symbol_lists452 for node in symbols453 for s in ([node] + succs.get(node, []))454 )455 456 457def form_by_frequency(symbol_lists):458 # Form the order file by just putting the most commonly occurring symbols459 # first. This assumes the data files didn't use the oneshot dtrace method.460 461 counts = {}462 for symbols in symbol_lists:463 for a in symbols:464 counts[a] = counts.get(a, 0) + 1465 466 by_count = list(counts.items())467 by_count.sort(key=lambda __n: -__n[1])468 return [s for s, n in by_count]469 470 471def form_by_random(symbol_lists):472 # Randomize the symbols.473 merged_symbols = uniq(s for symbols in symbol_lists for s in symbols)474 random.shuffle(merged_symbols)475 return merged_symbols476 477 478def form_by_alphabetical(symbol_lists):479 # Alphabetize the symbols.480 merged_symbols = list(set(s for symbols in symbol_lists for s in symbols))481 merged_symbols.sort()482 return merged_symbols483 484 485methods = dict(486 (name[len("form_by_") :], value)487 for name, value in locals().items()488 if name.startswith("form_by_")489)490 491 492def genOrderFile(args):493 parser = argparse.ArgumentParser("%prog [options] <dtrace data file directories>]")494 parser.add_argument("input", nargs="+", help="")495 parser.add_argument(496 "--binary",497 metavar="PATH",498 type=str,499 dest="binary_path",500 help="Path to the binary being ordered (for getting all symbols)",501 default=None,502 )503 parser.add_argument(504 "--output",505 dest="output_path",506 help="path to output order file to write",507 default=None,508 required=True,509 metavar="PATH",510 )511 parser.add_argument(512 "--show-missing-symbols",513 dest="show_missing_symbols",514 help="show symbols which are 'fixed up' to a valid name (requires --binary)",515 action="store_true",516 default=None,517 )518 parser.add_argument(519 "--output-unordered-symbols",520 dest="output_unordered_symbols_path",521 help="write a list of the unordered symbols to PATH (requires --binary)",522 default=None,523 metavar="PATH",524 )525 parser.add_argument(526 "--method",527 dest="method",528 help="order file generation method to use",529 choices=list(methods.keys()),530 default="call_order",531 )532 opts = parser.parse_args(args)533 534 # If the user gave us a binary, get all the symbols in the binary by535 # snarfing 'nm' output.536 if opts.binary_path is not None:537 output = subprocess.check_output(538 ["nm", "-P", opts.binary_path], universal_newlines=True539 )540 lines = output.split("\n")541 all_symbols = [ln.split(" ", 1)[0] for ln in lines if ln.strip()]542 print("found %d symbols in binary" % len(all_symbols))543 all_symbols.sort()544 else:545 all_symbols = []546 all_symbols_set = set(all_symbols)547 548 # Compute the list of input files.549 input_files = []550 for dirname in opts.input:551 input_files.extend(findFilesWithExtension(dirname, "dtrace"))552 553 # Load all of the input files.554 print("loading from %d data files" % len(input_files))555 missing_symbols = set()556 timestamped_symbol_lists = [557 list(558 parse_dtrace_symbol_file(559 path, all_symbols, all_symbols_set, missing_symbols, opts560 )561 )562 for path in input_files563 ]564 565 # Reorder each symbol list.566 symbol_lists = []567 for timestamped_symbols_list in timestamped_symbol_lists:568 timestamped_symbols_list.sort()569 symbol_lists.append([symbol for _, symbol in timestamped_symbols_list])570 571 # Execute the desire order file generation method.572 method = methods.get(opts.method)573 result = list(method(symbol_lists))574 575 # Report to the user on what percentage of symbols are present in the order576 # file.577 num_ordered_symbols = len(result)578 if all_symbols:579 print(580 "note: order file contains %d/%d symbols (%.2f%%)"581 % (582 num_ordered_symbols,583 len(all_symbols),584 100.0 * num_ordered_symbols / len(all_symbols),585 ),586 file=sys.stderr,587 )588 589 if opts.output_unordered_symbols_path:590 ordered_symbols_set = set(result)591 with open(opts.output_unordered_symbols_path, "w") as f:592 f.write("\n".join(s for s in all_symbols if s not in ordered_symbols_set))593 594 # Write the order file.595 with open(opts.output_path, "w") as f:596 f.write("\n".join(result))597 f.write("\n")598 599 return 0600 601 602def filter_bolt_optimized(inputs, instrumented_outputs, readelf):603 new_inputs = []604 new_instrumented_ouputs = []605 for input, instrumented_output in zip(inputs, instrumented_outputs):606 output = subprocess.check_output(607 [readelf, "-WS", input], universal_newlines=True608 )609 610 # This binary has already been bolt-optimized, so skip further processing.611 if re.search("\\.bolt\\.org\\.text", output, re.MULTILINE):612 print(f"Skipping {input}, it's already instrumented")613 else:614 new_inputs.append(input)615 new_instrumented_ouputs.append(instrumented_output)616 return new_inputs, new_instrumented_ouputs617 618 619def bolt_optimize(args):620 parser = argparse.ArgumentParser("%prog [options] ")621 parser.add_argument("--method", choices=["INSTRUMENT", "PERF", "LBR"])622 parser.add_argument("--input")623 parser.add_argument("--instrumented-output")624 parser.add_argument("--fdata")625 parser.add_argument("--perf-training-binary-dir")626 parser.add_argument("--readelf")627 parser.add_argument("--bolt")628 parser.add_argument("--lit")629 parser.add_argument("--merge-fdata")630 631 opts = parser.parse_args(args)632 633 inputs = opts.input.split(";")634 instrumented_outputs = opts.instrumented_output.split(";")635 assert len(inputs) == len(636 instrumented_outputs637 ), "inconsistent --input / --instrumented-output arguments"638 639 inputs, instrumented_outputs = filter_bolt_optimized(inputs,640 instrumented_outputs,641 opts.readelf)642 if not inputs:643 return 0644 645 environ = os.environ.copy()646 if opts.method == "INSTRUMENT":647 preloads = []648 for input, instrumented_output in zip(inputs, instrumented_outputs):649 args = [650 opts.bolt,651 input,652 "-o",653 instrumented_output,654 "-instrument",655 "--instrumentation-file-append-pid",656 f"--instrumentation-file={opts.fdata}",657 ]658 print("Running: " + " ".join(args))659 process = subprocess.run(660 args,661 stdout=subprocess.PIPE,662 stderr=subprocess.STDOUT,663 text=True,664 )665 666 for line in process.stdout:667 sys.stdout.write(line)668 process.check_returncode()669 670 # Shared library must be preloaded to be covered.671 if ".so" in input:672 preloads.append(instrumented_output)673 674 if preloads:675 print(676 f"Patching execution environment for dynamic libraries: {' '.join(preloads)}"677 )678 environ["LD_PRELOAD"] = os.pathsep.join(preloads)679 680 args = [681 sys.executable,682 opts.lit,683 "-v",684 os.path.join(opts.perf_training_binary_dir, f"bolt-fdata"),685 ]686 print("Running: " + " ".join(args))687 process = subprocess.run(688 args,689 stdout=subprocess.PIPE,690 stderr=subprocess.STDOUT,691 text=True,692 env=environ,693 )694 695 for line in process.stdout:696 sys.stdout.write(line)697 process.check_returncode()698 699 if opts.method in ["PERF", "LBR"]:700 args = [opts.bolt, opts.perf_training_binary_dir, opts.input]701 if opts.method == "LBR":702 args.extend("--lbr")703 perf2bolt(args)704 705 merge_fdata([opts.merge_fdata, opts.fdata, opts.perf_training_binary_dir])706 707 for input in inputs:708 shutil.copy(input, f"{input}-prebolt")709 710 args = [711 opts.bolt,712 f"{input}-prebolt",713 "-o",714 input,715 "-data",716 opts.fdata,717 "-reorder-blocks=ext-tsp",718 "-reorder-functions=cdsort",719 "-split-functions",720 "-split-all-cold",721 "-split-eh",722 "-dyno-stats",723 "-use-gnu-stack",724 "-update-debug-sections",725 "-ba" if opts.method == "PERF" else "",726 ]727 print("Running: " + " ".join(args))728 process = subprocess.run(729 args,730 stdout=subprocess.PIPE,731 stderr=subprocess.STDOUT,732 text=True,733 )734 735 for line in process.stdout:736 sys.stdout.write(line)737 process.check_returncode()738 739 740commands = {741 "bolt-optimize": bolt_optimize,742 "clean": clean,743 "merge": merge,744 "dtrace": dtrace,745 "cc1": cc1,746 "gen-order-file": genOrderFile,747 "merge-fdata": merge_fdata,748 "perf": perf,749 "perf2bolt": perf2bolt,750 "perf2prof": perf2prof,751}752 753 754def main():755 f = commands[sys.argv[1]]756 sys.exit(f(sys.argv[2:]))757 758 759if __name__ == "__main__":760 main()761