Claude Fable 5.1 & GPT-6 Astra packages are live

Budgets

Free

Performance budgets as a CI contract — p50/p95 latency and RSS caps per scenario, a reproducible benchmark script, a committed baseline, and a…

218 lines8.9 KB Grok Performance
targetModels
Grok 4.6Grok 4.5Grok 4 FamilyGrok Code FastFuture Grok Models
name
budgets
category
Performance
description
Performance budgets as a CI contract — p50/p95 latency and RSS caps per scenario, a reproducible benchmark script, a committed baseline, and a regression gate that fails the build.
license
MIT
author
Agent.md maintainers
last-verified
reviewed-by
unreviewed
<!-- Generated from models/_canonical by scripts/build-model-variants.js. Edit the canonical source, not this file. Behavioural profile for Grok: scripts/model-profiles.json -->

#Non-negotiable

The constraints hoisted below override anything later in this document. Read them first; the rest is rationale.


#Purpose

Rules for turning "it should be fast" into numbers the build enforces. A performance regression that is not measured on every change is found by a customer, weeks later, in a version nobody can bisect. A budget is a committed table of limits, a script that reproduces the measurement, and a CI step that fails when a number crosses its line.

This standard covers the process. What to optimise once a budget fails is Performance/optimization, Performance/queries and Performance/memory.


#Budgets are a table in the repo, not a wiki page

yaml
# perf/budget.yaml — the contract. Reviewed like code.
scenarios:
  api-search:
    command: node perf/bench.js api-search
    p50_ms: 45
    p95_ms: 180
    rss_mb: 220
  render-dashboard:
    command: node perf/bench.js render-dashboard
    p50_ms: 120
    p95_ms: 400
    rss_mb: 350
tolerance_pct: 5        # noise allowance before a breach counts
runs: 5                 # per scenario, per CI job; median is compared
  • Every scenario declares p50_ms, p95_ms and rss_mb. Latency without a memory cap lets a change trade RSS for speed until the pod is OOM-killed.
  • p95 is the user's experience; p50 is the trend. Both are required — a flat p50 with a doubling p95 is a lock contention bug, not noise.
  • Budgets are set from a measured baseline with headroom, not from hope. A budget nobody has ever met is ignored within a week.
  • Raising a budget is a code change with a reviewer, a reason in the commit and a link to what got slower. "CI was red" is not a reason.

#The benchmark script is reproducible or it is decoration

js
// perf/bench.js — same corpus, same warm-up, same output shape every run
import { performance } from "node:perf_hooks";
import { scenarios } from "./scenarios.js";

const [name] = process.argv.slice(2);
const run = scenarios[name];
const samples = [];

for (let i = 0; i < 20; i++) await run();            // warm-up: JIT, caches, pools
for (let i = 0; i < 200; i++) {
  const t = performance.now();
  await run();
  samples.push(performance.now() - t);
}
samples.sort((a, b) => a - b);
const pct = (p) => samples[Math.floor(samples.length * p)];
console.log(JSON.stringify({
  scenario: name,
  p50_ms: +pct(0.5).toFixed(2),
  p95_ms: +pct(0.95).toFixed(2),
  rss_mb: +(process.memoryUsage().rss / 1048576).toFixed(1),
  node: process.version, commit: process.env.GITHUB_SHA ?? "local",
}));
  • Fixed corpus committed under perf/corpus/ (or generated by a seeded script, perf/corpus.js --seed 42). A benchmark that reads production data measures the data, not the code.
  • Warm-up before sampling; discard it. The first request pays for JIT, connection pools and cold caches and belongs to no percentile.
  • Output is one JSON line per scenario with the fields the budget names, plus node and commit. Prose output cannot be compared by a script.
  • Never sample with Date.now(); use performance.now() or process.hrtime.bigint().
  • Measure RSS after the run, not before, and after a forced gc() when the runtime allows it (node --expose-gc), so the number is retained memory rather than garbage the collector had not reached yet.

#Baseline: committed, dated, from a named machine

