Skip to content

Commit dff4f7a

Browse files
committed
First pass at Web UI
1 parent eeccea6 commit dff4f7a

17 files changed

+998
-54
lines changed

Diff for: bench_runner/__main__.py

+9-11
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,3 @@
1-
import importlib
2-
import sys
3-
4-
5-
import rich
6-
7-
81
COMMANDS = {
92
"backfill": "Schedule benchmarking a number of commits",
103
"compare": "Compare a matrix of specific results",
@@ -23,15 +16,20 @@
2316
}
2417

2518
if __name__ == "__main__":
19+
import importlib
20+
import sys
21+
22+
from .util import rich_print
23+
2624
command = len(sys.argv) >= 2 and sys.argv[1] or ""
2725

2826
if command not in COMMANDS:
2927
command_length = max(len(k) for k in COMMANDS.keys())
30-
rich.print(f"Unknown command '{command}'.", file=sys.stderr)
31-
rich.print(file=sys.stderr)
32-
rich.print("Valid commands are:", file=sys.stderr)
28+
rich_print(f"Unknown command '{command}'.", file=sys.stderr)
29+
rich_print(file=sys.stderr)
30+
rich_print("Valid commands are:", file=sys.stderr)
3331
for k, v in COMMANDS.items():
34-
rich.print(f" [blue]{k:{command_length}}[/blue]: {v}")
32+
rich_print(f" [blue]{k:{command_length}}[/blue]: {v}")
3533
sys.exit(1)
3634

3735
sys.argv = [sys.argv[0], *sys.argv[2:]]

Diff for: bench_runner/config.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,17 @@
1515
def get_bench_runner_config(
1616
filepath: Path | str = Path("bench_runner.toml"),
1717
):
18-
with Path(filepath).open("rb") as fd:
19-
return tomllib.load(fd)
18+
filepath = Path(filepath)
19+
if filepath.is_file():
20+
with filepath.open("rb") as fd:
21+
return tomllib.load(fd)
22+
else:
23+
try:
24+
from pyodide.http import pyfetch # type: ignore[import]
25+
except ImportError:
26+
raise RuntimeError("bench_runner.toml not found")
27+
toml_content = pyfetch(str(filepath)).string()
28+
if toml_content:
29+
return tomllib.loads(toml_content)
30+
else:
31+
raise RuntimeError("bench_runner.toml not found")

Diff for: bench_runner/git.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@
77
import subprocess
88

99

10-
import rich
11-
12-
1310
from .util import PathLike
11+
from .util import rich_print
1412

1513

1614
def get_log(
@@ -98,7 +96,7 @@ def get_git_merge_base(dirname: PathLike) -> str | None:
9896
encoding="utf-8",
9997
).strip()
10098
except subprocess.CalledProcessError:
101-
rich.print("[red]Failed to get merge base[/red]")
99+
rich_print("[red]Failed to get merge base[/red]")
102100
return None
103101

104102
if merge_base == commit_hash:

Diff for: bench_runner/plot.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@
88
from pathlib import Path
99
import re
1010
import tempfile
11-
from typing import Callable, Iterable, Sequence
11+
from typing import Callable, Iterable, Sequence, TextIO
1212

1313

1414
from matplotlib import pyplot as plt
1515
import matplotlib
1616
import numpy as np
17-
import rich_argparse
18-
from scour import scour
17+
18+
try:
19+
from scour import scour
20+
except ImportError:
21+
scour = None
1922

2023

2124
matplotlib.use("agg")
@@ -49,7 +52,7 @@
4952
}
5053

5154

52-
def savefig(output_filename: PathLike, **kwargs):
55+
def savefig(output: PathLike | TextIO, **kwargs):
5356
class Options:
5457
quiet = True
5558
remove_descriptive_elements = True
@@ -59,19 +62,19 @@ class Options:
5962
shorten_ids = True
6063
digits = 3
6164

