#!/usr/bin/env python """ Script for downloading Engine benchmark results into a single static web page that visualizes all the benchmarks. Without any options, downloads and visualizes benchmark data for the last 14 days. It downloads the data synchronously and uses a cache directory by default. It is advised to use `-v|--verbose` option all the time. It queries only successful benchmark runs. If there are no successful benchmarks in a given period, no results will be written. The process of the script is roughly as follows: - Gather all the benchmark results from GH API into job reports (JobReport dataclass) - Use cache if possible to avoid unnecessary GH API queries - Transform the gathered results into data for a particular benchmark sorted by an appropriate commit timestamp. - BenchmarkData class If you wish to inspect the data yourself, just use --create-csv option. Dependencies for the script: - GH CLI utility - https://cli.github.com/ - Used for convenience to do the GH API queries. - It needs to be installed, and you should also authenticate. - Python version >= 3.7 - Python 3rd party packages: - pandas - Used for convenience for a very simple data processing - jinja2 - Used as a template engine for the HTML. """ import json import logging import logging.config import os import re import shutil import subprocess import sys import tempfile import zipfile from argparse import ArgumentParser, RawDescriptionHelpFormatter from csv import DictWriter from datetime import datetime, timedelta from os import path from typing import List, Dict, Optional, Any, Union from dataclasses import dataclass if not (sys.version_info.major >= 3 and sys.version_info.minor >= 7): print("ERROR: python version lower than 3.7") exit(1) try: import pandas as pd import numpy as np import jinja2 except ModuleNotFoundError as err: print("ERROR: One of pandas, numpy, or jinja2 packages not installed") exit(1) BENCH_RUN_NAME = "Benchmark Engine" DATE_FORMAT = "%Y-%m-%d" # Workflod ID of engine benchmarks, got via `gh api '/repos/enso-org/enso/actions/workflows'` BENCH_WORKFLOW_ID = 29450898 GH_DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" """ Date format as returned from responses in GH API""" ENSO_COMMIT_BASE_URL = "https://github.com/enso-org/enso/commit/" JINJA_TEMPLATE = "template_jinja.html" """ Path to the Jinja HTML template """ @dataclass class Author: name: str @dataclass class Commit: """ Corresponds to the commit from GH API """ id: str author: Author timestamp: str message: str @dataclass class JobRun: """ Gathered via the GH API. Defines a single run of an Engine benchmark job. """ id: str display_title: str html_url: str run_attempt: int """ An event as defined by the GitHub API, for example 'push' or 'schedule' """ event: str head_commit: Commit @dataclass class JobReport: """ Gathered via the GH API - a report that is pushed as an aritfact to the job. Contains a XML file with scores for all the benchmarks. """ label_score_dict: Dict[str, float] """ A mapping of benchmark labels to their scores """ bench_run: JobRun @dataclass class BenchmarkData: """ Data for a single benchmark compiled from all the job reports. """ @dataclass class Entry: score: float commit_id: str commit_msg: str commit_url: str commit_author: str commit_timestamp: datetime bench_run_url: str bench_run_event: str label: str """ Label for the benchmark, as reported by org.enso.interpreter.bench.BenchmarksRunner """ entries: List[Entry] """ Entries sorted by timestamps """ @dataclass class ChartRow: timestamp: datetime score: float tooltip: str @dataclass class TemplateBenchData: id: str rows: List[ChartRow] score_diffs: List[str] """ S string that is displayed in the selection info """ commit_ids: List[str] commit_msgs: List[str] commit_authors: List[str] commit_urls: List[str] bench_run_urls: List[str] """ URLs to Engine benchmark job """ @dataclass class JinjaData: bench_datas: List[TemplateBenchData] since: datetime until: datetime def _parse_bench_run_from_json(obj: Dict[Any, Any]) -> JobRun: return JobRun( id=str(obj["id"]), html_url=obj["html_url"], run_attempt=int(obj["run_attempt"]), event=obj["event"], display_title=obj["display_title"], head_commit=Commit( id=obj["head_commit"]["id"], message=obj["head_commit"]["message"], timestamp=obj["head_commit"]["timestamp"], author=Author( name=obj["head_commit"]["author"]["name"] ) ) ) def _parse_bench_report_from_json(obj: Dict[Any, Any]) -> JobReport: return JobReport( bench_run=_parse_bench_run_from_json(obj["bench_run"]), label_score_dict=obj["label_score_dict"] ) def _bench_report_to_json(bench_report: JobReport) -> Dict[Any, Any]: return { "bench_run": { "id": bench_report.bench_run.id, "html_url": bench_report.bench_run.html_url, "run_attempt": bench_report.bench_run.run_attempt, "event": bench_report.bench_run.event, "display_title": bench_report.bench_run.display_title, "head_commit": { "id": bench_report.bench_run.head_commit.id, "message": bench_report.bench_run.head_commit.message, "timestamp": bench_report.bench_run.head_commit.timestamp, "author": { "name": bench_report.bench_run.head_commit.author.name } } }, "label_score_dict": bench_report.label_score_dict } def _parse_bench_report_from_xml(bench_report_xml: str, bench_run: JobRun) -> "JobReport": logging.debug(f"Parsing BenchReport from {bench_report_xml}") with open(bench_report_xml, "r") as f: lines = f.readlines() label_pattern = re.compile("