Skip to content

Commit eb8e2ee

Browse files
authored
ci: generate baseline benchmark projects with the baseline release generator (#921)
* ci: generate release benchmark projects with each version's own generator The release performance comparison generated its benchmark projects with the candidate's generator and compiled them with the previous release, so any generator syntax newer than that release (such as FUNCTION descriptions) failed the baseline before timing. The baseline now runs projects written by its own generator: the release tag's source is extracted with git archive (or taken from --baseline-source) and its write_pristine_projects runs with this interpreter, with cwd and PYTHONPATH set to that source, through the checkout generator runner shared with the same-runner compile ratio job. The candidate keeps its own generator. Generation failures stop the comparison with the generator's stderr tail, and the report and evidence record the baseline's generator. * test: make the release-performance self-comparison E2E independent of runner noise The E2E compares one build with itself using a single run, so asserting a passed verdict depended on wall-time noise (a 1.31x wall ratio at 0.99x CPU failed CI). It now asserts the report structure, every command row, the per-side generator markers and the recorded evidence, and accepts either verdict as long as the exit code matches it. The production 1.25x limits are unchanged; the verdict unit tests gain fixed-timing cases for a wall-only regression at CPU parity and a wall-only slowdown just under the limit. Fixes CHI-481
1 parent 9fae2ec commit eb8e2ee

16 files changed

Lines changed: 386 additions & 17 deletions

File tree

‎scripts/compile_performance_ratio/_helpers/measure.py‎

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,18 +55,37 @@ def write_benchmark_project(*, kind: str, project_dir: Path, models: int) -> Non
5555
)
5656

5757

58+
def run_checkout_generator(
59+
*,
60+
checkout: Path,
61+
python: Path,
62+
entry: str,
63+
arguments: tuple[str, ...],
64+
environment: dict[str, str],
65+
) -> subprocess.CompletedProcess[str]:
66+
"""Run generator code from another checkout's source tree with this `python`."""
67+
68+
return subprocess.run(
69+
[str(python), "-c", entry, *arguments],
70+
cwd=checkout,
71+
capture_output=True,
72+
text=True,
73+
env={**environment, PYTHONPATH_KEY: str(checkout)},
74+
check=False,
75+
)
76+
77+
5878
def write_base_benchmark_project(
5979
*, base_root: Path, python: Path, kind: str, project_dir: Path, models: int
6080
) -> None:
6181
"""Write the base project with the base checkout's own generator, run by `python`."""
6282

