Repository navigation
GPUI frame benchmarks: interleaved A/B per platform, scene-equivalence gate, Cap editor A/B - #3886
Conversation
… and Linux Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ng the toolchain Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fault 1.95.0) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The frame benchmarks register through a small runner instead of #[gpui::bench] so that, with GPUI_SCENE_DIGEST set, every scene a benchmark submits is hashed before reaching the real headless renderer. scripts/gpui-scene-digest.py compares those digests between base and head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Sandchest reviewed Sandchest reviews every change for vulnerabilities an attacker could use · 2 credits · Review details |
| paths: | ||
| - "apps/desktop-gpui/patches/**" | ||
| - "scripts/gpui-app-bench.py" | ||
| - ".github/workflows/gpui-app-bench.yml" |
There was a problem hiding this comment.
P2: Untrusted fork PRs can repeatedly queue multi-hour GPUI benchmark jobs by editing benchmark-only paths
New PR workflows trigger 120-150 min macOS/Windows/Linux benchmark jobs when only the workflow, its scripts, or gpui-benches crate change.
Gate self-file/benchmark-triggered jobs to internal PRs or a maintainer-approved label/environment; keep fork runs bounded.
AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.
<file name=".github/workflows/gpui-app-bench.yml">
<violation number="1" location=".github/workflows/gpui-app-bench.yml:15">
<priority>P2</priority>
<title>Untrusted fork PRs can repeatedly queue multi-hour GPUI benchmark jobs by editing benchmark-only paths</title>
<evidence>The new `pull_request` trigger lists the workflow itself and `scripts/gpui-app-bench.py` as paths (lines 11-15), so a fork PR that changes nothing but this workflow or its runner script starts the `editor` job on `blacksmith-6vcpu-macos-15` with `timeout-minutes: 150` (lines 36-39). That job runs `node scripts/setup.js`, prepares head and base GPUI checkouts, builds `cap-gpui` twice (lines 107-112), and then runs the real editor timeline benchmark for `--rounds 5` interleaved A/B rounds over the reference recording (lines 126-139). The new `.github/workflows/gpui-bench.yml` has the same shape: paths at lines 12-17 include `apps/desktop-gpui/gpui-benches/**`, `scripts/gpui-bench-*`, and the workflow file itself, and each matching PR push queues three `bench` jobs (150-minute timeout) plus three `test` jobs (120-minute timeout) across macOS, Windows, and Linux. Neither workflow carries a `github.event.pull_request.head.repo.full_name == github.repository` condition, a required label, or an `environment:` approval gate; `concurrency` with `cancel-in-progress: true` only cancels overlapping runs and does not stop sequential pushes from each consuming a full run. The 150-minute and 120-minute timeouts are the worst-case per-run cost, and each run performs two full Cargo builds plus the interleaved measurement workload, so a fork contributor can repeatedly trigger expensive shared-runner work with a trivial edit to benchmark-only files.</evidence>
<recommendation>Restrict the self-file/benchmark-only triggers to internal pull requests, e.g. add `if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository` to the `bench`/`test`/`editor` jobs (or drop the workflow/script/crate paths from `pull_request.paths` and run them only from `workflow_dispatch` / `push` on trusted branches). For fork PRs, keep the benchmark suite reachable only through a maintainer-applied label or an `environment:` approval, and keep a short bounded validation run for workflow-only changes.</recommendation>
</violation>
</file>
There was a problem hiding this comment.
Fixed in e3a8c7f: the bench and test jobs in gpui-bench.yml and the editor job in gpui-app-bench.yml now run only when github.event.pull_request.head.repo.full_name == github.repository (or on push/workflow_dispatch). Fork PRs no longer start them; maintainers can still measure a fork's patch with workflow_dispatch.
There was a problem hiding this comment.
Correctly fixed: gating the long-running bench, test, and editor jobs to internal PRs via github.event.pull_request.head.repo.full_name == github.repository (and push/workflow_dispatch) prevents untrusted forks from automatically queuing expensive benchmark runs, while preserving maintainer flexibility. Dismissing.
| if not base or not head: | ||
| print("No scenes were digested (this platform has no headless renderer); skipping.") | ||
| return | ||
| compared = sorted(set(base) & set(head)) | ||
| differing = [key for key in compared if base[key] != head[key]] |
There was a problem hiding this comment.
Scene equivalence succeeds when scene coverage is missing
Medium Severity
On macOS, the patched current_headless_renderer() returns None when Metal initialization fails, so a benchmark can exit successfully without producing any digests. If only head encounters that failure, this branch skips the gate despite base having rendered scenes. Partial loss is also ignored because only intersecting frame keys are compared: head can stop submitting frames after an unchanged prefix and still report identical scenes. Restrict skipping to explicitly unsupported platforms and require expected benchmark/frame coverage before comparing hashes.
There was a problem hiding this comment.
Fixed in e3a8c7f. An empty digest on macOS now fails instead of skipping (skipping only happens when neither side digests anything on a non-macOS runner). Every benchmark must draw at least 16 frames on both sides, and every (benchmark, frame) position both builds reached must be present and equal, so a head that stops submitting a workload fails. Positions beyond what both reached aren't compared, because a faster build legitimately draws more frames in Criterion's fixed time.
| if not base or not head: | ||
| print("No scenes were digested (this platform has no headless renderer); skipping.") | ||
| return | ||
| compared = sorted(set(base) & set(head)) | ||
| differing = [key for key in compared if base[key] != head[key]] |
There was a problem hiding this comment.
If the head stops submitting a workload or submits fewer frames, the comparison drops those positions because it checks only shared keys. If either side submits nothing, it skips entirely. A patch that stops drawing can therefore pass the scene check. Require matching benchmark/frame keys, and allow empty results only on platforms explicitly known to lack a renderer.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/gpui-scene-digest.py
Line: 70-74
Comment:
**Missing frames pass the gate**
If the head stops submitting a workload or submits fewer frames, the comparison drops those positions because it checks only shared keys. If either side submits nothing, it skips entirely. A patch that stops drawing can therefore pass the scene check. Require matching benchmark/frame keys, and allow empty results only on platforms explicitly known to lack a renderer.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Same fix as above (e3a8c7f): a missing benchmark or one with fewer than 16 frames on either side fails, and every frame position both builds reached must be present and identical.
| cd "$zed_dir" | ||
| messages="$out_dir/cargo-messages.json" | ||
| cargo "+${GPUI_BENCH_TOOLCHAIN:-1.95.0}" bench -p gpui_cap_benches --bench frame --no-run --message-format json > "$messages" |
There was a problem hiding this comment.
The helper creates out_dir from the caller's directory, then changes to zed_dir before opening cargo-messages.json and copying the executable. Calling scripts/gpui-bench-build.sh ../zed-cap ./bench-out therefore writes to a different location or fails before Cargo runs. Resolve out_dir to an absolute path before changing directories.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/gpui-bench-build.sh
Line: 36-38
Comment:
**Relative output paths break**
The helper creates `out_dir` from the caller's directory, then changes to `zed_dir` before opening `cargo-messages.json` and copying the executable. Calling `scripts/gpui-bench-build.sh ../zed-cap ./bench-out` therefore writes to a different location or fails before Cargo runs. Resolve `out_dir` to an absolute path before changing directories.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.There was a problem hiding this comment.
Fixed in e9fad53: out_dir is resolved to an absolute path right after it's created, before the script changes directory.
| /// cover the warm-up frames every run draws, whatever Criterion's timing. | ||
| const DIGEST_FRAMES: usize = 64; | ||
|
|
||
| /// Wraps the platform's headless renderer and hashes each scene it submits. |
There was a problem hiding this comment.
This comment restates what SceneDigestRenderer does. The comments above cards and primitives do the same, and the pattern appears elsewhere. AGENTS.md requires comments to explain non-obvious context rather than narrate the code. Remove these descriptions while keeping comments that explain choices such as the measured build-path effects. This repository requirement must be satisfied before merging.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/gpui-benches/benches/frame.rs
Line: 585
Comment:
**Comments repeat the code**
This comment restates what `SceneDigestRenderer` does. The comments above `cards` and `primitives` do the same, and the pattern appears elsewhere. `AGENTS.md` requires comments to explain non-obvious context rather than narrate the code. Remove these descriptions while keeping comments that explain choices such as the measured build-path effects. This repository requirement must be satisfied before merging.
**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
…the benchmark runners - The digest now includes a hash of the pixels uploaded into every atlas tile a sprite shows, so a rasterization change fails the gate even when tile sizes and ids stay the same. - Each benchmark must draw at least 16 frames on both sides and every frame both builds reached must match; an empty run on macOS fails instead of skipping. Builds may still draw different numbers of frames in Criterion's fixed time. - The benchmark and test jobs only run for pull requests from this repository; forks can still be measured with workflow_dispatch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Benchmarks for Cap's GPUI patch stack, run per platform with base and head interleaved on the same machine, plus a gate that fails if a patch changes what gets drawn.
Frame benchmarks (
apps/desktop-gpui/gpui-benches)Window::drawwith the platform text system; on macOS each frame is also submitted to Metal through the headless renderer:cards_{64,256,1024}: bordered, shadowed cards with two labels each.text_rows_static_{50,200}andtext_rows_changing_{50,200}: label rows, the second with a timestamp that changes every frame.paragraphs_{12,48}: wrapped text in columns.recordings_scroll: a 10 000-rowuniform_listscrolling.hover_cards: pointer moves across 256 hoverable cards.cached_panels: 40.cached()views, one of which changes per frame.primitives_{1024,8192}: raw quads, shadows and paths.scripts/gpui-bench-build.shcopies it into the prepared zed checkout, so it builds against the exact patched gpui and Zed's lockfile.runinbenches/frame.rs) rather than#[gpui::bench], so the headless renderer can be wrapped.Measuring (
scripts/gpui-bench-compare.py)Window::drawalone, from GPUI's own frame report. On macOS, light frames are bound by the headless GPU handing back instance buffers, so wall time hides CPU work.Scene equivalence (
scripts/gpui-scene-digest.py)GPUI_SCENE_DIGEST=<file>, the runner hashes every scene a benchmark submits before passing it to the real renderer. The hash covers all primitives in the order the renderer receives them, plus the pixels uploaded into every atlas tile a sprite shows.Real app (
.github/workflows/gpui-app-bench.yml,scripts/gpui-app-bench.py)cap-gpuiagainst base and head, then runsCAP_GPUI_AUTO_TIMELINE_BENCH=1(src/editor_bench.rs) onCapSoftware/cap-performance-fixtures' reference recording. Every run gets a fresh profile and a fresh copy of the project.Profiling (
.github/workflows/gpui-profile.yml)gpui-profile/**, runs each benchmark under callgrind on Linux and uploads the annotated self and inclusive costs.Who can trigger it
workflow_dispatch, and pull requests from this repository. Fork pull requests don't start them.Tests
gpui-bench.ymlalso runscargo test -p gpui --lib --testsand the platform crate's tests (gpui_macos,gpui_windows, orgpui_wgpuon Vulkan) against the head stack on all three platforms.🤖 Generated with Claude Code