62-
output_filename = Path(output_filename)
63-
64-
plt.savefig(output_filename, **kwargs)
65+
plt.savefig(output, format="svg", **kwargs)
6566
plt.close("all")
6667

67-
if output_filename.suffix == ".svg":
68-
with tempfile.NamedTemporaryFile(
69-
dir=output_filename.parent, delete=False
70-
) as tmp:
71-
with open(output_filename) as fd:
72-
scour.start(Options(), fd, tmp)
73-
output_filename.unlink()
74-
Path(tmp.name).rename(output_filename)
68+
if isinstance(output, (str, Path)):
69+
output = Path(output)
70+
if output.suffix == ".svg" and scour:
71+
with tempfile.NamedTemporaryFile(
72+
dir=output_filename.parent, delete=False
73+
) as tmp:
74+
with open(output) as fd:
75+
scour.start(Options(), fd, tmp)
76+
output.unlink()
77+
Path(tmp.name).rename(output)
7578

7679

7780
@functools.cache
@@ -478,6 +481,8 @@ def get_comparison_value(ref, r):
478481

479482

480483
if __name__ == "__main__":
484+
import rich_argparse
485+
481486
parser = argparse.ArgumentParser(
482487
"Compare two benchmark .json files",
483488
formatter_class=rich_argparse.ArgumentDefaultsRichHelpFormatter,

Diff for: bench_runner/result.py

+32-8
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717

1818

1919
import numpy as np
20-
from packaging import version
21-
import pyperf
22-
import rich.progress
2320

2421

2522
from . import bases as mbases
@@ -30,7 +27,7 @@
3027
from . import plot
3128
from . import runners
3229
from . import util
33-
from .util import PathLike
30+
from .util import PathLike, rich_track
3431

3532

3633
CombinedData = list[tuple[str, np.ndarray | None, float]]
@@ -190,14 +187,18 @@ def write_table(self, filename: PathLike) -> str | None:
190187
def _get_combined_data(
191188
self, ref_data: dict[str, np.ndarray], head_data: dict[str, np.ndarray]
192189
) -> CombinedData:
190+
import pyperf
191+
193192
def remove_outliers(values, m=2):
194193
return values[
195194
abs(values - np.mean(values)) < np.multiply(m, np.std(values))
196195
]
197196

198197
def calculate_diffs(ref_values, head_values) -> tuple[np.ndarray | None, float]:
199198
if len(ref_values) > 3 and len(head_values) > 3:
200-
sig, t_score = pyperf._utils.is_significant(ref_values, head_values)
199+
sig, t_score = pyperf._utils.is_significant( # pyright: ignore
200+
ref_values, head_values
201+
)
201202
if not sig:
202203
return None, 0.0
203204
else:
@@ -461,6 +462,7 @@ def __init__(
461462
self._commit_datetime = commit_datetime
462463
self._filename = None
463464
self.bases = {}
465+
self._contents: None | dict[str, Any] = None
464466

465467
@classmethod
466468
def from_filename(cls, filename: PathLike) -> "Result":
@@ -496,6 +498,22 @@ def from_filename(cls, filename: PathLike) -> "Result":
496498
obj._filename = filename
497499
return obj
498500

501+
@classmethod
502+
def from_online_json(cls, url: str, json_content: str | dict) -> "Result":
503+
from urllib.parse import urlparse
504+
505+
parsed_url = urlparse(url)
506+
path = "/".join(parsed_url.path.split("/")[-2:])
507+
508+
result = cls.from_filename(Path(unquote(path)))
509+
result._filename = None
510+
if isinstance(json_content, str):
511+
result._contents = json.loads(json_content)
512+
else:
513+
result._contents = json_content
514+
515+
return result
516+
499517
@classmethod
500518
def from_arbitrary_filename(cls, filename: PathLike) -> "Result":
501519
filename = Path(filename)
@@ -600,10 +618,14 @@ def result_info(self) -> tuple[str | None, str | None, str | None]:
600618
f"Unknown result type (extra={self.extra} suffix={self.suffix})"
601619
)
602620

603-
@functools.cached_property
621+
@property
604622
def contents(self) -> dict[str, Any]:
623+
if self._contents is not None:
624+
return self._contents
605625
with self.filename.open("rb") as fd:
606-
return json.load(fd)
626+
self._contents = json.load(fd)
627+
assert self._contents is not None
628+
return self._contents
607629

608630
@property
609631
def metadata(self) -> dict[str, Any]:
@@ -705,6 +727,8 @@ def get_timing_data(self) -> dict[str, np.ndarray]:
705727
return data
706728

707729
def get_memory_data(self) -> dict[str, np.ndarray]:
730+
from packaging import version
731+
708732
data = {}
709733
excluded = util.get_excluded_benchmarks()
710734

@@ -800,7 +824,7 @@ def find_match(result, candidates, base, func):
800824
bases = []
801825

802826
if progress:
803-
track = rich.progress.track # type: ignore
827+
track = rich_track # type: ignore
804828
else:
805829

806830
def track(it, *_args, **_kwargs):

Diff for: bench_runner/scripts/generate_results.py

+49
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from collections import defaultdict
66
import datetime
77
import io
8+
import json
89
from pathlib import Path
910
import sys
1011
from typing import Iterable, TextIO, Sequence
@@ -17,6 +18,7 @@
1718

1819

1920
from bench_runner.bases import get_bases
21+
from bench_runner import config as mconfig
2022
from bench_runner import flags as mflags
2123
from bench_runner import plot
2224
from bench_runner.result import (
@@ -371,6 +373,51 @@ def filter_broken_memory_results(results):
371373
return [r for r in results if r.nickname != "darwin"]
372374

373375

376+
def generate_webui_index(bases: Iterable[str], results: Iterable[Result]) -> None:
377+
"""
378+
Generate a machine readable index of all the available results.
379+
"""
380+
381+
def make_path(result):
382+
return "/".join([result.filename.parent.name, result.filename.name])
383+
384+
row_data = []
385+
index = {}
386+
base_results = []
387+
for i, result in enumerate(results):
388+
row_data.append(
389+
[
390+
result.commit_date,
391+
result.cpython_hash,
392+
unquote(result.fork),
393+
unquote(result.ref),
394+
result.version,
395+
result.runner,
396+
",".join(result.flags),
397+
]
398+
)
399+
index[make_path(result)] = i
400+
401+
for result in results:
402+
if "base" in result.bases:
403+
base = result.bases["base"].ref
404+
base_results.append(index[make_path(base)])
405+
else:
406+
base_results.append(None)
407+
408+
content = {
409+
"rowData": row_data,
410+
"_index": list(index.keys()),
411+
"_base_results": base_results,
412+
"_bases": bases,
413+
}
414+
415+
with Path("pages/index.json").open("w") as fd:
416+
# By using indent=0, git can diff the file much better than if
417+
# it were on a single line
418+
json.dump(content, fd, indent=0)
419+
420+
374421
def _main(repo_dir: PathLike, force: bool = False, bases: Sequence[str] | None = None):
375422
repo_dir = Path(repo_dir)
376423
results_dir = repo_dir / "results"
@@ -385,6 +432,8 @@ def _main(repo_dir: PathLike, force: bool = False, bases: Sequence[str] | None =
385432
benchmarking_results = [r for r in results if r.result_info[0] == "raw results"]
386433
generate_indices(bases, results, benchmarking_results, repo_dir)
387434
generate_directory_indices(benchmarking_results)
435+
if mconfig.get_bench_runner_config().get("webui", False):
436+
generate_webui_index(bases, benchmarking_results)
388437

389438
memory_benchmarking_results = filter_broken_memory_results(benchmarking_results)
390439

0 commit comments

Comments
 (0)