|  | #!/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 | 
|  | """Upload benchmark results to IREE Benchmark Dashboards. | 
|  |  | 
|  | This script is meant to be used by Buildkite for automation. | 
|  |  | 
|  | Example usage: | 
|  | # Export necessary environment variables: | 
|  | export IREE_DASHBOARD_API_TOKEN=... | 
|  | # Then run the script: | 
|  | python3 upload_benchmarks.py /path/to/benchmark/json/file | 
|  | """ | 
|  |  | 
|  | import pathlib | 
|  | import sys | 
|  |  | 
|  | # Add build_tools python dir to the search path. | 
|  | sys.path.insert(0, str(pathlib.Path(__file__).parent.with_name("python"))) | 
|  |  | 
|  | import argparse | 
|  | import json | 
|  | import os | 
|  | import requests | 
|  |  | 
|  | from typing import Any, Dict, Optional, Union | 
|  |  | 
|  | from common.common_arguments import expand_and_check_file_paths | 
|  | from common import benchmark_definition, benchmark_presentation, benchmark_thresholds | 
|  |  | 
|  | IREE_DASHBOARD_URL = "https://perf.iree.dev" | 
|  | IREE_GITHUB_COMMIT_URL_PREFIX = 'https://github.com/openxla/iree/commit' | 
|  | IREE_PROJECT_ID = 'IREE' | 
|  | THIS_DIRECTORY = pathlib.Path(__file__).resolve().parent | 
|  |  | 
|  | COMMON_DESCRIPTION = """ | 
|  | <br> | 
|  | For the graph, the x axis is the Git commit index, and the y axis is the | 
|  | measured metrics. The unit for the numbers is shown in the "Unit" dropdown. | 
|  | <br> | 
|  | See <a href="https://github.com/openxla/iree/tree/main/benchmarks/dashboard.md"> | 
|  | https://github.com/openxla/iree/tree/main/benchmarks/dashboard.md | 
|  | </a> for benchmark philosophy, specification, and definitions. | 
|  | """ | 
|  |  | 
|  | # A non-exhaustive list of models and their source URLs. | 
|  | # For models listed here we can provide a nicer description for them on | 
|  | # webpage. | 
|  | IREE_TF_MODEL_SOURCE_URL = { | 
|  | 'MobileBertSquad': | 
|  | 'https://github.com/google-research/google-research/tree/master/mobilebert', | 
|  | 'MobileNetV2': | 
|  | 'https://www.tensorflow.org/api_docs/python/tf/keras/applications/MobileNetV2', | 
|  | 'MobileNetV3Small': | 
|  | 'https://www.tensorflow.org/api_docs/python/tf/keras/applications/MobileNetV3Small', | 
|  | } | 
|  |  | 
|  | IREE_TFLITE_MODEL_SOURCE_URL = { | 
|  | 'DeepLabV3': | 
|  | 'https://tfhub.dev/tensorflow/lite-model/deeplabv3/1/default/1', | 
|  | 'MobileSSD': | 
|  | 'https://www.tensorflow.org/lite/performance/gpu#demo_app_tutorials', | 
|  | 'PoseNet': | 
|  | 'https://tfhub.dev/tensorflow/lite-model/posenet/mobilenet/float/075/1/default/1', | 
|  | } | 
|  |  | 
|  |  | 
|  | def get_model_description(model_name: str, model_source: str) -> Optional[str]: | 
|  | """Gets the model description for the given benchmark.""" | 
|  | url = None | 
|  | if model_source == "TensorFlow": | 
|  | url = IREE_TF_MODEL_SOURCE_URL.get(model_name) | 
|  | elif model_source == "TFLite": | 
|  | url = IREE_TFLITE_MODEL_SOURCE_URL.get(model_name) | 
|  | if url is not None: | 
|  | description = f'{model_name} from <a href="{url}">{url}</a>.' | 
|  | return description | 
|  | return None | 
|  |  | 
|  |  | 
|  | def get_git_commit_hash(commit: str, verbose: bool = False) -> str: | 
|  | """Gets the commit hash for the given commit.""" | 
|  | return benchmark_definition.execute_cmd_and_get_stdout( | 
|  | ['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 = benchmark_definition.execute_cmd_and_get_stdout( | 
|  | ['git', 'rev-list', '--count', commit], | 
|  | cwd=THIS_DIRECTORY, | 
|  | verbose=verbose) | 
|  | return int(count) | 
|  |  | 
|  |  | 
|  | def get_git_commit_info(commit: str, verbose: bool = False) -> Dict[str, str]: | 
|  | """Gets commit information dictionary for the given commit.""" | 
|  | cmd = [ | 
|  | 'git', 'show', '--format=%H:::%h:::%an:::%ae:::%s', '--no-patch', commit | 
|  | ] | 
|  | info = benchmark_definition.execute_cmd_and_get_stdout(cmd, | 
|  | cwd=THIS_DIRECTORY, | 
|  | verbose=verbose) | 
|  | segments = info.split(':::') | 
|  | return { | 
|  | 'hash': segments[0], | 
|  | 'abbrevHash': segments[1], | 
|  | 'authorName': segments[2], | 
|  | 'authorEmail': segments[3], | 
|  | 'subject': segments[4], | 
|  | } | 
|  |  | 
|  |  | 
|  | def compose_series_payload(project_id: str, | 
|  | series_id: str, | 
|  | series_unit: str, | 
|  | series_name: Optional[str] = None, | 
|  | series_description: Optional[str] = None, | 
|  | average_range: Union[int, str] = '5%', | 
|  | average_min_count: int = 3, | 
|  | better_criterion: str = 'smaller', | 
|  | override: bool = False) -> Dict[str, Any]: | 
|  | """Composes the payload dictionary for a series.""" | 
|  | payload = { | 
|  | 'projectId': project_id, | 
|  | 'serieId': series_id, | 
|  | 'serieUnit': series_unit, | 
|  | 'serieName': series_name, | 
|  | 'analyse': { | 
|  | 'benchmark': { | 
|  | 'range': average_range, | 
|  | 'required': average_min_count, | 
|  | 'trend': better_criterion, | 
|  | } | 
|  | }, | 
|  | 'override': override, | 
|  | } | 
|  | if series_description is not None: | 
|  | payload['description'] = series_description | 
|  | return payload | 
|  |  | 
|  |  | 
|  | def compose_build_payload(project_id: str, | 
|  | project_github_commit_url: str, | 
|  | build_id: int, | 
|  | commit: str, | 
|  | override: bool = False) -> Dict[str, Any]: | 
|  | """Composes the payload dictionary for a build.""" | 
|  | commit_info = get_git_commit_info(commit) | 
|  | commit_info['url'] = f'{project_github_commit_url}/{commit_info["hash"]}' | 
|  | return { | 
|  | 'projectId': project_id, | 
|  | 'build': { | 
|  | 'buildId': build_id, | 
|  | 'infos': commit_info, | 
|  | }, | 
|  | 'override': override, | 
|  | } | 
|  |  | 
|  |  | 
|  | def compose_sample_payload(project_id: str, | 
|  | series_id: str, | 
|  | build_id: int, | 
|  | sample_unit: str, | 
|  | sample_value: int, | 
|  | override: bool = False) -> Dict[str, Any]: | 
|  | """Composes the payload dictionary for a sample.""" | 
|  | return { | 
|  | 'projectId': project_id, | 
|  | 'serieId': series_id, | 
|  | 'sampleUnit': sample_unit, | 
|  | 'sample': { | 
|  | 'buildId': build_id, | 
|  | 'value': sample_value | 
|  | }, | 
|  | 'override': override | 
|  | } | 
|  |  | 
|  |  | 
|  | def get_required_env_var(var: str) -> str: | 
|  | """Gets the value for a required environment variable.""" | 
|  | value = os.getenv(var) | 
|  | if value is None: | 
|  | raise RuntimeError(f'Missing environment variable "{var}"') | 
|  | return value | 
|  |  | 
|  |  | 
|  | def post_to_dashboard(url: str, | 
|  | payload: Dict[str, Any], | 
|  | dry_run: bool = False, | 
|  | verbose: bool = False): | 
|  | data = json.dumps(payload) | 
|  |  | 
|  | if dry_run or verbose: | 
|  | print(f'API request payload: {data}') | 
|  |  | 
|  | if dry_run: | 
|  | return | 
|  |  | 
|  | api_token = get_required_env_var('IREE_DASHBOARD_API_TOKEN') | 
|  | headers = { | 
|  | 'Content-type': 'application/json', | 
|  | 'Authorization': f'Bearer {api_token}', | 
|  | } | 
|  |  | 
|  | response = requests.post(url, data=data, headers=headers) | 
|  | code = response.status_code | 
|  | if code != 200: | 
|  | raise requests.RequestException( | 
|  | f'Failed to post to dashboard server with {code} - {response.text}') | 
|  |  | 
|  |  | 
|  | def add_new_iree_series(series_id: str, | 
|  | series_unit: str, | 
|  | series_name: str, | 
|  | series_description: Optional[str] = None, | 
|  | average_range: Optional[Union[str, int]] = None, | 
|  | override: bool = False, | 
|  | dry_run: bool = False, | 
|  | verbose: bool = False): | 
|  | """Posts a new series to the dashboard.""" | 
|  | if average_range is None: | 
|  | raise ValueError(f"no matched threshold setting for benchmark: {series_id}") | 
|  |  | 
|  | payload = compose_series_payload(IREE_PROJECT_ID, | 
|  | series_id, | 
|  | series_unit, | 
|  | series_name, | 
|  | series_description, | 
|  | average_range=average_range, | 
|  | override=override) | 
|  | post_to_dashboard(f'{IREE_DASHBOARD_URL}/apis/v2/addSerie', | 
|  | payload, | 
|  | dry_run=dry_run, | 
|  | verbose=verbose) | 
|  |  | 
|  |  | 
|  | def add_new_iree_build(build_id: int, | 
|  | commit: str, | 
|  | override: bool = False, | 
|  | dry_run: bool = False, | 
|  | verbose: bool = False): | 
|  | """Posts a new build to the dashboard.""" | 
|  | payload = compose_build_payload(IREE_PROJECT_ID, | 
|  | IREE_GITHUB_COMMIT_URL_PREFIX, build_id, | 
|  | commit, override) | 
|  | post_to_dashboard(f'{IREE_DASHBOARD_URL}/apis/addBuild', | 
|  | payload, | 
|  | dry_run=dry_run, | 
|  | verbose=verbose) | 
|  |  | 
|  |  | 
|  | def add_new_sample(series_id: str, | 
|  | build_id: int, | 
|  | sample_unit: str, | 
|  | sample_value: int, | 
|  | override: bool = False, | 
|  | dry_run: bool = False, | 
|  | verbose: bool = False): | 
|  | """Posts a new sample to the dashboard.""" | 
|  | payload = compose_sample_payload(IREE_PROJECT_ID, series_id, build_id, | 
|  | sample_unit, sample_value, override) | 
|  | post_to_dashboard(f'{IREE_DASHBOARD_URL}/apis/v2/addSample', | 
|  | payload, | 
|  | dry_run=dry_run, | 
|  | verbose=verbose) | 
|  |  | 
|  |  | 
|  | 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("--dry-run", | 
|  | action="store_true", | 
|  | help="Print the comment instead of posting to dashboard") | 
|  | parser.add_argument('--verbose', | 
|  | action='store_true', | 
|  | help='Print internal information during execution') | 
|  | args = parser.parse_args() | 
|  |  | 
|  | return args | 
|  |  | 
|  |  | 
|  | def main(args): | 
|  | benchmark_files = expand_and_check_file_paths(args.benchmark_files) | 
|  | compile_stats_files = expand_and_check_file_paths(args.compile_stats_files) | 
|  |  | 
|  | if len(benchmark_files) > 0: | 
|  | committish = benchmark_definition.BenchmarkResults.from_json_str( | 
|  | benchmark_files[0].read_text()).commit | 
|  | elif len(compile_stats_files) > 0: | 
|  | committish = benchmark_definition.CompilationResults.from_json_object( | 
|  | json.loads(compile_stats_files[0].read_text())).commit | 
|  | else: | 
|  | raise ValueError("No benchmark/compilation results.") | 
|  |  | 
|  | # Register a new build for the current commit. | 
|  | commit_hash = get_git_commit_hash(committish, verbose=args.verbose) | 
|  | commit_count = get_git_total_commit_count(commit_hash, verbose=args.verbose) | 
|  |  | 
|  | aggregate_results = benchmark_presentation.aggregate_all_benchmarks( | 
|  | benchmark_files=benchmark_files, expected_pr_commit=commit_hash) | 
|  |  | 
|  | all_compilation_metrics = benchmark_presentation.collect_all_compilation_metrics( | 
|  | compile_stats_files=compile_stats_files, expected_pr_commit=commit_hash) | 
|  |  | 
|  | # Allow override to support uploading data for the same build in | 
|  | # different batches. | 
|  | add_new_iree_build(commit_count, | 
|  | commit_hash, | 
|  | override=True, | 
|  | dry_run=args.dry_run, | 
|  | verbose=args.verbose) | 
|  |  | 
|  | # Upload benchmark results to the dashboard. | 
|  | for series_id, benchmark_latency in aggregate_results.items(): | 
|  | series_name = benchmark_latency.name | 
|  | benchmark_info = benchmark_latency.benchmark_info | 
|  | description = get_model_description(benchmark_info.model_name, | 
|  | benchmark_info.model_source) | 
|  | if description is None: | 
|  | description = "" | 
|  | description += COMMON_DESCRIPTION | 
|  |  | 
|  | threshold = next( | 
|  | (threshold for threshold in benchmark_thresholds.BENCHMARK_THRESHOLDS | 
|  | if threshold.regex.match(series_name)), None) | 
|  | average_range = (threshold.get_threshold_str() | 
|  | if threshold is not None else None) | 
|  |  | 
|  | # Override by default to allow updates to the series. | 
|  | add_new_iree_series(series_id=series_id, | 
|  | series_unit="ns", | 
|  | series_name=benchmark_latency.name, | 
|  | series_description=description, | 
|  | average_range=average_range, | 
|  | override=True, | 
|  | dry_run=args.dry_run, | 
|  | verbose=args.verbose) | 
|  | add_new_sample(series_id=series_id, | 
|  | build_id=commit_count, | 
|  | sample_unit="ns", | 
|  | sample_value=benchmark_latency.mean_time, | 
|  | dry_run=args.dry_run, | 
|  | verbose=args.verbose) | 
|  |  | 
|  | for target_id, compile_metrics in all_compilation_metrics.items(): | 
|  | description = get_model_description( | 
|  | compile_metrics.compilation_info.model_name, | 
|  | compile_metrics.compilation_info.model_source) | 
|  | if description is None: | 
|  | description = "" | 
|  | description += COMMON_DESCRIPTION | 
|  |  | 
|  | for mapper in benchmark_presentation.COMPILATION_METRICS_TO_TABLE_MAPPERS: | 
|  | sample_value, _ = mapper.get_current_and_base_value(compile_metrics) | 
|  | series_unit = mapper.get_unit() | 
|  | series_id = mapper.get_series_id(target_id) | 
|  | series_name = mapper.get_series_name(compile_metrics.name) | 
|  |  | 
|  | threshold = next( | 
|  | (threshold for threshold in mapper.get_metric_thresholds() | 
|  | if threshold.regex.match(series_name)), None) | 
|  | average_range = (threshold.get_threshold_str() | 
|  | if threshold is not None else None) | 
|  |  | 
|  | # Override by default to allow updates to the series. | 
|  | add_new_iree_series(series_id=series_id, | 
|  | series_unit=series_unit, | 
|  | series_name=series_name, | 
|  | series_description=description, | 
|  | average_range=average_range, | 
|  | override=True, | 
|  | dry_run=args.dry_run, | 
|  | verbose=args.verbose) | 
|  | add_new_sample(series_id=series_id, | 
|  | build_id=commit_count, | 
|  | sample_unit=series_unit, | 
|  | sample_value=sample_value, | 
|  | dry_run=args.dry_run, | 
|  | verbose=args.verbose) | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | main(parse_arguments()) |