|  | #!/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_output( | 
|  | ['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_output(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_output( | 
|  | ['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_output( | 
|  | 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=[], | 
|  | nargs="+", | 
|  | help=("Paths to the JSON files containing benchmark results, " | 
|  | "accepts wildcards")) | 
|  | parser.add_argument( | 
|  | "--compile_stats_files", | 
|  | metavar="<compile-stats-json-files>", | 
|  | default=[], | 
|  | nargs="+", | 
|  | 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()) |