json
{
  "captured": "2026-09-13",
  "machine": "ci-runner ubuntu-24.04 4 vCPU 16 GB",
  "commit": "a1b2c3d",
  "scenarios": {
    "api-search":       { "p50_ms": 41.3, "p95_ms": 162.0, "rss_mb": 204.5 },
    "render-dashboard": { "p50_ms": 108.9, "p95_ms": 355.2, "rss_mb": 312.0 }
  }
}
  • perf/baseline.json is the median of at least three full runs on the machine class CI uses. A laptop baseline compared against a CI runner fails on every push, so the gate gets disabled.
  • Record captured, machine and commit. A number without its origin cannot be re-derived when the runner image changes.
  • Re-capture the baseline deliberately (npm run perf:baseline) when the runner changes or a budget is raised — never as a side effect of a green build.

#The gate fails the build

yaml
# .github/workflows/perf.yml
- run: npm ci
- run: node --expose-gc perf/bench.js api-search > perf/out/api-search.json
- run: node --expose-gc perf/bench.js render-dashboard > perf/out/render-dashboard.json
- run: node perf/compare.js perf/budget.yaml perf/baseline.json perf/out/   # exit 1 on breach
js
// perf/compare.js — the only logic: number vs limit, with tolerance
for (const [name, limit] of Object.entries(budget.scenarios)) {
  const got = results[name];
  for (const key of ["p50_ms", "p95_ms", "rss_mb"]) {
    const cap = limit[key] * (1 + budget.tolerance_pct / 100);
    if (got[key] > cap) fail(`${name}.${key} ${got[key]} > ${limit[key]} (+${budget.tolerance_pct}%)`);
  }
}
  • Two comparisons, both required: against the budget (absolute cap) and against the baseline (relative regression, for example more than tolerance_pct slower). The budget catches slow drift; the baseline catches the single bad commit.
  • tolerance_pct absorbs runner noise. Measure the noise first: run the benchmark ten times on an unchanged commit and set tolerance above the spread. A tolerance guessed at 1% fails randomly and gets ignored.
  • The gate blocks merge. A continue-on-error perf job is a dashboard, and dashboards are not read.
  • Post the table to the PR (p50/p95/rss, baseline, delta) so a reviewer sees the cost of a change without opening CI logs.

#Scenarios cover what users pay for

Scenario typeExampleWhy it earns a budget
Hot requestGET /v1/search with the 95th-percentile queryMost traffic, most exposure
Heavy requestReport export with 50k rowsWhere memory caps are breached
Cold startProcess boot to first successful responseAutoscaling and deploys pay it
Long session500 sequential operations, RSS sampled each 50Finds leaks a single run hides
Client renderTime to interactive on the main dashboardThe number users actually feel
  • Three to six scenarios. A budget file with forty scenarios is maintained by nobody, and the important ones drown.
  • Each scenario has an owner named in budget.yaml; a breach pages a person, not a channel.
  • Long-session scenarios compare RSS at the end against the start; growth above the cap fails even when latency is fine. → Performance/memory

#Anti-patterns

Anti-patternWhy it failsFix
Budget only in the wikiNothing enforces itperf/budget.yaml, gate in CI
Latency budget without RSSSpeed bought with memory until OOMrss_mb on every scenario
Benchmark against live dataMeasures the data, not the codeCommitted or seeded corpus
No warm-upp50 includes JIT and cold cachesDiscard warm-up runs
Single run comparedNoise reads as regression or masks oneMedian of runs, tolerance_pct
Baseline from a laptopFails on CI every time, gate disabledCapture on the CI machine class
continue-on-error: true on perf jobRed is decorativeGate blocks merge
Raising the budget to go greenThe contract becomes whatever the code doesReason + reviewer per raise
Prose benchmark outputNothing can compare itOne JSON line per scenario
Forty scenariosUnmaintained, important ones drownThree to six, each owned
Date.now() for timingMillisecond granularity, clock jumpsperformance.now()

#Checklist

  • Verify: perf/budget.yaml committed with p50_ms, p95_ms and rss_mb per scenario
  • Verify: perf/bench.js uses a committed or seeded corpus, warms up, and prints one JSON line per scenario
  • Verify: perf/baseline.json records captured, machine and commit, median of three runs on the CI machine class
  • Verify: tolerance_pct set above measured runner noise, not guessed
  • Verify: perf/compare.js checks results against both budget and baseline and exits non-zero on breach
  • Verify: The perf job blocks merge; no continue-on-error
  • Verify: Results table posted to the PR with baseline and delta
  • Verify: Three to six scenarios, each with a named owner
  • Verify: At least one long-session scenario compares end RSS against start
  • Verify: Every budget raise is a reviewed commit with a stated reason