Files
gstack/benchmark/SKILL.md.tmpl
Garry Tanandcapy-ai[bot] 043feff8b3 v1.91.309.0 fix(flaky): one rule each in the judged review and Eng prose; a cancelled judge names what it was waiting on (GSTA-139 S3) (#3544)
* fix(flaky): give the judged review and Eng prose one rule each, and name what a cancelled judge received (GSTA-139 S3)

- review: Step 5.8 records an unresolved non-gate defect as open, and a repeat
  pass's reruns are revalidation that run after smoke expiry in the same
  PROBE_DIR with materialize once on the final pass (every sample of #3527's
  red panel 40617200857 and its rerun named one of these). Nearby prose trimmed
  so union, skeleton and host pins do not grow.
- plan-eng-review: drop the Eng-only em-dash Voice variant so the shared Voice
  hoists; the Scope gate's Format precedence names em dashes (5 of 6 samples of
  40634787316 named the near-copy Voice). Host size pins lowered.
- llm-judge: a cancelled judge request prints llm-judge-aborted with timing,
  429 waits and stream event counts (no content), so the next workflow-judge
  deadline (train 35 gate 40660626349) names its cause.
- benchmark: the Phase 6 example no longer lazy-loads the hero image.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(review): earlier passes link checkpoints; only the final pass materializes (all 3 samples of the focused judge run at 1bdc4ce)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs(changelog): Unreleased entry for the GSTA-139 S3 judge prose repairs

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* test: record llm-judge abort diagnostic as non-behavioral; abort tests wait on events, not sleeps

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix: pre-landing review fixes

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* docs: name the llm-judge-aborted line in TESTING_INTERNALS

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-29 05:50:41 +00:00

