| #!/usr/bin/env python3 -u |
| |
| # Copyright (C) 2025 Apple Inc. All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # 3. Neither the name of Apple Inc. ("Apple") nor the names of |
| # its contributors may be used to endorse or promote products derived |
| # from this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY |
| # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY |
| # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| import subprocess |
| import argparse |
| import os |
| import shlex |
| from collections import defaultdict |
| |
| # Tool to compare optimization decisions (e.g., inlining, loop unrolling) between two JSC builds. |
| # Supports comparing by test or test list, with optional filtering by JIT type (e.g., FTL, DFG). |
| # |
| # Usage Examples: |
| # compare-optimization-tracing -v -a <BUILD_A_PATH> -b <BUILD_B_PATH> --test <SINGLE_TEST_PATH> --filter FTL |
| # compare-optimization-tracing -v -a <BUILD_A_PATH> --a-option "--usePartialLoopUnrolling=0 --maxLoopUnrollingCount=5" -b <BUILD_B_PATH> --b-option "--usePartialLoopUnrolling=1 --maxLoopUnrollingCount=10" --test <SINGLE_TEST_PATH> --filter FTL |
| # compare-optimization-tracing -v -a <BUILD_A_PATH> -b <BUILD_B_PATH> --test "-e \"testList='crypto'\" cli.js" --cwd <BENCHMARK_WORKING_DIR> --filter FTL |
| # compare-optimization-tracing -v -a <BUILD_A_PATH> -b <BUILD_B_PATH> --test-list <TEST_LIST_FILE_PATH> --cwd <BENCHMARK_WORKING_DIR> --filter FTL |
| |
| def run_jsc_and_capture(verbose, build_path, jsc_args, cwd, extra_options=""): |
| jsc_binary = os.path.join(build_path, "jsc") |
| env = os.environ.copy() |
| env["DYLD_FRAMEWORK_PATH"] = build_path |
| |
| required_options = [ |
| "--useConcurrentJIT=0", |
| "--dumpOptimizationTracing=1", |
| ] |
| |
| command = [jsc_binary] + jsc_args + required_options + shlex.split(extra_options) |
| |
| if verbose: |
| def human_friendly_quote(arg): |
| if " " in arg or "'" in arg or '"' in arg: |
| return f'"{arg}"' |
| return arg |
| |
| print("\nRunning command:") |
| print(" cwd:", cwd) |
| print(" cmd:", " ".join(human_friendly_quote(arg) for arg in command)) |
| print(" env:") |
| for key, value in sorted(env.items()): |
| if key in ["DYLD_FRAMEWORK_PATH"] or key.startswith("JSC") or key.startswith("WEBKIT"): |
| print(f" {key}={value}") |
| |
| try: |
| result = subprocess.run( |
| command, |
| env=env, |
| cwd=cwd, |
| stdout=subprocess.PIPE, |
| stderr=subprocess.STDOUT, |
| text=True, |
| check=True |
| ) |
| |
| if verbose: |
| print("Command completed successfully.") |
| return result.stdout.splitlines() |
| |
| except subprocess.CalledProcessError as e: |
| print("Command failed with return code:", e.returncode) |
| print("Output:") |
| print(e.stdout) |
| return [] |
| |
| def parse_prefixes(line): |
| """Extracts (category, jit_type) from line like: [InlineCall][FTL] Callee:...""" |
| if not (line.startswith("[") and "]" in line): |
| return None, None |
| try: |
| first_end = line.index("]") |
| second_start = line.index("[", first_end) |
| second_end = line.index("]", second_start) |
| category = line[0:first_end+1] |
| jit_type = line[second_start:second_end+1] |
| return category, jit_type |
| except ValueError: |
| return None, None |
| |
| def extract_lines_by_category(lines, filter_jit_type=None): |
| categorized = defaultdict(set) |
| for line in lines: |
| line = line.strip() |
| category, jit_type = parse_prefixes(line) |
| if category is None or jit_type is None: |
| continue |
| if filter_jit_type: |
| if jit_type.strip("[]") != filter_jit_type: |
| continue |
| categorized[category].add(line) |
| return categorized |
| |
| def compare_sets(set_a, set_b): |
| removed = sorted(set_a - set_b) |
| added = sorted(set_b - set_a) |
| return removed, added |
| |
| def run_comparison_for_test(args, jsc_args, test_name): |
| print(f"\n=== Running for test: {test_name} ===") |
| |
| if args.verbose: |
| print("Running build A...") |
| output_a = run_jsc_and_capture(args.verbose, args.build_a, jsc_args, args.cwd, extra_options=args.a_option) |
| categories_a = extract_lines_by_category(output_a, args.filter) |
| |
| if args.verbose: |
| print("Running build B...") |
| output_b = run_jsc_and_capture(args.verbose, args.build_b, jsc_args, args.cwd, extra_options=args.b_option) |
| categories_b = extract_lines_by_category(output_b, args.filter) |
| |
| all_categories = set(categories_a.keys()).union(categories_b.keys()) |
| |
| for category in sorted(all_categories): |
| set_a = categories_a.get(category, set()) |
| set_b = categories_b.get(category, set()) |
| |
| removed, added = compare_sets(set_a, set_b) |
| |
| print(f"\n--- Category: {category} ---") |
| |
| if args.verbose: |
| print("\nLogs from Build A:") |
| for line in sorted(set_a): |
| print(" A:", line) |
| print("\nLogs from Build B:") |
| for line in sorted(set_b): |
| print(" B:", line) |
| |
| print("\nRemoved:") |
| if removed: |
| for line in removed: |
| print(" -", line) |
| else: |
| print(" None") |
| |
| print("\nAdded:") |
| if added: |
| for line in added: |
| print(" +", line) |
| else: |
| print(" None") |
| |
| def main(): |
| parser = argparse.ArgumentParser(description="Compare JIT output categorized by optimization between two JSC builds.") |
| parser.add_argument('-a', '--build-a', required=True, help="Path to build A") |
| parser.add_argument('--a-option', default="", help="Extra JSC options for build A (quoted string of space-delimited options)") |
| parser.add_argument('-b', '--build-b', required=True, help="Path to build B") |
| parser.add_argument('--b-option', default="", help="Extra JSC options for build B (quoted string of space-delimited options)") |
| parser.add_argument('--test', help="Test file or CLI args (quoted)") |
| parser.add_argument('--test-list', help="Path to a file with one test name per line (replaces 'crypto' in --test)") |
| parser.add_argument('--cwd', default='.', help="Working directory (default: current directory)") |
| parser.add_argument('-v', '--verbose', action='store_true', help="Enable verbose output") |
| parser.add_argument('--filter', help="Only compare optimization entries for this JIT type (DFG, FTL, Baseline, LLInt, Host)") |
| |
| args = parser.parse_args() |
| |
| if args.test_list: |
| if not args.cwd: |
| parser.error("--cwd must be specified when using --test-list") |
| jsc_args = ['-e', "", 'cli.js'] |
| with open(args.test_list) as f: |
| test_names = [line.strip() for line in f if line.strip()] |
| for test_name in test_names: |
| jsc_args[1] = f"testList='{test_name}'" |
| run_comparison_for_test(args, jsc_args, test_name) |
| elif args.test: |
| jsc_args = shlex.split(args.test) |
| run_comparison_for_test(args, jsc_args, args.test) |
| else: |
| parser.error("either --test or --test-list is needed") |
| |
| if __name__ == "__main__": |
| main() |