63-
completed: subprocess.CompletedProcess[str] = subprocess.run(
64-
[str(python), "-c", BASE_GENERATOR_ENTRY, kind, str(project_dir), str(models)],
65-
cwd=base_root,
66-
capture_output=True,
67-
text=True,
68-
env={**_compile_environment(), PYTHONPATH_KEY: str(base_root)},
69-
check=False,
83+
completed: subprocess.CompletedProcess[str] = run_checkout_generator(
84+
checkout=base_root,
85+
python=python,
86+
entry=BASE_GENERATOR_ENTRY,
87+
arguments=(kind, str(project_dir), str(models)),
88+
environment=_compile_environment(),
7089
)
7190
if completed.returncode != 0:
7291
raise CompileComparisonError(

‎scripts/release_performance/_helpers/benchmark.py‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
from pathlib import Path
1212
from typing import cast
1313

14+
from scripts.compile_performance_ratio._helpers.measure import run_checkout_generator
1415
from scripts.release_performance.constants import (
16+
BASELINE_GENERATOR_ENTRY,
1517
BUILD_PROJECT,
1618
BUILD_WARMUPS,
1719
COMMAND_TIMEOUT_SECONDS,
@@ -20,6 +22,7 @@
2022
EXCLUDED_ENVIRONMENT_PREFIX,
2123
INSPECTION_PROJECT,
2224
INSPECTION_WARMUPS,
25+
PRISTINE_DIRECTORY,
2326
STDERR_TAIL_CHARACTERS,
2427
TIME_FORMAT,
2528
)
@@ -42,14 +45,34 @@
4245
def write_pristine_projects(*, root: Path, inspection_models: int, build_models: int) -> Path:
4346
"""Generate the inspection and build benchmarks once so every version reads the same files."""
4447

45-
pristine: Path = root / "pristine"
48+
pristine: Path = root / PRISTINE_DIRECTORY
4649
write_inspection_benchmark_project(
4750
project_dir=pristine / INSPECTION_PROJECT, model_count=inspection_models
4851
)
4952
write_build_benchmark_project(project_dir=pristine / BUILD_PROJECT, model_count=build_models)
5053
return pristine
5154

5255

56+
def write_baseline_pristine_projects(
57+
*, source: Path, root: Path, inspection_models: int, build_models: int
58+
) -> Path:
59+
"""Generate the baseline's benchmarks with the baseline source's own generator."""
60+
61+
completed: subprocess.CompletedProcess[str] = run_checkout_generator(
62+
checkout=source,
63+
python=Path(sys.executable).absolute(),
64+
entry=BASELINE_GENERATOR_ENTRY,
65+
arguments=(str(root), str(inspection_models), str(build_models)),
66+
environment=_environment(),
67+
)
68+
if completed.returncode != 0:
69+
raise ReleasePerformanceError(
70+
f"Baseline benchmark generation with the generator in {source} failed with exit "
71+
f"{completed.returncode}:\n{completed.stderr[-STDERR_TAIL_CHARACTERS:]}"
72+
)
73+
return root / PRISTINE_DIRECTORY
74+
75+
5376
def prepare_version_projects(
5477
*, version: InstalledVersion, pristine: Path, root: Path
5578
) -> dict[str, Path]:

‎scripts/release_performance/_helpers/report.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ def comparison_markdown(*, comparison: ReleaseComparison) -> str:
3131
f"{comparison.runs} interleaved runs per version; wall and CPU are medians, "
3232
"peak RSS is the worst run.",
3333
"",
34+
"Benchmark projects are generated per side: the baseline runs projects from its own "
35+
f"generator ({comparison.baseline_generator}), the candidate from the candidate's.",
36+
"",
3437
f"Limits: wall {_percent(MAX_WALL_RATIO)} (ignored under "
3538
f"{MIN_WALL_REGRESSION_SECONDS:g} s), CPU {_percent(MAX_CPU_RATIO)} (ignored under "
3639
f"{MIN_CPU_REGRESSION_SECONDS:g} s), peak RSS {_percent(MAX_RSS_RATIO)} (ignored under "

‎scripts/release_performance/_helpers/versions.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
from __future__ import annotations
44

55
import hashlib
6+
import io
67
import json
78
import platform
89
import re
910
import shutil
1011
import subprocess
1112
import sys
13+
import tarfile
1214
import time
1315
import urllib.request
1416
from pathlib import Path
@@ -22,6 +24,7 @@
2224
PYPI_SIMPLE_JSON,
2325
PYPI_SIMPLE_URL,
2426
RELEASE_TAG_PATTERN,
27+
RELEASE_TAG_PREFIX,
2528
STDERR_TAIL_CHARACTERS,
2629
UNIVERSAL_WHEEL_MACHINE,
2730
WHEEL_MACHINE_ALIASES,
@@ -118,6 +121,28 @@ def tagged_versions(*, repo_dir: Path) -> tuple[str, ...]:
118121
)
119122

120123

124+
def release_source(*, version: str, repo_dir: Path, destination: Path) -> Path:
125+
"""Extract the source tree of release tag v<version> from repo_dir into destination."""
126+
127+
tag: str = RELEASE_TAG_PREFIX + version
128+
completed: subprocess.CompletedProcess[bytes] = subprocess.run(
129+
["git", "-C", str(repo_dir), "archive", "--format=tar", tag],
130+
capture_output=True,
131+
check=False,
132+
)
133+
if completed.returncode != 0:
134+
raise ReleasePerformanceError(
135+
f"Release tag {tag} is not available in {repo_dir}, so the baseline's own benchmark "
136+
"generator cannot be used; fetch the release tags or pass --baseline-source: "
137+
+ _tail(completed.stderr.decode("utf-8", errors="replace"))
138+
)
139+
shutil.rmtree(destination, ignore_errors=True)
140+
destination.mkdir(parents=True)
141+
with tarfile.open(fileobj=io.BytesIO(completed.stdout)) as archive:
142+
archive.extractall(destination, filter="data")
143+
return destination
144+
145+
121146
def resolve_baseline(*, candidate: str, repo_dir: Path) -> str:
122147
"""Return the highest published or not-yet-published tagged release below the candidate."""
123148

‎scripts/release_performance/_helpers/workflow.py‎

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from scripts.release_performance._helpers.benchmark import (
1414
compare_command,
1515
prepare_version_projects,
16+
write_baseline_pristine_projects,
1617
write_pristine_projects,
1718
)
1819
from scripts.release_performance._helpers.report import (
@@ -30,13 +31,17 @@
3031
install_published,
3132
install_wheel,
3233
installed_version,
34+
release_source,
3335
resolve_baseline,
3436
)
3537
from scripts.release_performance.constants import (
38+
BASELINE_GENERATED_DIRECTORY,
3639
BASELINE_LABEL,
3740
BASELINE_PUBLICATION_WAIT_SECONDS,
41+
BASELINE_SOURCE_DIRECTORY,
3842
BENCHMARK_COMMANDS,
3943
CANDIDATE_LABEL,
44+
RELEASE_TAG_PREFIX,
4045
REPO_ROOT,
4146
)
4247
from scripts.release_performance.exceptions import ReleasePerformanceError
@@ -104,16 +109,40 @@ def _compare(*, options: ComparisonOptions, root: Path) -> ReleaseComparison:
104109
file=sys.stderr,
105110
)
106111
load_before: tuple[float, float, float] = os.getloadavg()
107-
print("Generating benchmark projects", file=sys.stderr)
108-
pristine: Path = write_pristine_projects(
109-
root=root, inspection_models=options.inspection_models, build_models=options.build_models
112+
print("Generating candidate benchmark projects", file=sys.stderr)
113+
pristine: dict[str, Path] = {
114+
CANDIDATE_LABEL: write_pristine_projects(
115+
root=root,
116+
inspection_models=options.inspection_models,
117+
build_models=options.build_models,
118+
)
119+
}
120+
baseline_generator: str = (
121+
str(options.baseline_source)
122+
if options.baseline_source is not None
123+
else RELEASE_TAG_PREFIX + baseline.version
124+
)
125+
print(f"Generating baseline benchmark projects with {baseline_generator}", file=sys.stderr)
126+
pristine[BASELINE_LABEL] = write_baseline_pristine_projects(
127+
source=(
128+
options.baseline_source.absolute()
129+
if options.baseline_source is not None
130+
else release_source(
131+
version=baseline.version,
132+
repo_dir=REPO_ROOT,
133+
destination=root / BASELINE_SOURCE_DIRECTORY,
134+
)
135+
),
136+
root=root / BASELINE_GENERATED_DIRECTORY,
137+
inspection_models=options.inspection_models,
138+
build_models=options.build_models,
110139
)
111140
versions: tuple[InstalledVersion, InstalledVersion] = (baseline, candidate)
112141
projects: dict[str, dict[str, Path]] = {}
113142
for version in versions:
114143
print(f"Warming caches for {version.label} {version.version}", file=sys.stderr)
115144
projects[version.label] = prepare_version_projects(
116-
version=version, pristine=pristine, root=root
145+
version=version, pristine=pristine[version.label], root=root
117146
)
118147
measured: list[CommandComparison] = []
119148
skipped: list[SkippedCommand] = []
@@ -133,6 +162,7 @@ def _compare(*, options: ComparisonOptions, root: Path) -> ReleaseComparison:
133162
return ReleaseComparison(
134163
baseline_version=baseline.version,
135164
candidate_version=candidate.version,
165+
baseline_generator=baseline_generator,
136166
runs=options.runs,
137167
runner=RunnerContext(
138168
cpu_model=cpu_model(),

‎scripts/release_performance/constants.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@
4242
TIME_FORMAT: str = "%e %M %U %S"
4343
COMPILE_COMMAND: str = "compile"
4444
STDERR_TAIL_CHARACTERS: int = 2_000
45+
PRISTINE_DIRECTORY: str = "pristine"
46+
BASELINE_GENERATED_DIRECTORY: str = "baseline-generated"
47+
BASELINE_SOURCE_DIRECTORY: str = "baseline-source"
48+
RELEASE_TAG_PREFIX: str = "v"
49+
BASELINE_GENERATOR_ENTRY: str = (
50+
"import sys; from pathlib import Path; "
51+
"from scripts.release_performance._helpers.benchmark import write_pristine_projects; "
52+
"write_pristine_projects("
53+
"root=Path(sys.argv[1]), inspection_models=int(sys.argv[2]), build_models=int(sys.argv[3]))"
54+
)
4555

4656
BENCHMARK_COMMANDS: tuple[BenchmarkCommand, ...] = (
4757
BenchmarkCommand(

‎scripts/release_performance/main/compare.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ def _parse_args(argv: list[str] | None) -> ComparisonOptions:
3939
parser.add_argument("--python", default=DEFAULT_PYTHON)
4040
parser.add_argument("--inspection-models", type=int, default=DEFAULT_INSPECTION_MODELS)
4141
parser.add_argument("--build-models", type=int, default=DEFAULT_BUILD_MODELS)
42+
parser.add_argument(
43+
"--baseline-source",
44+
type=Path,
45+
default=None,
46+
help=(
47+
"Source tree whose benchmark generator writes the baseline's projects. Defaults to "
48+
"the baseline's release tag extracted from this repository."
49+
),
50+
)
4251
parser.add_argument("--work-dir", type=Path, default=None)
4352
parser.add_argument("--output", type=Path, default=None)
4453
args: argparse.Namespace = parser.parse_args(argv)
@@ -53,4 +62,5 @@ def _parse_args(argv: list[str] | None) -> ComparisonOptions:
5362
build_models=args.build_models,
5463
work_dir=args.work_dir,
5564
output=args.output,
65+
baseline_source=args.baseline_source,
5666
)

‎scripts/release_performance/models.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ class RunnerContext:
6868
class ReleaseComparison:
6969
baseline_version: str
7070
candidate_version: str
71+
baseline_generator: str
7172
runs: int
7273
runner: RunnerContext
7374
commands: tuple[CommandComparison, ...]
@@ -84,3 +85,4 @@ class ComparisonOptions:
8485
build_models: int
8586
work_dir: Path | None
8687
output: Path | None
88+
baseline_source: Path | None = None

‎tests/e2e/scripts/release_performance/_test_types.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ class ReleasePerformanceTestCase:
1010
description: str
1111
inspection_models: int
1212
build_models: int
13-
expected_return_code: int
13+
expected_outcomes: tuple[tuple[int, bool], ...]
1414
expected_fragments: tuple[str, ...]
15+
expected_marked_projects: tuple[str, ...]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Fixtures for release performance comparisons with per-side benchmark generators."""
2+
3+
from __future__ import annotations
4+
5+
import shutil
6+
from pathlib import Path
7+
8+
BASELINE_MARKER: str = "BASELINE_MARKER"
9+
_MARKING_WRAPPER: str = """
10+
11+
_unmarked_write_pristine_projects = write_pristine_projects
12+
13+
14+
def write_pristine_projects(*, root: Path, inspection_models: int, build_models: int) -> Path:
15+
pristine: Path = _unmarked_write_pristine_projects(
16+
root=root, inspection_models=inspection_models, build_models=build_models
17+
)
18+
for project in pristine.iterdir():
19+
(project / "BASELINE_MARKER").write_text("baseline", encoding="utf-8")
20+
return pristine
21+
"""
22+
23+
24+
def write_marked_baseline_source(*, repo_root: Path, destination: Path) -> Path:
25+
"""Copy this checkout's generators and mark every project the copy generates."""
26+
27+
for name in ("scripts", "tests"):
28+
_ = shutil.copytree(
29+
repo_root / name, destination / name, ignore=shutil.ignore_patterns("__pycache__")
30+
)
31+
benchmark: Path = destination / "scripts" / "release_performance" / "_helpers" / "benchmark.py"
32+
_ = benchmark.write_text(
33+
benchmark.read_text(encoding="utf-8") + _MARKING_WRAPPER, encoding="utf-8"
34+
)
35+
return destination

0 commit comments

Comments
 (0)