brintos

brintos / llvm-project-archived public Read only

0
0
Text · 23.8 KiB · 97b7711 Raw
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