Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
182 commits
Select commit Hold shift + click to select a range
ff3b0b2
[code-infra] Add tachometer benchmarking
Janpot Sep 1, 2026
da12b90
[code-infra] Add a tachometer smoke-test harness
Janpot Sep 1, 2026
8a6a438
Dedupe the lockfile after adding the tachometer dependencies
Janpot Sep 1, 2026
99b9d72
[code-infra] Bring tacho run up to base-ui-mosaic's current design
Janpot Sep 1, 2026
9d6e638
[code-infra] Match the report contract mosaic's table renderer expects
Janpot Sep 1, 2026
d0cde3e
Merge remote-tracking branch 'origin/master' into worktree-tacho-extract
Janpot Sep 1, 2026
b1a0808
[code-infra] Inherit the repository's overrides in the isolated install
Janpot Sep 1, 2026
516af6e
[code-infra] Render the tachometer table from tacho run
Janpot Sep 1, 2026
220f809
[code-infra] Report tachometer results on the pull request
Janpot Sep 2, 2026
6cef12b
[dashboard] Enable the tachometer section for base-ui-mosaic
Janpot Sep 2, 2026
15e4c33
[code-infra] Run Chrome without its sandbox as root
Janpot Sep 2, 2026
42a5b43
[code-infra] Run the tachometer smoke test in CI
Janpot Sep 2, 2026
6a6408c
[dashboard] Read each tachometer row in the variant's own direction
Janpot Sep 2, 2026
ad39187
[dashboard] Structure the tachometer page like the tables CI prints
Janpot Sep 2, 2026
825742c
[dashboard] Format the tachometer view
Janpot Sep 2, 2026
f34a71c
[code-infra] Share the tachometer helpers each file had written itself
Janpot Sep 3, 2026
455116c
[code-infra] Let tachometer cases live in folders, and filter on the …
Janpot Sep 3, 2026
5d70a0d
[code-infra] Write every tachometer output under one .tachometer folder
Janpot Sep 3, 2026
076fe56
[code-infra] Lean on the platform and the existing helpers in the tac…
Janpot Sep 3, 2026
aa0597c
[code-infra] Build a ref's benchmark pages in place, resolving throug…
Janpot Sep 8, 2026
b584810
[code-infra] Give vite preview the harness's output directory
Janpot Sep 8, 2026
dedbdbf
[code-infra] Match tachometer's own browser and measurement naming
Janpot Sep 8, 2026
e59c223
[dashboard] Label the tachometer baseline with the baseline's own ref
Janpot Sep 8, 2026
2103bbe
[code-infra] Flatten a benchmark's expand tree once, the way tachomet…
Janpot Sep 8, 2026
41726a3
[code-infra] Resolve a ref's tree through its own manifest
Janpot Sep 8, 2026
71cd328
[code-infra] Emit the case index into the build, and use vite's usual…
Janpot Sep 8, 2026
ff3007f
[code-infra] Pin a ref's tree to the versions the harness resolved
Janpot Sep 9, 2026
6647f07
Merge remote-tracking branch 'origin/master' into worktree-tacho-extract
Janpot Sep 9, 2026
23ca0ca
[code-infra] Add `tacho report` to print a saved report
Janpot Sep 9, 2026
480e486
[dashboard] Drop the cross-library aside from comparisonsAcrossRefs
Janpot Sep 9, 2026
45a735a
[benchmark] Move the tachometer tooling out of code-infra
Janpot Sep 9, 2026
e9b2a14
[code-infra] Leave code-infra untouched
Janpot Sep 9, 2026
e7861e5
Drop code-infra's tachometer devDependencies from the lockfile
Janpot Sep 9, 2026
4e85d86
[benchmark] Ship the whole package, and stop describing the report twice
Janpot Sep 9, 2026
67cd039
[benchmark] Run the harness CLI without the bin shim
Janpot Sep 9, 2026
90c3e1b
[benchmark] Import vite's types once
Janpot Sep 9, 2026
10421ba
[benchmark] Find the manifest from the published layout too
Janpot Sep 9, 2026
0540488
[benchmark] Read the manifest from one place
Janpot Sep 9, 2026
04b47a0
[benchmark] Import the manifest instead of resolving it by hand
Janpot Sep 9, 2026
3b68e88
[benchmark] Resolve and name a ref correctly
Janpot Sep 10, 2026
dff0e47
[benchmark] Set the baseline's ref through URL
Janpot Sep 10, 2026
522cae8
[benchmark] Fail on a pnpm-workspace.yaml that cannot be read
Janpot Sep 10, 2026
a18bac1
[benchmark] Mark the build's output directory, and yield the index to…
Janpot Sep 10, 2026
6a7292c
[dashboard] Store the tachometer report, not an envelope around it
Janpot Sep 10, 2026
ad6f4f3
[test] Describe how the harness page resolves its import
Janpot Sep 10, 2026
84bf0c3
[benchmark] Cover the ref resolution this branch changed
Janpot Sep 10, 2026
cc65a1e
[benchmark] Tidy up after the review fixes
Janpot Sep 10, 2026
fed52d5
[benchmark] Stop reporting transferred bytes
Janpot Sep 10, 2026
bf0296e
[benchmark] Build both sides with one command
Janpot Sep 10, 2026
e629c3a
[code-infra] Add a baseline command, and stop deriving one per tool
Janpot Sep 10, 2026
a9cb4f1
[code-infra] Keep the baseline helpers with the rest of git
Janpot Sep 10, 2026
519a0aa
[benchmark] Document the tachometer axis, and compare against the for…
Janpot Sep 10, 2026
c205334
[code-infra] Print the baseline, nothing else
Janpot Sep 10, 2026
a1ae091
[benchmark] One definition of the report, and of how it reads
Janpot Sep 10, 2026
7e6d0a0
[benchmark] Import playwright by name
Janpot Sep 10, 2026
14dec34
[benchmark] Fail when no chromedriver is reachable
Janpot Sep 10, 2026
21104b8
[benchmark] Trim the browser check's header comment
Janpot Sep 10, 2026
ab456d9
[benchmark] Make the baseline the only ref, and `expand` what picks it
Janpot Sep 10, 2026
aa12530
[core] Build packages before the static and unit checks
Janpot Sep 10, 2026
3929142
[benchmark] Fold the review's cleanups into the baseline change
Janpot Sep 10, 2026
8a6cf63
[benchmark] Act on the review: measurements, variant names, Windows p…
Janpot Sep 10, 2026
bc8b333
[code-infra] Read the base branch from a remote's HEAD, never from a …
Janpot Sep 10, 2026
0c83353
[benchmark] Finish the review's small items
Janpot Sep 10, 2026
735838d
[benchmark] Fold the tacho command group into the CLI entry
Janpot Sep 11, 2026
d8cd60d
[benchmark] Say who supplies chromedriver, and why it sits at the root
Janpot Sep 11, 2026
7ac72a6
[benchmark] Add an in-place resolve mode behind a flag
Janpot Sep 11, 2026
d6a7221
[benchmark] Resolve in place by default
Janpot Sep 11, 2026
dd17c92
[benchmark] Capture the output of commands worth nothing on success
Janpot Sep 11, 2026
59d8566
[benchmark] Let the in-place installs write a lockfile
Janpot Sep 11, 2026
db1a699
[benchmark] Keep an in-place run from pinning its own tooling
Janpot Sep 11, 2026
513ba78
[code-infra] Fetch the base branch, and refuse when there is none
Janpot Sep 14, 2026
45df370
[benchmark] Export runCase() as a single-iteration primitive
Janpot Sep 29, 2026
457fa3c
[benchmark] Prototype an interleaved engine for tacho run
Janpot Sep 25, 2026
50d8359
[benchmark] Add compare() for comparing implementations
Janpot Sep 25, 2026
ae0ac34
[benchmark] Remove epochs from the interleaved engine
Janpot Sep 25, 2026
af69c8e
[benchmark] Give each page-case variant its own context again
Janpot Sep 25, 2026
e9fe4a0
[benchmark] Run only *.bench.tsx files in the interleaved engine
Janpot Sep 29, 2026
3ccbcce
[benchmark] Replace tacho run with benchmark run and a raw-sample report
Janpot Sep 29, 2026
e465a39
[code-infra-dashboard] Read benchmark-run reports instead of tachometer
Janpot Sep 29, 2026
daaacaa
[benchmark] Document benchmark run
Janpot Sep 29, 2026
028a7f9
[benchmark] Keep tacho run, upload benchmark run as benchmark report v2
Janpot Sep 29, 2026
42ba08d
[benchmark] Simplify benchmark run
Janpot Sep 29, 2026
241cee4
[benchmark] Upload the benchmark run report to the PR preview in CI
Janpot Sep 29, 2026
b5a582c
[benchmark] Remove the benchmark report command
Janpot Sep 29, 2026
49304c0
[benchmark] Trim benchmark run options
Janpot Sep 29, 2026
97f3d1c
[benchmark] Split the page runtime into benchmark and reactBenchmark
Janpot Sep 29, 2026
95285ae
[benchmark] Compare cases directly instead of variant modules
Janpot Sep 29, 2026
31a73b9
[benchmark] Simplify the page runtime and its runner
Janpot Sep 29, 2026
18fb5f7
Merge remote-tracking branch 'origin/master' into janpot/benchmark-in…
Janpot Sep 29, 2026
e99fdb9
[benchmark] Upload the tachometer report to this pull request's preview
Janpot Sep 29, 2026
300b204
[benchmark] Sample benchmark run adaptively, like tachometer
Janpot Sep 29, 2026
381087c
[benchmark] Resolve sampling where a case registers; build dashboard …
Janpot Sep 29, 2026
5beba92
[benchmark] Default sampleSize to 50, like tachometer
Janpot Sep 29, 2026
7359674
[benchmark] Remove the tachometer runner from this pull request
Janpot Sep 29, 2026
9e0afec
[benchmark] Drop the thin wrappers from the shared core
Janpot Sep 29, 2026
8ee7a2c
[benchmark] Name tarballs scope+name, which no two packages share
Janpot Sep 29, 2026
de26455
[benchmark] Drop tarballFor from the shared core
Janpot Sep 29, 2026
b4eeb4e
[benchmark] Test the pack API through packRef and packWorkingTree
Janpot Sep 29, 2026
b1a0bcc
[code-infra] Test base-branch detection through resolveBaseline
Janpot Sep 29, 2026
c26a1fa
[benchmark] Remove a temporary checkout with rm and prune
Janpot Sep 29, 2026
abfc5bb
[benchmark] Shorten the removeCheckout comment
Janpot Sep 29, 2026
315b88d
[benchmark] Return the packed packages from packRef
Janpot Sep 29, 2026
241b5c8
[benchmark] Remove packRef's staging folder in finally
Janpot Sep 29, 2026
fad0b32
[code-infra] Simplify resolveBaseline to a fetch and a merge-base
Janpot Sep 29, 2026
8758653
[code-infra] Pick the baseline's remote by URL, via getRepositoryInfo
Janpot Sep 30, 2026
767bc69
[code-infra] Choose the canonical remote by name
Janpot Sep 30, 2026
4e4779a
[code-infra] Override the baseline's remote with --remote
Janpot Sep 30, 2026
9878c9d
[benchmark] Throw case failures from measureIteration; keep runCase p…
Janpot Sep 30, 2026
bda6508
[code-infra] Take upstream, then origin, only if it points at mui
Janpot Oct 1, 2026
53f8371
[code-infra] Rename defaultBranchOf to getDefaultBranchOf
Janpot Oct 1, 2026
e84ae17
Merge remote-tracking branch 'origin/master' into janpot/benchmark-in…
Janpot Oct 1, 2026
32726ef
[benchmark] Simplify the interleaved runner and its reports
Janpot Oct 1, 2026
4b5bf7f
[benchmark] Collect garbage before every sample; report how sampling …
Janpot Oct 1, 2026
15ce6f2
[benchmark] Run benchmark run on 3 vCPUs; warn below 3 logical CPUs
Janpot Oct 1, 2026
62e9885
Merge remote-tracking branch 'origin/master' into janpot/benchmark-in…
Janpot Oct 2, 2026
838e995
[benchmark] Sample every variant in a freshly loaded page of one shar…
Janpot Oct 6, 2026
82ea68c
[benchmark] Share one 5% false-alarm chance across a run's alarmed co…
Janpot Oct 6, 2026
4c1fb34
[benchmark] Simplify the fresh-load runner and the false-alarm correc…
Janpot Oct 6, 2026
9259b3a
[benchmark] Run the benchmarks from the root command, not `benchmark …
Janpot Oct 6, 2026
4cfbe22
[benchmark] Read tables old to new, keep the comment to changes, stop…
Janpot Oct 6, 2026
98fec27
[benchmark] Fold the CLI's one command into its entry; tidy the table…
Janpot Oct 6, 2026
a6905eb
[benchmark] Say "no change detected" rather than "unsure"
Janpot Oct 6, 2026
b87310f
[benchmark] Read the footnote's verdict names from formatChange
Janpot Oct 6, 2026
9995a22
Merge remote-tracking branch 'origin/master' into janpot/benchmark-in…
Janpot Oct 6, 2026
33271ac
[benchmark] Keep function names and emit sourcemaps in the measured p…
Janpot Oct 6, 2026
24d3951
[benchmark] Colour changes, line up tables, report how long a run took
Janpot Oct 6, 2026
119208c
[benchmark] Leave the repository as it was: lockfile, checkouts, pnpm
Janpot Oct 6, 2026
2a51450
[benchmark] Time a paint from when its recording window began
Janpot Oct 6, 2026
125cb1d
[benchmark] Assume pnpm is on PATH again
Janpot Oct 6, 2026
701283a
[benchmark] Simplify interruption recovery, alarm gating and the comm…
Janpot Oct 6, 2026
36b884c
[benchmark] Default to a 5% horizon; add sampling flags, -t and a JSO…
Janpot Oct 7, 2026
bc50876
[benchmark] Record performance traces with --profile
Janpot Oct 7, 2026
73805fb
[benchmark] Run a nightly A/A on master to track the false-alarm rate
Janpot Oct 7, 2026
c9bdb60
[benchmark] Sampling flags set defaults, as in Vitest, instead of ove…
Janpot Oct 7, 2026
e0ff841
[benchmark] Hand cases the DevTools Protocol as cdp
Janpot Oct 7, 2026
776be8f
[benchmark] Upload into a named timeline with --timeline
Janpot Oct 7, 2026
bc8081d
[code-infra-dashboard] Give each changed benchmark its own table in t…
Janpot Oct 7, 2026
65851df
[code-infra-dashboard] Let the run's tables span the page, the metric…
Janpot Oct 7, 2026
1bc5445
[code-infra-dashboard] Keep the PR comment to one line unless somethi…
Janpot Oct 8, 2026
430cf45
[benchmark] Alarm on paint as on render, with 2% and 5% bands
Janpot Oct 8, 2026
6cb5034
[benchmark] Alarm at a fixed 99% instead of Bonferroni
Janpot Oct 8, 2026
fd0c0f9
[code-infra-dashboard] Show each comparison's precision on the run page
Janpot Oct 8, 2026
90878e9
[benchmark] Decide once whether a comparison can alarm
Janpot Oct 8, 2026
3e3a425
[benchmark] Drop the warn band from version 2 judgments
Janpot Oct 8, 2026
c12e1b4
[benchmark] Tidy version 2 alarms after dropping the warn band
Janpot Oct 8, 2026
c1f2f2d
[benchmark] One 99% level, and only alarmable differences extend samp…
Janpot Oct 8, 2026
5b3253d
[benchmark] Let each benchmark set its own alarm; sample until it set…
Janpot Oct 8, 2026
153dabd
[benchmark] Default scalar alarms to a 5% band, and settle only the w…
Janpot Oct 8, 2026
c231ea9
[benchmark] Name each improved benchmark once; expect a near-0% A/A rate
Janpot Oct 8, 2026
b9c34f7
[code-infra-dashboard] Table improvements in the PR comment like regr…
Janpot Oct 13, 2026
a16175c
[benchmark] Share what counts as a regression or an improvement
Janpot Oct 13, 2026
c10b352
[code-infra-dashboard] Show only "timed out" in the warning colour
Janpot Oct 13, 2026
a8889d2
[benchmark] Share one page load per round between a file's benchmarks
Janpot Oct 13, 2026
621b803
Merge remote-tracking branch 'origin/master' into janpot/benchmark-in…
Janpot Oct 13, 2026
feb4328
[benchmark] Don't warn about 'use client' directives when building th…
Janpot Oct 13, 2026
115961d
[benchmark] Print each file's runtime, and its benchmarks' rounds as …
Janpot Oct 13, 2026
f172e98
[benchmark] Simplify the per-file runner
Janpot Oct 13, 2026
a898de2
[benchmark] Say "timed out" in the progress line, as the summary and …
Janpot Oct 13, 2026
565949e
[benchmark] Count the sample size in the time limit, and stop gracefu…
Janpot Oct 13, 2026
201c1e8
[benchmark] Trim the abort checks to the ones that matter
Janpot Oct 13, 2026
84cecce
Merge branch 'master' of https://github.com/mui/mui-public into janpo…
Janpot Oct 14, 2026
edbf53c
[code-infra-dashboard] Comment benchmark results on base-ui-plus pull…
Janpot Oct 14, 2026
1c1cb8c
[benchmark] Upload a report with its own command
Janpot Oct 14, 2026
70c0524
[benchmark] Run the benchmark CLI as pnpm test:benchmark
Janpot Oct 14, 2026
8672388
[benchmark] Drop the pinch and tap gestures
Janpot Oct 14, 2026
f55753b
[benchmark] Drop input; gestures go through cdp
Janpot Oct 15, 2026
56af0f3
[benchmark] Validate sampling options without resolving them
Janpot Oct 15, 2026
0405b76
[benchmark] Check sampling options once, where they come in
Janpot Oct 15, 2026
b2ea5df
[benchmark] Require @playwright/test as a peer
Janpot Oct 15, 2026
d84a6af
[benchmark] Let Playwright find and report its own browser
Janpot Oct 15, 2026
aca68d7
[benchmark] Launch Playwright's own Chromium without browser.ts
Janpot Oct 15, 2026
e6d1e1e
[benchmark] Upload in one function
Janpot Oct 15, 2026
4a7f65e
[benchmark] Serve the builds with sirv
Janpot Oct 15, 2026
59670d8
[benchmark] Build the version 2 upload from the shared CI envelope
Janpot Oct 15, 2026
3a0c1ca
[benchmark] Tidy the run analysis
Janpot Oct 15, 2026
d5b3a2d
[benchmark] Simplify the server lifecycle and the run analysis
Janpot Oct 15, 2026
a0053e3
[benchmark] Upload this repository's benchmark report to production
Janpot Oct 20, 2026
e432565
Merge remote-tracking branch 'origin/master' into janpot/benchmark-in…
Janpot Oct 20, 2026
dbaf2af
[benchmark] Remove the Vitest benchmark suite
Janpot Oct 20, 2026
d74fcc8
[benchmark] Use the US spelling of color
Janpot Oct 20, 2026
76f0998
[benchmark] Don't call a fixed sample size a timeout
Janpot Oct 20, 2026
c51e84e
[benchmark] Sample this repository's benchmarks for 10 rounds
Janpot Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 75 additions & 5 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ jobs:
# make sure the netlify ignore command has the correct dependencies
name: '`pnpm update-netlify-ignore` changes committed?'
command: pnpm code-infra netlify-ignore ./apps/code-infra-dashboard --check
- run:
# A package that publishes from `build/` is linked there, not at its source, so a consumer
# of one sees no types at all until it is built.
name: Build packages
command: pnpm release:build
- run:
name: Tests TypeScript
command: pnpm typescript
Expand All @@ -75,6 +80,11 @@ jobs:
steps:
- checkout
- code-infra/install-deps
- run:
# A package that publishes from `build/` is linked there, not at its source, so an import
# from one fails to resolve until it is built.
name: Build packages
command: pnpm release:build
- run:
name: 'Run tests'
command: pnpm test
Expand All @@ -92,18 +102,71 @@ jobs:
<<: *default-job
executor:
name: code-infra/mui-node-browser
playwright-img-version: 'v1.64.0-noble'
playwright-img-version: 'v1.62.1-noble'
# 2 vCPUs starve V8's background compilers, which makes the same code's speed vary from sample to
# sample; see the benchmark package's README.
resource_class: medium+
steps:
- checkout
- code-infra/install-deps
- run:
name: Build packages
command: pnpm release:build
- run:
name: Run benchmarks
environment:
BENCHMARK_UPLOAD: 'true'
command: pnpm -F ./test/performance benchmark
name: Resolve the baseline commit
command: |
set -e
BASE_SHA=$(pnpm code-infra baseline --base-branch master)
echo "Baseline commit: $BASE_SHA"
echo "$BASE_SHA" > .benchmark-baseline-sha
- restore_cache:
name: Restore the packed-baseline cache
keys:
- benchmark-run-cache-v1-{{ checksum ".benchmark-baseline-sha" }}
- run:
name: Run the benchmark smoke test
no_output_timeout: 30m
command: pnpm test:benchmark --baseline "$(cat .benchmark-baseline-sha)"
- run:
name: Upload the benchmark report
# Also after a failed run that wrote its report, such as one where every benchmark errored,
# so the comment says what happened.
when: always
command: pnpm test:benchmark upload
- save_cache:
name: Save the packed-baseline cache
key: benchmark-run-cache-v1-{{ checksum ".benchmark-baseline-sha" }}
paths:
- test/benchmark/.benchmark/packed
# The commit against itself: any regression it reports is a false alarm, so over many nights the
# job's failure rate is the benchmarks' false-alarm rate. An alarm needs a change confidently past
# its error band, so expect close to 0%; a rate that climbs means noise or bias in a benchmark.
benchmark_aa:
<<: *default-job
executor:
name: code-infra/mui-node-browser
playwright-img-version: 'v1.62.1-noble'
resource_class: medium+
steps:
- checkout
- code-infra/install-deps
- run:
name: Build packages
command: pnpm release:build
- run:
name: Benchmark the commit against itself
no_output_timeout: 30m
command: pnpm --silent -F ./test/benchmark benchmark --baseline HEAD --reporter json > aa.json
- store_artifacts:
path: aa.json
- run:
name: Fail on a false alarm
command: |
node -e "
const { regressions } = JSON.parse(require('fs').readFileSync('aa.json', 'utf8'));
for (const { benchmark, metric } of regressions) console.error('False alarm: ' + benchmark + ' · ' + metric);
process.exit(regressions.length > 0 ? 1 : 0);
"

test_browser:
<<: *default-job
Expand Down Expand Up @@ -148,3 +211,10 @@ workflows:
<<: *default-context
- test_browser:
<<: *default-context
# Started nightly on master by a scheduled pipeline that sets `workflow` to `benchmark-aa`.
benchmark-aa:
when:
equal: [benchmark-aa, << pipeline.parameters.workflow >>]
jobs:
- benchmark_aa:
<<: *default-context
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,10 @@ next-env.d.ts
# Claude Code worktrees
.claude/worktrees/
.claude/cache/

# Everything the benchmark harness writes: built pages per ref, packed workspace tarballs, the
# isolated installs, the run reports, and the pages generated per benchmark file. Both directories
# ignore themselves from the inside too, but are listed here because ESLint is fed from the root
# .gitignore and never reads nested ones.
/test/benchmark/.benchmark/
/test/benchmark/src/__bench__/
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Applies to the whole repository.

- **CRITICAL**: When running pnpm commands for workspace packages, always use the `-F` flag followed by the package name.
- **Example**: `pnpm -F @mui/internal-bundle-size-checker add micromatch`
- Private packages without a `name` field in `package.json` must be filtered by their relative path (for example, `pnpm -F ./test/performance add <dependency>`).
- Private packages without a `name` field in `package.json` must be filtered by their relative path (for example, `pnpm -F ./test/benchmark add <dependency>`).
- **Do NOT use `cd` to navigate into package directories** for workspace operations.
- **Do NOT manually edit package.json files to add/remove dependencies** - always use `pnpm -F <workspace> add <dependency>` or `pnpm -F <workspace> remove <dependency>` to keep the order deterministic.
- **ALWAYS run `pnpm dedupe`** after installing a dependency.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as React from 'react';
import type { Metadata } from 'next';
import BenchmarkRunDetails from '@/views/BenchmarkRunDetails';

export async function generateMetadata({
params,
}: {
params: Promise<{ owner: string; repo: string }>;
}): Promise<Metadata> {
const { owner, repo } = await params;
return { title: `Benchmark run - ${owner}/${repo}` };
}

export default function BenchmarkRunPage() {
return <BenchmarkRunDetails />;
}
223 changes: 223 additions & 0 deletions apps/code-infra-dashboard/src/components/BenchmarkRunHistory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import * as React from 'react';
import NextLink from 'next/link';
import Autocomplete from '@mui/material/Autocomplete';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Link from '@mui/material/Link';
import Paper from '@mui/material/Paper';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { BarChartPro } from '@mui/x-charts-pro/BarChartPro';
import { analyzeRun, formatChange, formatComparison } from '@mui/internal-benchmark/runReport';
import type {
BenchmarkRunReport,
Change,
MetricComparison,
} from '@mui/internal-benchmark/runReport';
import { isBenchmarkRunUpload } from '@/utils/fetchCiReport';
import { useMasterCommits } from '../hooks/useMasterCommits';
import { useCiReports } from '../hooks/useCiReports';
import ErrorDisplay from './ErrorDisplay';

/**
* Master history of the `benchmark` CLI reports. Every master commit is benchmarked against its parent
* in the same run, so each bar is that commit's own paired change — which commit moved a number is
* read off the chart directly, rather than inferred from a noisy trend.
*/

// An `unchanged` comparison sits at zero, so it has no bar to color.
const CHANGE_COLORS: Record<Exclude<Change, 'unchanged'>, string> = {
worse: 'var(--mui-palette-error-main)',
better: 'var(--mui-palette-success-main)',
undetected: 'var(--mui-palette-grey-500)',
};

/** Per benchmark, per metric: a commit's change against its parent. */
type CommitComparisons = Map<string, Map<string, MetricComparison>>;

interface CommitPoint {
sha: string;
date: Date;
comparisons: CommitComparisons;
}

// A report is analysed once, however often the list of loaded reports changes around it. Keyed by
// the report object, which the query cache keeps stable for as long as it holds the report.
const comparisonsByReport = new WeakMap<BenchmarkRunReport, CommitComparisons>();

function comparisonsOf(report: BenchmarkRunReport): CommitComparisons {
let comparisons = comparisonsByReport.get(report);
if (!comparisons) {
comparisons = new Map();
for (const { benchmark, metrics } of analyzeRun(report)) {
if (benchmark.kind === 'baseline') {
comparisons.set(
benchmark.name,
new Map(metrics.map(({ metric, comparisons: [comparison] }) => [metric, comparison])),
);
}
}
comparisonsByReport.set(report, comparisons);
}
return comparisons;
}

interface BenchmarkRunHistoryProps {
repo: string;
}

export default function BenchmarkRunHistory({ repo }: BenchmarkRunHistoryProps) {
const { commits, isLoading, isFetchingNextPage, hasNextPage, error, fetchNextPage } =
useMasterCommits(repo);
const { reports, isLoading: reportsLoading } = useCiReports(repo, commits, 'benchmark.json');

const points = React.useMemo(
() =>
commits.flatMap(({ timestamp, commit }): CommitPoint[] => {
const upload = reports[commit.sha];
// Version 1 reports are `DailyBenchmarkChart`'s.
if (!upload || !isBenchmarkRunUpload(upload)) {
return [];
}
return [
{ sha: commit.sha, date: new Date(timestamp), comparisons: comparisonsOf(upload.report) },
];
}),
[commits, reports],
);

const benchmarkNames = React.useMemo(
() => [...new Set(points.flatMap((point) => [...point.comparisons.keys()]))].sort(),
[points],
);
const [userBenchmark, setUserBenchmark] = React.useState<string | null>(null);
const benchmark = userBenchmark ?? benchmarkNames[0] ?? null;

const metricNames = React.useMemo(
() =>
benchmark === null
? []
: [
...new Set(
points.flatMap((point) => [...(point.comparisons.get(benchmark)?.keys() ?? [])]),
),
],
[points, benchmark],
);
const [userMetric, setUserMetric] = React.useState<string | null>(null);
const metric =
[userMetric, 'render', metricNames[0]].find(
(name): name is string => name != null && metricNames.includes(name),
) ?? null;

// One series per kind of change, so each bar takes its change's color. A commit's bar is the
// midpoint of its confidence interval; the tooltip gives the interval itself.
const series = React.useMemo(() => {
const selected = points.map((point) =>
benchmark === null || metric === null
? undefined
: point.comparisons.get(benchmark)?.get(metric),
);
return (['worse', 'better', 'undetected'] as const satisfies Change[]).map((change) => ({
type: 'bar' as const,
stack: 'change',
label: formatChange(change),
color: CHANGE_COLORS[change],
data: selected.map((comparison) =>
comparison?.change === change
? (comparison.relative.low + comparison.relative.high) / 2
: null,
),
valueFormatter: (_value: number | null, { dataIndex }: { dataIndex: number }) => {
const comparison = selected[dataIndex];
return comparison?.change === change ? formatComparison(comparison) : null;
},
}));
}, [points, benchmark, metric]);

const [selectedSha, setSelectedSha] = React.useState<string | null>(null);

// A repository still on version 1 reports has nothing to draw here.
if (!error && !isLoading && !reportsLoading && points.length === 0) {
return null;
}

return (
<Paper elevation={2} sx={{ p: 3, mt: 3 }}>
<Typography variant="h6" component="h2" gutterBottom>
Benchmark runs
</Typography>

{error ? (
<ErrorDisplay title="Error loading benchmark history" error={error} />
) : (
<React.Fragment>
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
Each master commit against its parent, measured in the same run: the midpoint of the 99%
confidence interval on the paired change. Click a bar to open that run.
</Typography>

<Box sx={{ display: 'flex', gap: 2, mb: 2, flexWrap: 'wrap' }}>
<Autocomplete
options={benchmarkNames}
value={benchmark}
onChange={(_event, value) => setUserBenchmark(value)}
size="small"
sx={{ minWidth: 320 }}
renderInput={(inputParams) => <TextField {...inputParams} label="Benchmark" />}
/>
<Autocomplete
options={metricNames}
value={metric}
onChange={(_event, value) => setUserMetric(value)}
size="small"
sx={{ minWidth: 200 }}
renderInput={(inputParams) => <TextField {...inputParams} label="Metric" />}
/>
</Box>

<BarChartPro
xAxis={[
{
data: points.map((point) => point.date),
scaleType: 'band',
valueFormatter: (date: Date, context: { location: string }) =>
context.location === 'tick'
? date.toLocaleDateString()
: `${date.toLocaleString()} (${
points.find((point) => point.date === date)?.sha.slice(0, 7) ?? ''
})`,
},
]}
yAxis={[{ width: 60, valueFormatter: (value: number) => `${value.toFixed(1)}%` }]}
series={series}
onAxisClick={(_event, data) => {
if (data) {
setSelectedSha(points[data.dataIndex]?.sha ?? null);
}
}}
loading={isLoading || reportsLoading}
height={300}
skipAnimation
grid={{ horizontal: true }}
/>

<Box sx={{ mt: 2, display: 'flex', justifyContent: 'space-between', gap: 2 }}>
<Typography variant="body2">
{selectedSha ? (
<Link component={NextLink} href={`/benchmark-run/${repo}?sha=${selectedSha}`}>
Open the run for {selectedSha.slice(0, 7)}
</Link>
) : null}
</Typography>
{hasNextPage && (
<Button size="small" onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
{isFetchingNextPage ? 'Loading…' : 'Load older commits'}
</Button>
)}
</Box>
</React.Fragment>
)}
</Paper>
);
}
Loading
Loading