| #!/usr/bin/env python3 |
| |
| import argparse |
| import glob |
| import math |
| import subprocess |
| import os |
| |
| PROFILED_DYLIBS = ["JavaScriptCore", "WebCore", "WebKit"] |
| BENCHMARK_GROUP_WEIGHTS = [("speedometer", 0.6), ("jetstream", 0.2), ("motionmark", 0.2)] |
| |
| def pad(string, max_length): |
| if len(string) > max_length: |
| return string[:max_length - 1] + u"…" |
| return string.ljust(max_length) |
| |
| |
| def shell(command): |
| return subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT).strip() |
| |
| |
| def shortened(name): |
| return name if len(name) < 200 else name[0:99] + u"…" + name[-98:] |
| |
| |
| def assert_directory(directory_path): |
| assert os.path.isdir(directory_path), f"No directory at {directory_path}" |
| return directory_path |
| |
| |
| def assert_file(file_path): |
| assert os.path.isfile(file_path), f"No file at {file_path}" |
| return file_path |
| |
| |
| def summarize_parser(subparsers): |
| parser = subparsers.add_parser("summarize", help="Dumps function names in a given .profraw file, sorted in descending order by function count") |
| parser.add_argument("file", help="Path to the .profraw file") |
| parser.set_defaults(func=summarize) |
| return parser |
| |
| |
| def summarize(args): |
| file = assert_file(args.file) |
| |
| lines = shell(f"xcrun -sdk macosx llvm-profdata show --all-functions --value-cutoff=10 \"{file}\" | c++filt -n").decode("utf-8").splitlines() |
| |
| counts_and_functions = [] |
| |
| for line_number in range(len(lines)): |
| line = lines[line_number].strip() |
| if line.startswith("Function count: "): |
| count = int(line.split()[-1]) |
| symbol = lines[line_number - 3].strip()[:-1] |
| counts_and_functions.append((count, symbol)) |
| |
| counts_and_functions.sort(reverse=True) |
| for count, name in counts_and_functions: |
| print(pad(str(count), 15), shortened(name)) |
| |
| |
| def merge_parser(subparsers): |
| parser = subparsers.add_parser("merge", help="Merge a pile of *.profraw files into the *.profdata files we can build with.") |
| parser.add_argument("directory", help="Path to the directory containing the *.profraw files") |
| parser.set_defaults(func=merge) |
| return parser |
| |
| |
| def merge(args): |
| path = assert_directory(args.directory) |
| |
| for lib in PROFILED_DYLIBS: |
| print("Merging", lib) |
| inputs = glob.glob(os.path.join(path, lib) + "*.profraw") |
| inputs = [f"\"{i}\"" for i in inputs] |
| inputs = " ".join(inputs) |
| output_file = os.path.join(path, lib + '.profdata') |
| print(shell(f"xcrun -sdk macosx.internal llvm-profdata merge --sparse {inputs} -output=\"{output_file}\"")) |
| |
| |
| def combine_parser(subparsers): |
| parser = subparsers.add_parser("combine", help="Combine directories containing *.profdata files from different platforms together.") |
| for i in range(0, len(BENCHMARK_GROUP_WEIGHTS)): |
| group, _ = BENCHMARK_GROUP_WEIGHTS[i] |
| parser.add_argument(f"--{group}", default=None, help=f"Path to the directory containing the *.profdata files from a {group} run.") |
| parser.add_argument("--output", help="Path to the directory where the output will be placed.") |
| parser.set_defaults(func=combine) |
| return parser |
| |
| |
| def combine(args): |
| assert args.output, "Must specify output directory." |
| out = assert_directory(args.output) |
| args = vars(args) |
| group_paths = [] |
| |
| for i in range(0, len(BENCHMARK_GROUP_WEIGHTS)): |
| group, weight = BENCHMARK_GROUP_WEIGHTS[i] |
| if args[group]: |
| path = assert_directory(args[group]) |
| group_paths.append((path, weight)) |
| |
| assert len(group_paths) > 0, "Must specify at least one group." |
| |
| sum = 0 |
| max = 0 |
| # We need to turn percentages into weights > 1, but we don't want crazy high multipliers. |
| # For example, if we have weights 0.35 and 0.65, we don't need a 7:13 ratio when 5:9 is good enough. |
| max_multiplier = 15 |
| for group, weight in group_paths: |
| sum = sum + weight |
| if weight > max: |
| max = weight |
| |
| gcd = int(max * max_multiplier) |
| for group, weight in group_paths: |
| gcd = math.gcd(gcd, int((weight / sum) * max_multiplier)) |
| for i in range(0, len(group_paths)): |
| group, weight = group_paths[i] |
| group_paths[i] = (group, int((weight / sum) * max_multiplier) // gcd) |
| |
| print("Simplified group weights: ", group_paths) |
| |
| for lib in PROFILED_DYLIBS: |
| print("Merging", lib) |
| group_input = ["--weighted-input={},\"{}\"".format(weight, os.path.join(path, lib + ".profdata")) for path, weight in group_paths] |
| group_input = " ".join(group_input) |
| output_file = os.path.join(out, lib + '.profdata') |
| print(shell(f"xcrun -sdk macosx.internal llvm-profdata merge --sparse {group_input} -output=\"{output_file}\"")) |
| |
| |
| def compress_parser(subparsers): |
| parser = subparsers.add_parser("compress", help="Compress *.profdata files so that they can be checked in.") |
| parser.add_argument("--input", help="Path to the directory containing the input *.profdata files.") |
| parser.add_argument("--output", help="Path to the directory where the output will be placed.") |
| parser.set_defaults(func=compress) |
| return parser |
| |
| |
| def compress(args): |
| out = assert_directory(args.output) |
| input_directory = assert_directory(args.input) |
| |
| for lib in PROFILED_DYLIBS: |
| print("Compressing", lib) |
| input_file = os.path.join(input_directory, lib + ".profdata") |
| output_file = os.path.join(out, lib + ".profdata.compressed") |
| print(shell(f"compression_tool -encode -i \"{input_file}\" -o \"{output_file}\" -a lzfse")) |
| |
| |
| if __name__ == "__main__": |
| parser = argparse.ArgumentParser(prog='pgo-profile') |
| subparsers = parser.add_subparsers(help='valid sub-commands', required=True, dest='sub command') |
| merge_parser(subparsers) |
| summarize_parser(subparsers) |
| combine_parser(subparsers) |
| compress_parser(subparsers) |
| |
| args = parser.parse_args() |
| try: |
| args.func(args) |
| except subprocess.CalledProcessError as e: |
| print(e.stdout) |
| raise e |