258 lines
13 KiB
Cheetah
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: benchmark
preamble-tier: 1
version: 1.0.0
description: |
Web performance regression detection. Establishes
baselines for page load times, Core Web Vitals, and resource sizes.
Compares before/after on every PR. Tracks performance trends over time.
Use when: "web performance benchmark", "page speed", "lighthouse", "web vitals",
"bundle size", "load time". Not for AI model, agent eval or business
benchmarks; use /benchmark-models for cross-model skill comparisons. (gstack)
voice-triggers:
- "speed test"
- "check performance"
triggers:
- web performance benchmark
- check page speed
- detect page performance regression
allowed-tools:
- Bash
- Read
- Write
- Glob
- AskUserQuestion
---
{{PREAMBLE}}
{{ASIDE_SETUP}}
{{BROWSE_FALLBACK}}
# /benchmark — Performance Regression Detection
You are a **Performance Engineer**. Performance dies by a thousand paper cuts. Each PR adds 50ms here, 20KB there, until the app takes 8 seconds to load and nobody knows when it got slow.
You measure, baseline, compare and alert, reading `performance.getEntries()` from the live page in the Aside browser: real numbers, not estimates.
## User-invocable
When the user types `/benchmark`, run this skill.
## Arguments
- `/benchmark <url>` — full performance audit with baseline comparison
- `/benchmark <url> --baseline` — capture baseline (run before making changes)
- `/benchmark <url> --quick` — one-run ballpark, no baseline
- `/benchmark <url> --pages /,/dashboard,/api/health` — specify pages
- `/benchmark --diff` — benchmark only pages affected by current branch
- `/benchmark --trend` — show performance trends from historical data
---
{{SECTION_INDEX:benchmark}}
---
## Instructions
### Phase 1: Setup
```bash
SLUG=$(~/.claude/skills/gstack/bin/gstack-slug --get SLUG 2>/dev/null) || SLUG=unknown
mkdir -p .gstack/benchmark-reports
mkdir -p .gstack/benchmark-reports/baselines
git update-index --refresh >/dev/null 2>&1 || true
```
Run the refresh before timing anything that spawns git (a dev server's version stamp,
a build step, `--diff` discovery): in a benchmark or bisect worktree a stale index makes
the first git call pay a full refresh inside the timed region.
### Phase 2: Page Discovery
Use `--pages` when given. Otherwise open the URL, collect its same-origin `a[href]`
links (skip logout/delete-style URLs), and propose the homepage plus up to 5 main
navigation targets; confirm the list with the user before measuring.
If `--diff` mode:
```bash
git diff $(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || echo main)...HEAD --name-only
```
### Phase 3: Performance Data Collection
For each page, ONE `aside repl` script opens the page and prints every metric as a labelled line. Tabs die when the script ends, so nothing carries over between pages — each page gets its own run:
```bash
aside repl '
const pg = await openTab("<page-url>");
await pg.waitForLoadState("load");
console.log("NAV=" + await pg.evaluate(() => JSON.stringify(performance.getEntriesByType("navigation")[0]))); // stringify IN the page: PerformanceEntry fields are getters and serialize to {} across the bridge
console.log("PAINT=" + await pg.evaluate(() => JSON.stringify(performance.getEntriesByType("paint").map(p => ({ name: p.name, start: Math.round(p.startTime) })))));
console.log("LCP=" + await pg.evaluate(() => new Promise(res => { const po = new PerformanceObserver(l => { const e = l.getEntries().pop(); if (e) res(Math.round(e.startTime)); }); po.observe({ type: "largest-contentful-paint", buffered: true }); setTimeout(() => res(null), 3000); })));
console.log("RESOURCES=" + JSON.stringify(await pg.evaluate(() => performance.getEntriesByType("resource").map(r => ({ name: r.name.split("/").pop().split("?")[0], type: r.initiatorType, size: r.transferSize, duration: Math.round(r.duration) })).sort((a, b) => b.duration - a.duration).slice(0, 15))));
console.log("SCRIPTS=" + JSON.stringify(await pg.evaluate(() => performance.getEntriesByType("resource").filter(r => r.initiatorType === "script").map(r => ({ name: r.name.split("/").pop().split("?")[0], size: r.transferSize })))));
console.log("CSS=" + JSON.stringify(await pg.evaluate(() => performance.getEntriesByType("resource").filter(r => r.initiatorType === "css").map(r => ({ name: r.name.split("/").pop().split("?")[0], size: r.transferSize })))));
console.log("SUMMARY=" + JSON.stringify(await pg.evaluate(() => { const r = performance.getEntriesByType("resource"); return { total_requests: r.length, total_transfer: r.reduce((s, e) => s + (e.transferSize || 0), 0), by_type: Object.entries(r.reduce((a, e) => { a[e.initiatorType] = (a[e.initiatorType] || 0) + 1; return a; }, {})).sort((a, b) => b[1] - a[1]) }; })));
await closeTab(pg); console.log("GSTACK_STEP_OK");
'
```
`NAV=` is the navigation timing entry, `PAINT=` the paint entries (FCP lives here), `LCP=` the largest-contentful-paint start time, `RESOURCES=` the 15 slowest resources, `SCRIPTS=` / `CSS=` the bundle inventory, `SUMMARY=` request count, total transfer, and requests by type. A missing `GSTACK_STEP_OK` or a line starting with `[error` means the page did not load — record it as a failure, not a slow page.
Extract key metrics from the labelled lines (`NAV=` unless stated otherwise):
- **TTFB** (Time to First Byte): `responseStart - requestStart`
- **FCP** (First Contentful Paint): the `first-contentful-paint` entry in `PAINT=`
- **LCP** (Largest Contentful Paint): the `LCP=` line (`null` if the page emitted no LCP entry — record it as missing, not 0)
- **DOM Interactive**: `domInteractive - startTime`
- **DOM Complete**: `domComplete - startTime`
- **Full Load**: `loadEventEnd - startTime`
Run the script 5 times per page; later phases use each metric's median and report the run count and range. `--quick` is the one-run ballpark, with absolute numbers and budgets and no comparison verdict.
### Phase 4: Baseline Capture (--baseline mode)
Save metrics to baseline file:
```json
{
"url": "<url>",
"timestamp": "<ISO>",
"branch": "<branch>",
"runs": 5,
"pages": {
"/": {
"ttfb_ms": 120,
"fcp_ms": 450,
"lcp_ms": 800,
"dom_interactive_ms": 600,
"dom_complete_ms": 1200,
"full_load_ms": 1400,
"total_requests": 42,
"total_transfer_bytes": 1250000,
"js_bundle_bytes": 450000,
"css_bundle_bytes": 85000,
"range_ms": {"lcp": [760, 840], "full_load": [1350, 1480]},
"largest_resources": [
{"name": "main.js", "size": 320000, "duration": 180},
{"name": "vendor.js", "size": 130000, "duration": 90}
]
}
}
}
```
Each `_ms` value is the median of `runs` runs; `range_ms` holds each timing metric's min and max (two shown). Write to `.gstack/benchmark-reports/baselines/baseline.json`.
Also retain an immutable `{UTC-timestamp}-baseline.json` beside it for trends. Without `--baseline`, never overwrite it; Phase 9 saves the run.
### Phase 5: Comparison
{{SECTION:vet-the-number}}
If baseline exists, compare current metrics against it. The baseline ran earlier, so question 5's alternation is unavailable; instead compare each delta with both sides' `range_ms` and name the limiter from the Phase 6 resources. A baseline without `runs` counts as one run; one run on either side, or an unnamed limiter, makes the verdict inconclusive (say which) and recommends a fresh `--baseline`.
Without a baseline, report absolute numbers and budgets only, mark the comparison unavailable and recommend `--baseline`. Missing metrics remain N/A. A zero baseline makes percentage change N/A; absolute timing thresholds still apply.
```
PERFORMANCE REPORT — [url]
══════════════════════════
VERDICT: slower — LCP 800ms → 1600ms at the median, outside both ranges, bound by the hero image decode
Branch: [current-branch] vs baseline ([baseline-branch])
Runs: 5 per side (medians; ranges in the JSON report)
Page: /
Metric Baseline Current Delta Status
TTFB 120ms 135ms +15ms OK
FCP 450ms 480ms +30ms OK
LCP 800ms 1600ms +800ms REGRESSION
DOM Interactive 600ms 650ms +50ms OK
DOM Complete 1200ms 1350ms +150ms OK
Full Load 1400ms 2100ms +700ms REGRESSION
Total Requests 42 58 +16 WARNING
Transfer Size 1.2MB 1.8MB +0.6MB REGRESSION
JS Bundle 450KB 720KB +270KB REGRESSION
CSS Bundle 85KB 88KB +3KB OK
REGRESSIONS DETECTED: 4
[1] LCP 800ms → 1600ms — a large new image or blocking resource
[2] Transfer 1.2MB → 1.8MB — new JS bundles
[3] JS 450KB → 720KB — new dependency or missing tree-shaking
[4] Full load 1400ms → 2100ms — see Phase 6
```
**Regression thresholds:**
- Timing metrics: >50% increase OR >500ms absolute increase = REGRESSION
- Timing metrics: >20% increase = WARNING
- Bundle size and total transfer: >25% increase = REGRESSION
- Bundle size and total transfer: >10% increase = WARNING
- Request count: >30% increase = WARNING (no separate regression threshold)
Apply REGRESSION before WARNING; otherwise OK. A delta smaller than either side's spread (`range_ms` max − min) is OK (no measurable difference) whatever threshold it crosses. Negative deltas are improvements.
### Phase 6: Slowest Resources
```
TOP 10 SLOWEST RESOURCES
═════════════════════════
# Resource Type Size Duration
1 vendor.chunk.js script 320KB 480ms
2 main.js script 250KB 320ms
3 hero-image.webp img 180KB 280ms
4 analytics.js script 45KB 250ms ← third-party
5 fonts/inter-var.woff2 font 95KB 180ms
...
RECOMMENDATIONS:
- vendor.chunk.js: code-split — 320KB on initial load
- analytics.js: load async/defer — blocks rendering for 250ms
- hero-image.webp: add width/height (CLS)
```
Third-party = any host but the page's origin. Each recommendation names a resource here or in a Phase 5 regression, the metric it moves and one remedy (split, defer, compress, lazy-load, cache).
### Phase 7: Performance Budget
Check against industry budgets:
For each available metric, FAIL at or above the budget, WARNING from 90% to below 100%, otherwise PASS. Missing metrics are N/A and excluded. Grade by the proportion below budget (PASS or WARNING): A = all, B = at least two-thirds, C = at least half, D = fewer than half, N/A = none measured.
```
PERFORMANCE BUDGET CHECK
════════════════════════
Metric Budget Actual Status
FCP < 1.8s 0.48s PASS
LCP < 2.5s 1.6s PASS
Total JS < 500KB 720KB FAIL
Total CSS < 100KB 88KB PASS
Total Transfer < 2MB 1.8MB WARNING (90%)
HTTP Requests < 50 58 FAIL
Grade: B (4/6 below budget: 3 PASS, 1 WARNING, 2 FAIL)
```
### Phase 8: Trend Analysis (--trend mode)
Load every `*-benchmark.json` and `baselines/*-baseline.json` under `.gstack/benchmark-reports/`, order by `timestamp`, show the last 5 per page: FCP and LCP medians, Bundle = `js_bundle_bytes`, Requests = `total_requests`, Grade = the file's `grade` (`-` for baseline files).
```
PERFORMANCE TRENDS (last 5 benchmarks)
══════════════════════════════════════
Date FCP LCP Bundle Requests Grade
2026-03-10 420ms 750ms 380KB 38 A
2026-03-12 440ms 780ms 410KB 40 A
2026-03-14 450ms 800ms 450KB 42 A
2026-03-16 460ms 850ms 520KB 48 B
2026-03-18 480ms 1600ms 720KB 58 B
TREND: degrading. LCP doubled in 8 days; JS bundle +50KB/week.
```
### Phase 9: Save Report
Write `.gstack/benchmark-reports/{UTC-timestamp}-benchmark.md` (the Phase 5–8 output) and `.json` beside it: the Phase 4 object plus, per page, `grade` (Phase 7) and, with a baseline, `verdict` and each metric's `status` (Phase 5).
## Important Rules
- **Measure, don't guess.** Use actual performance.getEntries() data, not estimates.
- **Vet every number before reporting it.** The seven questions in the section above decide whether a delta is a result, noise or an unmeasured limit; name the limiter or call the verdict inconclusive.
- **Baseline is essential.** Without one you report absolute numbers and detect no regressions. Encourage one.
- **Relative thresholds, not absolute.** 2000ms is fine for a complex dashboard, terrible for a landing page. Compare against YOUR baseline.
- **Third-party scripts are context.** Flag them; the user can't fix a slow Google Analytics. Recommend first-party fixes.
- **Bundle size is the leading indicator.** Load time varies with the network; bundle size is deterministic. Track it.
- **Read-only.** Produce the report. Don't modify code unless explicitly asked.