| #!/usr/bin/env python3 |
| # Copyright 2021 The IREE Authors |
| # |
| # Licensed under the Apache License v2.0 with LLVM Exceptions. |
| # See https://llvm.org/LICENSE.txt for license information. |
| # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| """Generates benchmark results as pull request comments. |
| |
| This script is meant to be used by CI and uses pip package "markdown_strings". |
| """ |
| |
| import sys |
| import pathlib |
| |
| # Add build_tools python dir to the search path. |
| sys.path.insert(0, str(pathlib.Path(__file__).parent.with_name("python"))) |
| |
| import argparse |
| import dataclasses |
| import json |
| from typing import Any, Dict, Optional, Set, Tuple |
| |
| import markdown_strings as md |
| import requests |
| |
| from common import benchmark_definition, benchmark_presentation, common_arguments |
| from reporting import benchmark_comment |
| |
| GITHUB_IREE_REPO_PREFIX = "https://github.com/openxla/iree" |
| IREE_DASHBOARD_URL = "https://perf.iree.dev/apis/v2" |
| IREE_PROJECT_ID = 'IREE' |
| # The maximal numbers of trials when querying base commit benchmark results. |
| MAX_BASE_COMMIT_QUERY_COUNT = 10 |
| # The max number of rows to show per table. |
| TABLE_SIZE_CUT = 3 |
| THIS_DIRECTORY = pathlib.Path(__file__).resolve().parent |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class CommentDef(object): |
| title: str |
| type_id: str |
| |
| |
| # Map from comment type to comment definition. |
| COMMENT_DEF_MAP = { |
| "android-benchmark-summary": |
| CommentDef(title="Abbreviated Android Benchmark Summary", |
| type_id="bf8cdf94-a992-466d-b11c-778cbd805a22"), |
| "linux-benchmark-summary": |
| CommentDef(title="Abbreviated Linux Benchmark Summary", |
| type_id="37549014-3c67-4e74-8d88-8e929231abe3"), |
| "benchmark-summary": |
| CommentDef(title="Abbreviated Benchmark Summary", |
| type_id="5b42cbfe-26a0-4164-a51c-07f06762e2dc") |
| } |
| |
| |
| def get_git_total_commit_count(commit: str, verbose: bool = False) -> int: |
| """Gets the total commit count in history ending with the given commit.""" |
| # TODO(#11703): Should use --first-parent here. See issue for the required |
| # work. |
| count = benchmark_definition.execute_cmd_and_get_stdout( |
| ['git', 'rev-list', '--count', commit], |
| cwd=THIS_DIRECTORY, |
| verbose=verbose) |
| return int(count) |
| |
| |
| def get_from_dashboard(url: str, |
| payload: Dict[str, Any], |
| verbose: bool = False) -> Dict[str, Dict[str, Any]]: |
| headers = {'Content-type': 'application/json'} |
| data = json.dumps(payload) |
| |
| if verbose: |
| print(f'API request payload: {data}') |
| |
| response = requests.get(url, data=data, headers=headers) |
| code = response.status_code |
| if code != 200: |
| raise requests.RequestException( |
| f'Failed to get from dashboard server with status code {code}') |
| |
| data = response.json() |
| if verbose: |
| print(f'Queried base benchmark data: {data}') |
| return data |
| |
| |
| BenchmarkQueryResults = Dict[str, Dict[str, Any]] |
| |
| |
| def query_base_benchmark_results( |
| commit: str, verbose: bool = False) -> BenchmarkQueryResults: |
| """Queries the benchmark results for the given commit.""" |
| build_id = get_git_total_commit_count(commit, verbose) |
| payload = {'projectId': IREE_PROJECT_ID, 'buildId': build_id} |
| return get_from_dashboard(f'{IREE_DASHBOARD_URL}/getBuild', |
| payload, |
| verbose=verbose) |
| |
| |
| @dataclasses.dataclass(frozen=True) |
| class ComparableBenchmarkResults(object): |
| commit_sha: str |
| benchmark_results: BenchmarkQueryResults |
| |
| |
| def _find_comparable_benchmark_results( |
| start_commit: str, |
| required_benchmark_keys: Set[str], |
| verbose: bool = False) -> Optional[ComparableBenchmarkResults]: |
| cmds = [ |
| "git", "rev-list", "--first-parent", |
| f"--max-count={MAX_BASE_COMMIT_QUERY_COUNT}", start_commit |
| ] |
| output = benchmark_definition.execute_cmd_and_get_stdout(cmds, |
| cwd=THIS_DIRECTORY, |
| verbose=verbose) |
| previous_commits = output.splitlines() |
| # Try to query some base benchmark to diff against, from the top of the |
| # tree. Bail out if the maximal trial number is exceeded. |
| for base_commit in previous_commits: |
| base_benchmarks = query_base_benchmark_results(commit=base_commit, |
| verbose=verbose) |
| base_benchmark_keys = set(base_benchmarks.keys()) |
| if required_benchmark_keys <= base_benchmark_keys: |
| return ComparableBenchmarkResults(commit_sha=base_commit, |
| benchmark_results=base_benchmarks) |
| |
| return None |
| |
| |
| def _get_git_commit_hash(ref: str, verbose: bool = False) -> str: |
| """Gets the commit hash for the given commit.""" |
| return benchmark_definition.execute_cmd_and_get_stdout( |
| ['git', 'rev-parse', ref], cwd=THIS_DIRECTORY, verbose=verbose) |
| |
| |
| def _get_git_merge_base_commit(pr_commit: str, |
| target_branch: str, |
| verbose: bool = False) -> str: |
| return benchmark_definition.execute_cmd_and_get_stdout( |
| args=["git", "merge-base", target_branch, pr_commit], |
| cwd=THIS_DIRECTORY, |
| verbose=verbose) |
| |
| |
| def _get_benchmark_result_markdown( |
| execution_benchmarks: Dict[ |
| str, benchmark_presentation.AggregateBenchmarkLatency], |
| compilation_metrics: Dict[str, benchmark_presentation.CompilationMetrics], |
| pr_url: str, build_url: str, comment_def: CommentDef, |
| commit_info_md: str) -> Tuple[str, str]: |
| """Gets the full/abbreviated markdown summary of all benchmarks in files.""" |
| |
| pr_info = md.link("Pull request", pr_url) |
| build_info = md.link("Build", build_url) |
| |
| # Compose the full benchmark tables. |
| full_table = [md.header("Full Benchmark Summary", 2)] |
| full_table.append(md.unordered_list([commit_info_md, pr_info, build_info])) |
| |
| # Compose the abbreviated benchmark tables. |
| abbr_table = [md.header(comment_def.title, 2)] |
| abbr_table.append(commit_info_md) |
| |
| if len(execution_benchmarks) > 0: |
| full_table.append( |
| benchmark_presentation.categorize_benchmarks_into_tables( |
| execution_benchmarks)) |
| |
| abbr_benchmarks_tables = benchmark_presentation.categorize_benchmarks_into_tables( |
| execution_benchmarks, TABLE_SIZE_CUT) |
| if len(abbr_benchmarks_tables) == 0: |
| abbr_table.append("No improved or regressed benchmarks 🏖️") |
| else: |
| abbr_table.append(abbr_benchmarks_tables) |
| |
| # Compose the full compilation metrics tables. |
| if len(compilation_metrics) > 0: |
| full_table.append( |
| benchmark_presentation.categorize_compilation_metrics_into_tables( |
| compilation_metrics)) |
| |
| abbr_compilation_metrics_tables = benchmark_presentation.categorize_compilation_metrics_into_tables( |
| compilation_metrics, TABLE_SIZE_CUT) |
| if len(abbr_compilation_metrics_tables) == 0: |
| abbr_table.append("No improved or regressed compilation metrics 🏖️") |
| else: |
| abbr_table.append(abbr_compilation_metrics_tables) |
| |
| abbr_table.append("For more information:") |
| # We don't know until a Gist is really created. Use a placeholder for now and |
| # replace later. |
| full_result_info = md.link("Full benchmark result tables", |
| benchmark_comment.GIST_LINK_PLACEHORDER) |
| abbr_table.append(md.unordered_list([full_result_info, build_info])) |
| |
| # Append the unique comment type id to help identify and update the existing |
| # comment. |
| abbr_table.append(f"<!--Comment type id: {comment_def.type_id}-->") |
| |
| return "\n\n".join(full_table), "\n\n".join(abbr_table) |
| |
| |
| def parse_arguments(): |
| """Parses command-line options.""" |
| |
| parser = argparse.ArgumentParser() |
| parser.add_argument( |
| "--benchmark_files", |
| metavar="<benchmark-json-files>", |
| default=[], |
| action="append", |
| help=("Paths to the JSON files containing benchmark results, " |
| "accepts wildcards")) |
| parser.add_argument( |
| "--compile_stats_files", |
| metavar="<compile-stats-json-files>", |
| default=[], |
| action="append", |
| help=("Paths to the JSON files containing compilation statistics, " |
| "accepts wildcards")) |
| parser.add_argument("--pr_number", required=True, type=int, help="PR number") |
| parser.add_argument("--pr_committish", |
| type=str, |
| default="HEAD", |
| help="PR commit hash or ref") |
| parser.add_argument("--pr_base_branch", |
| type=str, |
| default=None, |
| help="Base branch to merge the PR.") |
| parser.add_argument("--comment_type", |
| required=True, |
| choices=COMMENT_DEF_MAP.keys(), |
| help="Type of summary comment") |
| parser.add_argument("--build_url", |
| required=True, |
| type=str, |
| help="CI build page url to show in the report") |
| parser.add_argument("--output", type=pathlib.Path, default=None) |
| parser.add_argument("--verbose", |
| action="store_true", |
| help="Print internal information during execution") |
| |
| return parser.parse_args() |
| |
| |
| def main(args): |
| benchmark_files = common_arguments.expand_and_check_file_paths( |
| args.benchmark_files) |
| compile_stats_files = common_arguments.expand_and_check_file_paths( |
| args.compile_stats_files) |
| |
| pr_commit = _get_git_commit_hash(ref=args.pr_committish, verbose=args.verbose) |
| execution_benchmarks = benchmark_presentation.aggregate_all_benchmarks( |
| benchmark_files=benchmark_files, expected_pr_commit=pr_commit) |
| compilation_metrics = benchmark_presentation.collect_all_compilation_metrics( |
| compile_stats_files=compile_stats_files, expected_pr_commit=pr_commit) |
| |
| if args.pr_base_branch is None: |
| pr_base_commit = None |
| else: |
| pr_base_commit = _get_git_merge_base_commit( |
| pr_commit=pr_commit, |
| target_branch=args.pr_base_branch, |
| verbose=args.verbose) |
| |
| if pr_base_commit is None: |
| comparable_results = None |
| else: |
| required_benchmark_keys = set(execution_benchmarks.keys()) |
| for target_id in compilation_metrics: |
| for mapper in benchmark_presentation.COMPILATION_METRICS_TO_TABLE_MAPPERS: |
| required_benchmark_keys.add(mapper.get_series_id(target_id)) |
| |
| comparable_results = _find_comparable_benchmark_results( |
| start_commit=pr_base_commit, |
| required_benchmark_keys=required_benchmark_keys, |
| verbose=args.verbose) |
| |
| if comparable_results is None: |
| comparable_commit = None |
| else: |
| comparable_commit = comparable_results.commit_sha |
| # Update the execution benchmarks with base numbers. |
| for bench in execution_benchmarks: |
| base_benchmark = comparable_results.benchmark_results[bench] |
| if base_benchmark["sampleUnit"] != "ns": |
| raise ValueError("Only support nanoseconds for latency sample.") |
| execution_benchmarks[bench].base_mean_time = base_benchmark["sample"] |
| |
| # Update the compilation metrics with base numbers. |
| for target_id, metrics in compilation_metrics.items(): |
| updated_metrics = metrics |
| for mapper in benchmark_presentation.COMPILATION_METRICS_TO_TABLE_MAPPERS: |
| base_benchmark = comparable_results.benchmark_results[ |
| mapper.get_series_id(target_id)] |
| if base_benchmark["sampleUnit"] != mapper.get_unit(): |
| raise ValueError("Unit of the queried sample is mismatched.") |
| updated_metrics = mapper.update_base_value(updated_metrics, |
| base_benchmark["sample"]) |
| compilation_metrics[target_id] = updated_metrics |
| |
| pr_commit_link = md.link(pr_commit, |
| f"{GITHUB_IREE_REPO_PREFIX}/commit/{pr_commit}") |
| commit_info_md = f"@ commit {pr_commit_link}" |
| if comparable_commit is not None: |
| baseline_commit_link = md.link( |
| comparable_commit, |
| f"{GITHUB_IREE_REPO_PREFIX}/commit/{comparable_commit}") |
| commit_info_md += f" (vs. base {baseline_commit_link})" |
| elif pr_base_commit is not None: |
| commit_info_md += " (no previous benchmark results to compare)" |
| |
| comment_def = COMMENT_DEF_MAP[args.comment_type] |
| full_md, abbr_md = _get_benchmark_result_markdown( |
| execution_benchmarks=execution_benchmarks, |
| compilation_metrics=compilation_metrics, |
| pr_url=f"{GITHUB_IREE_REPO_PREFIX}/pull/{args.pr_number}", |
| build_url=args.build_url, |
| comment_def=comment_def, |
| commit_info_md=commit_info_md) |
| |
| comment_data = benchmark_comment.CommentData( |
| type_id=comment_def.type_id, |
| abbr_md=abbr_md, |
| full_md=full_md, |
| unverified_pr_number=args.pr_number) |
| comment_json_data = json.dumps(dataclasses.asdict(comment_data), indent=2) |
| if args.output is None: |
| print(comment_json_data) |
| else: |
| args.output.write_text(comment_json_data) |
| |
| |
| if __name__ == "__main__": |
| main(parse_arguments()) |