| #!/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 | 
 | """Posts benchmark results to GitHub as pull request comments. | 
 |  | 
 | This script is meant to be used by Buildkite for automation. It requires the | 
 | following environment to be set: | 
 |  | 
 | - BUILDKITE_BUILD_URL: the link to the current Buildkite build. | 
 | - BUILDKITE_COMMIT: the pull request HEAD commit. | 
 | - BUILDKITE_PULL_REQUEST: the current pull request number. | 
 | - GITHUB_TOKEN: personal access token to authenticate against GitHub API. | 
 |  | 
 | if --query-base in toggled on, then it additionally requires: | 
 |  | 
 | - BUILDKITE_PULL_REQUEST_BASE_BRANCH: the targeting base branch. | 
 | - IREE_DASHBOARD_URL: the url to IREE's performance dashboard. | 
 |  | 
 | This script uses pip package "markdown_strings". | 
 |  | 
 | Example usage: | 
 |   # Export necessary environment variables: | 
 |   export ... | 
 |   # Then run the script: | 
 |   python3 post_benchmarks_as_pr_comment.py <benchmark-json-file>... | 
 |   #   where each <benchmark-json-file> is expected to be of format expected | 
 |   #   by BenchmarkResults objects. | 
 | """ | 
 |  | 
 | import argparse | 
 | import json | 
 | import os | 
 | import requests | 
 | import markdown_strings as md | 
 |  | 
 | from typing import Any, Dict, Sequence, Tuple, Union | 
 |  | 
 | from common.benchmark_description import BenchmarkResults, get_output | 
 |  | 
 | GITHUB_IREE_API_PREFIX = "https://api.github.com/repos/google/iree" | 
 | IREE_PROJECT_ID = 'IREE' | 
 | THIS_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) | 
 | RESULT_EMPHASIS_THRESHOLD = 0.05 | 
 |  | 
 |  | 
 | def get_git_commit_hash(commit: str, verbose: bool = False) -> str: | 
 |   """Gets the commit hash for the given commit.""" | 
 |   return get_output(['git', 'rev-parse', commit], | 
 |                     cwd=THIS_DIRECTORY, | 
 |                     verbose=verbose) | 
 |  | 
 |  | 
 | def get_git_total_commit_count(commit: str, verbose: bool = False) -> int: | 
 |   """Gets the total commit count in history ending with the given commit.""" | 
 |   count = get_output(['git', 'rev-list', '--count', commit], | 
 |                      cwd=THIS_DIRECTORY, | 
 |                      verbose=verbose) | 
 |   return int(count) | 
 |  | 
 |  | 
 | def get_required_env_var(var: str) -> str: | 
 |   """Gets the value for a required environment variable.""" | 
 |   value = os.getenv(var, None) | 
 |   if value is None: | 
 |     raise RuntimeError(f'Missing environment variable "{var}"') | 
 |   return value | 
 |  | 
 |  | 
 | def get_from_dashboard(url: str, | 
 |                        payload: Dict[str, Any], | 
 |                        verbose: bool = False) -> Dict[str, int]: | 
 |   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}') | 
 |  | 
 |   return response.json() | 
 |  | 
 |  | 
 | def aggregate_all_benchmarks( | 
 |     benchmark_files: Sequence[str]) -> Sequence[Tuple[Union[str, int]]]: | 
 |   """Aggregates all benchmarks in the given files. | 
 |  | 
 |   Args: | 
 |   - benchmark_files: A list of JSON files, each can be decoded as a | 
 |     BenchmarkResults. | 
 |  | 
 |   Returns: | 
 |   - A list of (name, mean-latency, median-latency, stddev-latency) tuples. | 
 |   """ | 
 |  | 
 |   pr_commit = get_required_env_var("BUILDKITE_COMMIT") | 
 |   aggregate_results = {} | 
 |  | 
 |   for benchmark_file in benchmark_files: | 
 |     with open(benchmark_file) as f: | 
 |       content = f.read() | 
 |     file_results = BenchmarkResults.from_json_str(content) | 
 |  | 
 |     if file_results.commit != pr_commit: | 
 |       raise ValueError("Inconsistent pull request commit") | 
 |  | 
 |     for benchmark_index in range(len(file_results.benchmarks)): | 
 |       benchmark_case = file_results.benchmarks[benchmark_index] | 
 |  | 
 |       # Make sure each benchmark has a unique name. | 
 |       name = str(benchmark_case["benchmark"]) | 
 |       if name in aggregate_results: | 
 |         raise ValueError(f"Duplicated benchmarks: {name}") | 
 |  | 
 |       # Now scan all benchmark iterations and find the aggregate results. | 
 |       mean_time = file_results.get_aggregate_time(benchmark_index, "mean") | 
 |       median_time = file_results.get_aggregate_time(benchmark_index, "median") | 
 |       stddev_time = file_results.get_aggregate_time(benchmark_index, "stddev") | 
 |  | 
 |       aggregate_results[name] = (mean_time, median_time, stddev_time) | 
 |  | 
 |   return sorted([(k,) + v for k, v in aggregate_results.items()]) | 
 |  | 
 |  | 
 | def query_base_benchmark_results(commit, | 
 |                                  verbose: bool = False) -> Dict[str, int]: | 
 |   """Queries the benchmark results for the given commit.""" | 
 |   build_id = get_git_total_commit_count(commit, verbose) | 
 |  | 
 |   url = get_required_env_var('IREE_DASHBOARD_URL') | 
 |   payload = {'projectId': IREE_PROJECT_ID, 'buildId': build_id} | 
 |   return get_from_dashboard(f'{url}/apis/getBuild', payload, verbose=verbose) | 
 |  | 
 |  | 
 | def get_comparsion_against_base(pr_means: Sequence[int], | 
 |                                 base_means: Sequence[int]) -> Sequence[str]: | 
 |   """Returns a tuple of strings comparsing mean latency numbers.""" | 
 |   comparisions = [] | 
 |  | 
 |   for pr, base in zip(pr_means, base_means): | 
 |     if base is None: | 
 |       comparisions.append(str(pr)) | 
 |       continue | 
 |  | 
 |     diff = abs(pr - base) / base | 
 |     if pr > base: | 
 |       percent = "{:.2%}".format(diff) | 
 |       direction = "↑" | 
 |       if diff > RESULT_EMPHASIS_THRESHOLD: | 
 |         direction += ", 🚩" | 
 |     elif pr < base: | 
 |       percent = "{:.2%}".format(diff) | 
 |       direction = "↓" | 
 |       if diff > RESULT_EMPHASIS_THRESHOLD: | 
 |         direction += ", 🎉" | 
 |     else: | 
 |       percent = "{:.0%}".format(diff) | 
 |       direction = "" | 
 |  | 
 |     comparisions.append(f"{pr} (vs. {base}, {percent}{direction})") | 
 |  | 
 |   return tuple(comparisions) | 
 |  | 
 |  | 
 | def get_benchmark_result_markdown(benchmark_files: Sequence[str], | 
 |                                   query_base: bool, | 
 |                                   verbose: bool = False) -> str: | 
 |   """Gets markdown summary of all benchmarks in the given files.""" | 
 |   all_benchmarks = aggregate_all_benchmarks(benchmark_files) | 
 |   names, means, medians, stddevs = zip(*all_benchmarks) | 
 |  | 
 |   build_url = get_required_env_var("BUILDKITE_BUILD_URL") | 
 |   pr_commit = get_required_env_var("BUILDKITE_COMMIT") | 
 |   if query_base: | 
 |     base_branch = get_required_env_var("BUILDKITE_PULL_REQUEST_BASE_BRANCH") | 
 |     commit = get_git_commit_hash(base_branch, verbose) | 
 |     base_benchmarks = query_base_benchmark_results(commit, verbose) | 
 |     base_means = [base_benchmarks.get(v) for v in names] | 
 |     means = get_comparsion_against_base(means, base_means) | 
 |     commit_info = f"@ commit {pr_commit} (vs. base {commit})" | 
 |   else: | 
 |     commit_info = f"@ commit {pr_commit}" | 
 |  | 
 |   names = ("Benchmark Name",) + names | 
 |   means = ("Average Latency (ms)",) + means | 
 |   medians = ("Median Latency (ms)",) + medians | 
 |   stddevs = ("Latency Standard Deviation (ms)",) + stddevs | 
 |  | 
 |   header = md.header("Benchmark results", 3) | 
 |   benchmark_table = md.table([names, means, medians, stddevs]) | 
 |   link = "See more details on " + md.link("Buildkite", build_url) | 
 |  | 
 |   return "\n\n".join([header, commit_info, benchmark_table, link]) | 
 |  | 
 |  | 
 | def comment_on_pr(content): | 
 |   """Posts the given content as comments to the current pull request.""" | 
 |   pr_number = get_required_env_var("BUILDKITE_PULL_REQUEST") | 
 |   # Buildkite sets this to "false" if not running on a PR: | 
 |   # https://buildkite.com/docs/pipelines/environment-variables#bk-env-vars-buildkite-pull-request | 
 |   if pr_number == "false": | 
 |     raise ValueError("Not a pull request") | 
 |  | 
 |   api_token = get_required_env_var('GITHUB_TOKEN') | 
 |   headers = { | 
 |       "Accept": "application/vnd.github.v3+json", | 
 |       "Authorization": f"token {api_token}", | 
 |   } | 
 |   payload = json.dumps({"event": "COMMENT", "body": content}) | 
 |  | 
 |   api_endpoint = f"{GITHUB_IREE_API_PREFIX}/pulls/{pr_number}/reviews" | 
 |   request = requests.post(api_endpoint, data=payload, headers=headers) | 
 |   if request.status_code != 200: | 
 |     raise requests.RequestException( | 
 |         f"Failed to comment on GitHub; error code: {request.status_code}") | 
 |  | 
 |  | 
 | def parse_arguments(): | 
 |   """Parses command-line options.""" | 
 |  | 
 |   def check_file_path(path): | 
 |     if os.path.isfile(path): | 
 |       return path | 
 |     else: | 
 |       raise ValueError(path) | 
 |  | 
 |   parser = argparse.ArgumentParser() | 
 |   parser.add_argument("benchmark_files", | 
 |                       metavar="<benchmark-json-file>", | 
 |                       type=check_file_path, | 
 |                       nargs="+", | 
 |                       help="Path to the JSON file containing benchmark results") | 
 |   parser.add_argument("--dry-run", | 
 |                       action="store_true", | 
 |                       help="Print the comment instead of posting to GitHub") | 
 |   parser.add_argument( | 
 |       "--query-base", | 
 |       action="store_true", | 
 |       help= | 
 |       "Query the dashboard for the benchmark results of the targeting base branch" | 
 |   ) | 
 |   parser.add_argument("--verbose", | 
 |                       action="store_true", | 
 |                       help="Print internal information during execution") | 
 |   args = parser.parse_args() | 
 |  | 
 |   return args | 
 |  | 
 |  | 
 | def main(args): | 
 |   benchmarks_md = get_benchmark_result_markdown(args.benchmark_files, | 
 |                                                 query_base=args.query_base, | 
 |                                                 verbose=args.verbose) | 
 |  | 
 |   if args.dry_run: | 
 |     print(benchmarks_md) | 
 |   else: | 
 |     comment_on_pr(benchmarks_md) | 
 |  | 
 |  | 
 | if __name__ == "__main__": | 
 |   main(parse_arguments()) |