[ci] Run benchmarks in-deployment to avoid proxy overhead - #2967

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps
Jul 17, 2026
Merged

[ci] Run benchmarks in-deployment to avoid proxy overhead#2967
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The CI benchmark drove load from a GitHub Actions runner through the public api.vercel.com ingress. Datadog traces of real runs proved the reported latencies were dominated by that external path and the CI runner's clock, not the runtime:

  • TTFS was steps[0].start − clientStart(GHA), so it included the CI→ingress leg, GHA↔deployment clock skew, and the full VQS dispatch. The earlier revision of this PR anchored TTFS/WO on the server run_created timestamp and added a flat +80ms RTT estimate — proxy-independent, but a fudge, and it still measured the external-trigger dispatch rather than what a real in-Vercel caller experiences.
  • SL read the stream from the GHA runner, so it was dominated by the api.vercel.com read path.

Goal (hard requirement): no api.vercel.com / CI-runner timing may appear in any measured window.

Approach

Trigger every run from inside the deployment and measure entirely from deployment-side clocks.

  • New POST /api/bench route on the nextjs-turbopack workbench app resolves a bench workflow from the generated registry, stamps clientStart with the deployment's clock immediately before start(), and returns { runId, clientStart }. The GHA→route request (and its proxy path) sits beforeclientStart, so it is outside every measured window. Calling start() in-deployment also engages the runtime's in-process fast path (optimisticStart), so turbo TTFS now reflects the inline path and non-turbo (hook) reflects the dispatch path — both proxy-free.
  • benchSlWorkflow measures SL entirely on the deployment: a reader step and a writer step run in parallel on a dedicated namespaced stream. The writer waits a fixed delay so the reader is attached and blocked on the first chunk first (measuring live pub/sub propagation, not a warm read), then writes writtenAt; the reader stamps readAt. SL = readAt − writtenAt, both deployment step-body clocks, excluding the api.vercel.com read path.
  • benchmark.test.ts triggers each run via the route (with trusted-sources headers) and polls returnValue by runId. TTFS/WO derive from the in-deployment clientStart + step timings; SL from the return value. Removed runCreatedServerMs, the flat RTT estimate, and the wall-clock diagnostic. STSO was already deployment-clock only and is unchanged.

Result

All four metrics (TTFS, STSO, WO, SL) are now computed purely from Vercel-side timestamps. The only residual skew is intra-Vercel (step-runner instances in the same region), NTP-bounded and small relative to the measured values.

Latency targets are provisional — now that the proxy leg and the external-dispatch framing are gone, they will be re-tightened once a few in-deployment baselines land on main.

Notes

  • Touches an e2e test + CI script + workbench (route + workflow) only — no published package output, so no changeset is required (pnpm changeset status --since=main is clean).
  • Bench matrix is nextjs-turbopack only; the route lives in that app.

The CI benchmark drives load from a GitHub Actions runner through the public
api.vercel.com ingress, so TTFS/WO were dominated by that external path rather
than the runtime. Datadog traces of a real run show server-side "queue pickup →
step body" is only ~170ms, yet the client-anchored TTFS was ~1192ms; ~800ms of
that is VQS dispatch on an idle deployment plus the create request's inbound
leg through api.vercel.com.
Anchor TTFS/WO on the Vercel-assigned run_created timestamp (server-stamped
createdAt, read via world.events.list with a world.runs.get fallback) and end
on the deployment's step-body clock, making both metrics independent of the CI
runner's clock and its network path to api.vercel.com. Add a flat +80ms
(BENCH_RTT_OVERHEAD_MS) as an estimate of the client→ingress request overhead
the server anchor doesn't capture, and note it in the PR comment. A diagnostic
log prints server-anchored vs client wall-clock TTFS so the 80ms can be tuned.
Move WO off the stream scenarios (where a single step makes it algebraically
identical to TTFS) to a server-anchored whole-run aggregate on the sequential
scenario. STSO is unchanged (already measured between step bodies on the
deployment); SL stays client-observed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 22:39
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6c7ede8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6c7ede8 · Fri, 17 Jul 2026 21:31:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep136213041368 🔴1396 🔴1726 🔴30
TTFSstream133112711350 🔴1373 🔴1418 🔴30
TTFShook + stream159115231615 🔴1671 🔴1853 🔴30
STSO1020 steps (1-20)294224328 🔴427 🔴511 🔴19
STSO1020 steps (101-120)290230340 🔴394 🔴401 🔴19
STSO1020 steps (1001-1020)785692785 🔴850 🔴1328 🔴19
WO1020 steps5210135210135210135210135210131
SLstream latency159117175 🔴187 🔴332 🔴30
📜 Previous results (3)

db0bb85

Fri, 17 Jul 2026 20:47:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep138713241408 🔴1454 🔴1614 🔴30
TTFSstream1351 (+6.7%)12961375 🔴1396 🔴1513 🔴30
TTFShook + stream1652 (+24%)15371681 🔴1766 🔴1830 🔴30
STSO1020 steps (1-20)290 (-3.1%)210314 🔴345 🔴629 🔴19
STSO1020 steps (101-120)270 (-13%)226280 🔴377 🔴380 🔴19
STSO1020 steps (1001-1020)737 (+18%)654822 🔴868 🔴871 🔴19
WO1020 steps5201435201435201435201435201431
SLstream latency223166234 🔴387 🔴430 🔴30

63ddeb4

Fri, 17 Jul 2026 20:00:22 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream983 (-22%)1310 🔴1327 🔴1358 🔴30
TTFShook + stream1233 (-7.2%)1580 🔴1604 🔴1633 🔴30
STSO1020 steps (1-20)290 (-3.0%)296 🔴422 🔴554 🔴19
STSO1020 steps (101-120)334 (+8.3%)342 🔴474 🔴621 🔴19
STSO1020 steps (1001-1020)742 (+19%)802 🔴951 🔴1024 🔴19
WO1020 steps5242035242035242035242031
SLstream latency200211 🔴280 🔴362 🔴30

9b712d5

Fri, 17 Jul 2026 17:11:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream950 (-12%)1350 🔴1386 🔴1622 🔴30
TTFShook + stream1283 (-2.8%)1569 🔴1635 🔴1653 🔴30
STSO1020 steps (1-20)321 (+15%)356 🔴433 🔴523 🔴19
STSO1020 steps (101-120)467 (+23%)541 🔴596 🔴769 🔴19
STSO1020 steps (1001-1020)726 (-40%)774 🔴815 🔴833 🔴19
WO1020 steps5989295989295989295989291
SLstream3215 (+194%)5882 🔴6011 🔴6263 🔴30
SLhook + stream4549 (+164%)5862 🔴5909 🔴6016 🔴30

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

Cold starts on the low-traffic workbench /flow invocation (~65% of runs on
preview) are the reason TTFS p75+ is ~1.3s despite warm dispatch being
~300ms. Keep them in the numbers (they are real bursty-workload latency) and
make the warm floor visible instead of hiding it.
- Add a p10 column to every metric (computeStats + render table). p10 shows
the warm-start floor next to the cold-start-inflated upper percentiles.
- Add benchStepWorkflow: one trivial no-op step, no stream, turbo mode — the
cleanest TTFS scenario ("step"), alongside the existing streaming-step
turbo scenario.
- Document in the file header and PR comment footnote that cold starts are
intentionally included and p10 is the warm reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four inline findings from the benchmark methodology review.

@@ -483,22 +574,11 @@ describe('workflow benchmarks', () => {
results.map((r) => r.ttfsMs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Version the new methodology before comparing baselines. TTFS keeps the existing ttfs/stream and ttfs/hook + stream keys while changing from the CI/proxy-inclusive clock window to an in-deployment window, and the result schema is still version: 1. annotateWithBaseline() matches only backend/app/metric/scenario, so the PR comment's TTFS deltas compare incompatible measurements; historical entries also inherit the new footer. Please add a methodology/schema version to baseline and history matching, or suppress those comparisons until main has produced a compatible baseline.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Results now carry methodologyVersion: 2 and annotateWithBaseline() keys deltas on methodologyVersion/backend/app/metric/scenario, so old proxy-inclusive main baselines no longer match the in-deployment runs — the delta column stays blank until main has produced a v2 baseline, then v2-vs-v2 comparisons resume. Added a suppresses deltas when the baseline methodology version differs test. (The footer legend is a single global block describing the current methodology; per-entry footers for collapsed history are out of scope here.)

namespace: SL_STREAM_NAMESPACE,
});
const writer = writable.getWriter();
await new Promise((resolve) => setTimeout(resolve, SL_WRITE_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace the fixed delay with an explicit reader-ready barrier. A 750 ms sleep cannot guarantee the parallel reader step has started and is blocked: under scheduler delay or load, the writer can publish first and the reader will consume a retained chunk. That measures scheduling/catch-up rather than live pub/sub propagation. Please initiate reader.read(), signal readiness through a separate hook/stream, and let the writer stamp/write only after observing that signal.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Replaced the 750ms sleep with an explicit reader-ready barrier over a second stream: the reader initiates its SL read() (establishing the stream GET) then writes a marker to bench-sl-ready; the writer blocks reading that marker before it writes+stamps the SL chunk. So the write always lands on an already-attached reader (live propagation), not a retained chunk consumed by a late reader. The ready handshake tolerates either attach order (a retained marker is fine — we don't measure its latency), so no deadlock.

ttfs: {
name: 'TTFS',
description:
'time to first step body (in-deployment start() → first step body, deployment clocks)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the renderer test with the new output shape..github/scripts/render-benchmark-comment.test.js still expects the old table header without P10 (line 106) and the old TTFS wording (lines 111-114), so node --test .github/scripts/render-benchmark-comment.test.js fails. Please update both assertions and add coverage for the P10 cell.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated render-benchmark-comment.test.js: the header assertion now includes P10 (ms), the TTFS wording matcher is updated, and there's a new P10-cell assertion (| 412 | 357 | 398 🔴 |). node --test passes (11/11).

* Vercel only.
* @workflow/world-postgres, local filesystem otherwise. Because SL is now
* measured inside the workflow (not by a reader in this process), it no longer
* depends on `run.getReadable()` working across processes; CI still runs this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Keep the benchmark workflow documentation aligned..github/workflows/benchmarks.yml:98-100 still says SL requires run.getReadable() from the test process and therefore cannot use the local world's in-process streamer. This PR moves the reader inside the workflow, so that comment now contradicts the implementation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated the benchmarks.yml comment — SL is now measured inside the workflow (benchSlWorkflow's parallel reader/writer steps), so it no longer needs run.getReadable() from the test process; the runner only polls returnValue.

…st/doc sync
- [P1] Add methodologyVersion (=2) to results; renderer keys baseline deltas
on it so the in-deployment numbers aren't diffed against old proxy-inclusive
main baselines (deltas stay blank until main has a v2 baseline). Covered by
a new "suppresses deltas when methodology differs" test.
- [P1] Replace the fixed 750ms SL writer delay with an explicit reader-ready
barrier: the reader initiates its SL read then signals on a second stream;
the writer blocks on that signal before writing. Measures live propagation,
not a late reader catching up on a retained chunk.
- [P2] Update render-benchmark-comment.test.js for the P10 column header and
new TTFS wording, and add P10-cell coverage.
- [P3] Fix the stale benchmarks.yml comment claiming SL needs run.getReadable()
from the test process — SL is now measured inside the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for d53b055 (AI decision).

This commit refines a benchmark harness that only exists on main: stable still uses the older vitest bench setup (bench.bench.ts, the render-benchmarks action, and a completely different 97_bench.ts), and lacks packages/core/e2e/benchmark.test.ts, .github/scripts/render-benchmark-comment.mjs, and the /api/bench route this change modifies/depends on. The change is CI-only, builds on main-only benchmark infrastructure, and has no published-package impact (no changeset), so it should not be backported.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

d53b055a2b1aadf54afe59fd122cb1d960043bcb

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[ci] Run benchmarks in-deployment to avoid proxy overhead - #2967

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps
Jul 17, 2026
Merged

[ci] Run benchmarks in-deployment to avoid proxy overhead#2967
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The CI benchmark drove load from a GitHub Actions runner through the public api.vercel.com ingress. Datadog traces of real runs proved the reported latencies were dominated by that external path and the CI runner's clock, not the runtime:

  • TTFS was steps[0].start − clientStart(GHA), so it included the CI→ingress leg, GHA↔deployment clock skew, and the full VQS dispatch. The earlier revision of this PR anchored TTFS/WO on the server run_created timestamp and added a flat +80ms RTT estimate — proxy-independent, but a fudge, and it still measured the external-trigger dispatch rather than what a real in-Vercel caller experiences.
  • SL read the stream from the GHA runner, so it was dominated by the api.vercel.com read path.

Goal (hard requirement): no api.vercel.com / CI-runner timing may appear in any measured window.

Approach

Trigger every run from inside the deployment and measure entirely from deployment-side clocks.

  • New POST /api/bench route on the nextjs-turbopack workbench app resolves a bench workflow from the generated registry, stamps clientStart with the deployment's clock immediately before start(), and returns { runId, clientStart }. The GHA→route request (and its proxy path) sits beforeclientStart, so it is outside every measured window. Calling start() in-deployment also engages the runtime's in-process fast path (optimisticStart), so turbo TTFS now reflects the inline path and non-turbo (hook) reflects the dispatch path — both proxy-free.
  • benchSlWorkflow measures SL entirely on the deployment: a reader step and a writer step run in parallel on a dedicated namespaced stream. The writer waits a fixed delay so the reader is attached and blocked on the first chunk first (measuring live pub/sub propagation, not a warm read), then writes writtenAt; the reader stamps readAt. SL = readAt − writtenAt, both deployment step-body clocks, excluding the api.vercel.com read path.
  • benchmark.test.ts triggers each run via the route (with trusted-sources headers) and polls returnValue by runId. TTFS/WO derive from the in-deployment clientStart + step timings; SL from the return value. Removed runCreatedServerMs, the flat RTT estimate, and the wall-clock diagnostic. STSO was already deployment-clock only and is unchanged.

Result

All four metrics (TTFS, STSO, WO, SL) are now computed purely from Vercel-side timestamps. The only residual skew is intra-Vercel (step-runner instances in the same region), NTP-bounded and small relative to the measured values.

Latency targets are provisional — now that the proxy leg and the external-dispatch framing are gone, they will be re-tightened once a few in-deployment baselines land on main.

Notes

  • Touches an e2e test + CI script + workbench (route + workflow) only — no published package output, so no changeset is required (pnpm changeset status --since=main is clean).
  • Bench matrix is nextjs-turbopack only; the route lives in that app.

The CI benchmark drives load from a GitHub Actions runner through the public
api.vercel.com ingress, so TTFS/WO were dominated by that external path rather
than the runtime. Datadog traces of a real run show server-side "queue pickup →
step body" is only ~170ms, yet the client-anchored TTFS was ~1192ms; ~800ms of
that is VQS dispatch on an idle deployment plus the create request's inbound
leg through api.vercel.com.
Anchor TTFS/WO on the Vercel-assigned run_created timestamp (server-stamped
createdAt, read via world.events.list with a world.runs.get fallback) and end
on the deployment's step-body clock, making both metrics independent of the CI
runner's clock and its network path to api.vercel.com. Add a flat +80ms
(BENCH_RTT_OVERHEAD_MS) as an estimate of the client→ingress request overhead
the server anchor doesn't capture, and note it in the PR comment. A diagnostic
log prints server-anchored vs client wall-clock TTFS so the 80ms can be tuned.
Move WO off the stream scenarios (where a single step makes it algebraically
identical to TTFS) to a server-anchored whole-run aggregate on the sequential
scenario. STSO is unchanged (already measured between step bodies on the
deployment); SL stays client-observed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 22:39
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6c7ede8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6c7ede8 · Fri, 17 Jul 2026 21:31:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep136213041368 🔴1396 🔴1726 🔴30
TTFSstream133112711350 🔴1373 🔴1418 🔴30
TTFShook + stream159115231615 🔴1671 🔴1853 🔴30
STSO1020 steps (1-20)294224328 🔴427 🔴511 🔴19
STSO1020 steps (101-120)290230340 🔴394 🔴401 🔴19
STSO1020 steps (1001-1020)785692785 🔴850 🔴1328 🔴19
WO1020 steps5210135210135210135210135210131
SLstream latency159117175 🔴187 🔴332 🔴30
📜 Previous results (3)

db0bb85

Fri, 17 Jul 2026 20:47:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep138713241408 🔴1454 🔴1614 🔴30
TTFSstream1351 (+6.7%)12961375 🔴1396 🔴1513 🔴30
TTFShook + stream1652 (+24%)15371681 🔴1766 🔴1830 🔴30
STSO1020 steps (1-20)290 (-3.1%)210314 🔴345 🔴629 🔴19
STSO1020 steps (101-120)270 (-13%)226280 🔴377 🔴380 🔴19
STSO1020 steps (1001-1020)737 (+18%)654822 🔴868 🔴871 🔴19
WO1020 steps5201435201435201435201435201431
SLstream latency223166234 🔴387 🔴430 🔴30

63ddeb4

Fri, 17 Jul 2026 20:00:22 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream983 (-22%)1310 🔴1327 🔴1358 🔴30
TTFShook + stream1233 (-7.2%)1580 🔴1604 🔴1633 🔴30
STSO1020 steps (1-20)290 (-3.0%)296 🔴422 🔴554 🔴19
STSO1020 steps (101-120)334 (+8.3%)342 🔴474 🔴621 🔴19
STSO1020 steps (1001-1020)742 (+19%)802 🔴951 🔴1024 🔴19
WO1020 steps5242035242035242035242031
SLstream latency200211 🔴280 🔴362 🔴30

9b712d5

Fri, 17 Jul 2026 17:11:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream950 (-12%)1350 🔴1386 🔴1622 🔴30
TTFShook + stream1283 (-2.8%)1569 🔴1635 🔴1653 🔴30
STSO1020 steps (1-20)321 (+15%)356 🔴433 🔴523 🔴19
STSO1020 steps (101-120)467 (+23%)541 🔴596 🔴769 🔴19
STSO1020 steps (1001-1020)726 (-40%)774 🔴815 🔴833 🔴19
WO1020 steps5989295989295989295989291
SLstream3215 (+194%)5882 🔴6011 🔴6263 🔴30
SLhook + stream4549 (+164%)5862 🔴5909 🔴6016 🔴30

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

Cold starts on the low-traffic workbench /flow invocation (~65% of runs on
preview) are the reason TTFS p75+ is ~1.3s despite warm dispatch being
~300ms. Keep them in the numbers (they are real bursty-workload latency) and
make the warm floor visible instead of hiding it.
- Add a p10 column to every metric (computeStats + render table). p10 shows
the warm-start floor next to the cold-start-inflated upper percentiles.
- Add benchStepWorkflow: one trivial no-op step, no stream, turbo mode — the
cleanest TTFS scenario ("step"), alongside the existing streaming-step
turbo scenario.
- Document in the file header and PR comment footnote that cold starts are
intentionally included and p10 is the warm reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four inline findings from the benchmark methodology review.

@@ -483,22 +574,11 @@ describe('workflow benchmarks', () => {
results.map((r) => r.ttfsMs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Version the new methodology before comparing baselines. TTFS keeps the existing ttfs/stream and ttfs/hook + stream keys while changing from the CI/proxy-inclusive clock window to an in-deployment window, and the result schema is still version: 1. annotateWithBaseline() matches only backend/app/metric/scenario, so the PR comment's TTFS deltas compare incompatible measurements; historical entries also inherit the new footer. Please add a methodology/schema version to baseline and history matching, or suppress those comparisons until main has produced a compatible baseline.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Results now carry methodologyVersion: 2 and annotateWithBaseline() keys deltas on methodologyVersion/backend/app/metric/scenario, so old proxy-inclusive main baselines no longer match the in-deployment runs — the delta column stays blank until main has produced a v2 baseline, then v2-vs-v2 comparisons resume. Added a suppresses deltas when the baseline methodology version differs test. (The footer legend is a single global block describing the current methodology; per-entry footers for collapsed history are out of scope here.)

namespace: SL_STREAM_NAMESPACE,
});
const writer = writable.getWriter();
await new Promise((resolve) => setTimeout(resolve, SL_WRITE_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace the fixed delay with an explicit reader-ready barrier. A 750 ms sleep cannot guarantee the parallel reader step has started and is blocked: under scheduler delay or load, the writer can publish first and the reader will consume a retained chunk. That measures scheduling/catch-up rather than live pub/sub propagation. Please initiate reader.read(), signal readiness through a separate hook/stream, and let the writer stamp/write only after observing that signal.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Replaced the 750ms sleep with an explicit reader-ready barrier over a second stream: the reader initiates its SL read() (establishing the stream GET) then writes a marker to bench-sl-ready; the writer blocks reading that marker before it writes+stamps the SL chunk. So the write always lands on an already-attached reader (live propagation), not a retained chunk consumed by a late reader. The ready handshake tolerates either attach order (a retained marker is fine — we don't measure its latency), so no deadlock.

ttfs: {
name: 'TTFS',
description:
'time to first step body (in-deployment start() → first step body, deployment clocks)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the renderer test with the new output shape..github/scripts/render-benchmark-comment.test.js still expects the old table header without P10 (line 106) and the old TTFS wording (lines 111-114), so node --test .github/scripts/render-benchmark-comment.test.js fails. Please update both assertions and add coverage for the P10 cell.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated render-benchmark-comment.test.js: the header assertion now includes P10 (ms), the TTFS wording matcher is updated, and there's a new P10-cell assertion (| 412 | 357 | 398 🔴 |). node --test passes (11/11).

* Vercel only.
* @workflow/world-postgres, local filesystem otherwise. Because SL is now
* measured inside the workflow (not by a reader in this process), it no longer
* depends on `run.getReadable()` working across processes; CI still runs this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Keep the benchmark workflow documentation aligned..github/workflows/benchmarks.yml:98-100 still says SL requires run.getReadable() from the test process and therefore cannot use the local world's in-process streamer. This PR moves the reader inside the workflow, so that comment now contradicts the implementation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated the benchmarks.yml comment — SL is now measured inside the workflow (benchSlWorkflow's parallel reader/writer steps), so it no longer needs run.getReadable() from the test process; the runner only polls returnValue.

…st/doc sync
- [P1] Add methodologyVersion (=2) to results; renderer keys baseline deltas
on it so the in-deployment numbers aren't diffed against old proxy-inclusive
main baselines (deltas stay blank until main has a v2 baseline). Covered by
a new "suppresses deltas when methodology differs" test.
- [P1] Replace the fixed 750ms SL writer delay with an explicit reader-ready
barrier: the reader initiates its SL read then signals on a second stream;
the writer blocks on that signal before writing. Measures live propagation,
not a late reader catching up on a retained chunk.
- [P2] Update render-benchmark-comment.test.js for the P10 column header and
new TTFS wording, and add P10-cell coverage.
- [P3] Fix the stale benchmarks.yml comment claiming SL needs run.getReadable()
from the test process — SL is now measured inside the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for d53b055 (AI decision).

This commit refines a benchmark harness that only exists on main: stable still uses the older vitest bench setup (bench.bench.ts, the render-benchmarks action, and a completely different 97_bench.ts), and lacks packages/core/e2e/benchmark.test.ts, .github/scripts/render-benchmark-comment.mjs, and the /api/bench route this change modifies/depends on. The change is CI-only, builds on main-only benchmark infrastructure, and has no published-package impact (no changeset), so it should not be backported.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

d53b055a2b1aadf54afe59fd122cb1d960043bcb

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[ci] Run benchmarks in-deployment to avoid proxy overhead - #2967

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps
Jul 17, 2026
Merged

[ci] Run benchmarks in-deployment to avoid proxy overhead#2967
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The CI benchmark drove load from a GitHub Actions runner through the public api.vercel.com ingress. Datadog traces of real runs proved the reported latencies were dominated by that external path and the CI runner's clock, not the runtime:

  • TTFS was steps[0].start − clientStart(GHA), so it included the CI→ingress leg, GHA↔deployment clock skew, and the full VQS dispatch. The earlier revision of this PR anchored TTFS/WO on the server run_created timestamp and added a flat +80ms RTT estimate — proxy-independent, but a fudge, and it still measured the external-trigger dispatch rather than what a real in-Vercel caller experiences.
  • SL read the stream from the GHA runner, so it was dominated by the api.vercel.com read path.

Goal (hard requirement): no api.vercel.com / CI-runner timing may appear in any measured window.

Approach

Trigger every run from inside the deployment and measure entirely from deployment-side clocks.

  • New POST /api/bench route on the nextjs-turbopack workbench app resolves a bench workflow from the generated registry, stamps clientStart with the deployment's clock immediately before start(), and returns { runId, clientStart }. The GHA→route request (and its proxy path) sits beforeclientStart, so it is outside every measured window. Calling start() in-deployment also engages the runtime's in-process fast path (optimisticStart), so turbo TTFS now reflects the inline path and non-turbo (hook) reflects the dispatch path — both proxy-free.
  • benchSlWorkflow measures SL entirely on the deployment: a reader step and a writer step run in parallel on a dedicated namespaced stream. The writer waits a fixed delay so the reader is attached and blocked on the first chunk first (measuring live pub/sub propagation, not a warm read), then writes writtenAt; the reader stamps readAt. SL = readAt − writtenAt, both deployment step-body clocks, excluding the api.vercel.com read path.
  • benchmark.test.ts triggers each run via the route (with trusted-sources headers) and polls returnValue by runId. TTFS/WO derive from the in-deployment clientStart + step timings; SL from the return value. Removed runCreatedServerMs, the flat RTT estimate, and the wall-clock diagnostic. STSO was already deployment-clock only and is unchanged.

Result

All four metrics (TTFS, STSO, WO, SL) are now computed purely from Vercel-side timestamps. The only residual skew is intra-Vercel (step-runner instances in the same region), NTP-bounded and small relative to the measured values.

Latency targets are provisional — now that the proxy leg and the external-dispatch framing are gone, they will be re-tightened once a few in-deployment baselines land on main.

Notes

  • Touches an e2e test + CI script + workbench (route + workflow) only — no published package output, so no changeset is required (pnpm changeset status --since=main is clean).
  • Bench matrix is nextjs-turbopack only; the route lives in that app.

The CI benchmark drives load from a GitHub Actions runner through the public
api.vercel.com ingress, so TTFS/WO were dominated by that external path rather
than the runtime. Datadog traces of a real run show server-side "queue pickup →
step body" is only ~170ms, yet the client-anchored TTFS was ~1192ms; ~800ms of
that is VQS dispatch on an idle deployment plus the create request's inbound
leg through api.vercel.com.
Anchor TTFS/WO on the Vercel-assigned run_created timestamp (server-stamped
createdAt, read via world.events.list with a world.runs.get fallback) and end
on the deployment's step-body clock, making both metrics independent of the CI
runner's clock and its network path to api.vercel.com. Add a flat +80ms
(BENCH_RTT_OVERHEAD_MS) as an estimate of the client→ingress request overhead
the server anchor doesn't capture, and note it in the PR comment. A diagnostic
log prints server-anchored vs client wall-clock TTFS so the 80ms can be tuned.
Move WO off the stream scenarios (where a single step makes it algebraically
identical to TTFS) to a server-anchored whole-run aggregate on the sequential
scenario. STSO is unchanged (already measured between step bodies on the
deployment); SL stays client-observed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 22:39
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6c7ede8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6c7ede8 · Fri, 17 Jul 2026 21:31:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep136213041368 🔴1396 🔴1726 🔴30
TTFSstream133112711350 🔴1373 🔴1418 🔴30
TTFShook + stream159115231615 🔴1671 🔴1853 🔴30
STSO1020 steps (1-20)294224328 🔴427 🔴511 🔴19
STSO1020 steps (101-120)290230340 🔴394 🔴401 🔴19
STSO1020 steps (1001-1020)785692785 🔴850 🔴1328 🔴19
WO1020 steps5210135210135210135210135210131
SLstream latency159117175 🔴187 🔴332 🔴30
📜 Previous results (3)

db0bb85

Fri, 17 Jul 2026 20:47:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep138713241408 🔴1454 🔴1614 🔴30
TTFSstream1351 (+6.7%)12961375 🔴1396 🔴1513 🔴30
TTFShook + stream1652 (+24%)15371681 🔴1766 🔴1830 🔴30
STSO1020 steps (1-20)290 (-3.1%)210314 🔴345 🔴629 🔴19
STSO1020 steps (101-120)270 (-13%)226280 🔴377 🔴380 🔴19
STSO1020 steps (1001-1020)737 (+18%)654822 🔴868 🔴871 🔴19
WO1020 steps5201435201435201435201435201431
SLstream latency223166234 🔴387 🔴430 🔴30

63ddeb4

Fri, 17 Jul 2026 20:00:22 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream983 (-22%)1310 🔴1327 🔴1358 🔴30
TTFShook + stream1233 (-7.2%)1580 🔴1604 🔴1633 🔴30
STSO1020 steps (1-20)290 (-3.0%)296 🔴422 🔴554 🔴19
STSO1020 steps (101-120)334 (+8.3%)342 🔴474 🔴621 🔴19
STSO1020 steps (1001-1020)742 (+19%)802 🔴951 🔴1024 🔴19
WO1020 steps5242035242035242035242031
SLstream latency200211 🔴280 🔴362 🔴30

9b712d5

Fri, 17 Jul 2026 17:11:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream950 (-12%)1350 🔴1386 🔴1622 🔴30
TTFShook + stream1283 (-2.8%)1569 🔴1635 🔴1653 🔴30
STSO1020 steps (1-20)321 (+15%)356 🔴433 🔴523 🔴19
STSO1020 steps (101-120)467 (+23%)541 🔴596 🔴769 🔴19
STSO1020 steps (1001-1020)726 (-40%)774 🔴815 🔴833 🔴19
WO1020 steps5989295989295989295989291
SLstream3215 (+194%)5882 🔴6011 🔴6263 🔴30
SLhook + stream4549 (+164%)5862 🔴5909 🔴6016 🔴30

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

Cold starts on the low-traffic workbench /flow invocation (~65% of runs on
preview) are the reason TTFS p75+ is ~1.3s despite warm dispatch being
~300ms. Keep them in the numbers (they are real bursty-workload latency) and
make the warm floor visible instead of hiding it.
- Add a p10 column to every metric (computeStats + render table). p10 shows
the warm-start floor next to the cold-start-inflated upper percentiles.
- Add benchStepWorkflow: one trivial no-op step, no stream, turbo mode — the
cleanest TTFS scenario ("step"), alongside the existing streaming-step
turbo scenario.
- Document in the file header and PR comment footnote that cold starts are
intentionally included and p10 is the warm reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four inline findings from the benchmark methodology review.

@@ -483,22 +574,11 @@ describe('workflow benchmarks', () => {
results.map((r) => r.ttfsMs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Version the new methodology before comparing baselines. TTFS keeps the existing ttfs/stream and ttfs/hook + stream keys while changing from the CI/proxy-inclusive clock window to an in-deployment window, and the result schema is still version: 1. annotateWithBaseline() matches only backend/app/metric/scenario, so the PR comment's TTFS deltas compare incompatible measurements; historical entries also inherit the new footer. Please add a methodology/schema version to baseline and history matching, or suppress those comparisons until main has produced a compatible baseline.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Results now carry methodologyVersion: 2 and annotateWithBaseline() keys deltas on methodologyVersion/backend/app/metric/scenario, so old proxy-inclusive main baselines no longer match the in-deployment runs — the delta column stays blank until main has produced a v2 baseline, then v2-vs-v2 comparisons resume. Added a suppresses deltas when the baseline methodology version differs test. (The footer legend is a single global block describing the current methodology; per-entry footers for collapsed history are out of scope here.)

namespace: SL_STREAM_NAMESPACE,
});
const writer = writable.getWriter();
await new Promise((resolve) => setTimeout(resolve, SL_WRITE_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace the fixed delay with an explicit reader-ready barrier. A 750 ms sleep cannot guarantee the parallel reader step has started and is blocked: under scheduler delay or load, the writer can publish first and the reader will consume a retained chunk. That measures scheduling/catch-up rather than live pub/sub propagation. Please initiate reader.read(), signal readiness through a separate hook/stream, and let the writer stamp/write only after observing that signal.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Replaced the 750ms sleep with an explicit reader-ready barrier over a second stream: the reader initiates its SL read() (establishing the stream GET) then writes a marker to bench-sl-ready; the writer blocks reading that marker before it writes+stamps the SL chunk. So the write always lands on an already-attached reader (live propagation), not a retained chunk consumed by a late reader. The ready handshake tolerates either attach order (a retained marker is fine — we don't measure its latency), so no deadlock.

ttfs: {
name: 'TTFS',
description:
'time to first step body (in-deployment start() → first step body, deployment clocks)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the renderer test with the new output shape..github/scripts/render-benchmark-comment.test.js still expects the old table header without P10 (line 106) and the old TTFS wording (lines 111-114), so node --test .github/scripts/render-benchmark-comment.test.js fails. Please update both assertions and add coverage for the P10 cell.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated render-benchmark-comment.test.js: the header assertion now includes P10 (ms), the TTFS wording matcher is updated, and there's a new P10-cell assertion (| 412 | 357 | 398 🔴 |). node --test passes (11/11).

* Vercel only.
* @workflow/world-postgres, local filesystem otherwise. Because SL is now
* measured inside the workflow (not by a reader in this process), it no longer
* depends on `run.getReadable()` working across processes; CI still runs this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Keep the benchmark workflow documentation aligned..github/workflows/benchmarks.yml:98-100 still says SL requires run.getReadable() from the test process and therefore cannot use the local world's in-process streamer. This PR moves the reader inside the workflow, so that comment now contradicts the implementation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated the benchmarks.yml comment — SL is now measured inside the workflow (benchSlWorkflow's parallel reader/writer steps), so it no longer needs run.getReadable() from the test process; the runner only polls returnValue.

…st/doc sync
- [P1] Add methodologyVersion (=2) to results; renderer keys baseline deltas
on it so the in-deployment numbers aren't diffed against old proxy-inclusive
main baselines (deltas stay blank until main has a v2 baseline). Covered by
a new "suppresses deltas when methodology differs" test.
- [P1] Replace the fixed 750ms SL writer delay with an explicit reader-ready
barrier: the reader initiates its SL read then signals on a second stream;
the writer blocks on that signal before writing. Measures live propagation,
not a late reader catching up on a retained chunk.
- [P2] Update render-benchmark-comment.test.js for the P10 column header and
new TTFS wording, and add P10-cell coverage.
- [P3] Fix the stale benchmarks.yml comment claiming SL needs run.getReadable()
from the test process — SL is now measured inside the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for d53b055 (AI decision).

This commit refines a benchmark harness that only exists on main: stable still uses the older vitest bench setup (bench.bench.ts, the render-benchmarks action, and a completely different 97_bench.ts), and lacks packages/core/e2e/benchmark.test.ts, .github/scripts/render-benchmark-comment.mjs, and the /api/bench route this change modifies/depends on. The change is CI-only, builds on main-only benchmark infrastructure, and has no published-package impact (no changeset), so it should not be backported.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

d53b055a2b1aadf54afe59fd122cb1d960043bcb

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[ci] Run benchmarks in-deployment to avoid proxy overhead - #2967

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps
Jul 17, 2026
Merged

[ci] Run benchmarks in-deployment to avoid proxy overhead#2967
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The CI benchmark drove load from a GitHub Actions runner through the public api.vercel.com ingress. Datadog traces of real runs proved the reported latencies were dominated by that external path and the CI runner's clock, not the runtime:

  • TTFS was steps[0].start − clientStart(GHA), so it included the CI→ingress leg, GHA↔deployment clock skew, and the full VQS dispatch. The earlier revision of this PR anchored TTFS/WO on the server run_created timestamp and added a flat +80ms RTT estimate — proxy-independent, but a fudge, and it still measured the external-trigger dispatch rather than what a real in-Vercel caller experiences.
  • SL read the stream from the GHA runner, so it was dominated by the api.vercel.com read path.

Goal (hard requirement): no api.vercel.com / CI-runner timing may appear in any measured window.

Approach

Trigger every run from inside the deployment and measure entirely from deployment-side clocks.

  • New POST /api/bench route on the nextjs-turbopack workbench app resolves a bench workflow from the generated registry, stamps clientStart with the deployment's clock immediately before start(), and returns { runId, clientStart }. The GHA→route request (and its proxy path) sits beforeclientStart, so it is outside every measured window. Calling start() in-deployment also engages the runtime's in-process fast path (optimisticStart), so turbo TTFS now reflects the inline path and non-turbo (hook) reflects the dispatch path — both proxy-free.
  • benchSlWorkflow measures SL entirely on the deployment: a reader step and a writer step run in parallel on a dedicated namespaced stream. The writer waits a fixed delay so the reader is attached and blocked on the first chunk first (measuring live pub/sub propagation, not a warm read), then writes writtenAt; the reader stamps readAt. SL = readAt − writtenAt, both deployment step-body clocks, excluding the api.vercel.com read path.
  • benchmark.test.ts triggers each run via the route (with trusted-sources headers) and polls returnValue by runId. TTFS/WO derive from the in-deployment clientStart + step timings; SL from the return value. Removed runCreatedServerMs, the flat RTT estimate, and the wall-clock diagnostic. STSO was already deployment-clock only and is unchanged.

Result

All four metrics (TTFS, STSO, WO, SL) are now computed purely from Vercel-side timestamps. The only residual skew is intra-Vercel (step-runner instances in the same region), NTP-bounded and small relative to the measured values.

Latency targets are provisional — now that the proxy leg and the external-dispatch framing are gone, they will be re-tightened once a few in-deployment baselines land on main.

Notes

  • Touches an e2e test + CI script + workbench (route + workflow) only — no published package output, so no changeset is required (pnpm changeset status --since=main is clean).
  • Bench matrix is nextjs-turbopack only; the route lives in that app.

The CI benchmark drives load from a GitHub Actions runner through the public
api.vercel.com ingress, so TTFS/WO were dominated by that external path rather
than the runtime. Datadog traces of a real run show server-side "queue pickup →
step body" is only ~170ms, yet the client-anchored TTFS was ~1192ms; ~800ms of
that is VQS dispatch on an idle deployment plus the create request's inbound
leg through api.vercel.com.
Anchor TTFS/WO on the Vercel-assigned run_created timestamp (server-stamped
createdAt, read via world.events.list with a world.runs.get fallback) and end
on the deployment's step-body clock, making both metrics independent of the CI
runner's clock and its network path to api.vercel.com. Add a flat +80ms
(BENCH_RTT_OVERHEAD_MS) as an estimate of the client→ingress request overhead
the server anchor doesn't capture, and note it in the PR comment. A diagnostic
log prints server-anchored vs client wall-clock TTFS so the 80ms can be tuned.
Move WO off the stream scenarios (where a single step makes it algebraically
identical to TTFS) to a server-anchored whole-run aggregate on the sequential
scenario. STSO is unchanged (already measured between step bodies on the
deployment); SL stays client-observed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 22:39
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6c7ede8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6c7ede8 · Fri, 17 Jul 2026 21:31:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep136213041368 🔴1396 🔴1726 🔴30
TTFSstream133112711350 🔴1373 🔴1418 🔴30
TTFShook + stream159115231615 🔴1671 🔴1853 🔴30
STSO1020 steps (1-20)294224328 🔴427 🔴511 🔴19
STSO1020 steps (101-120)290230340 🔴394 🔴401 🔴19
STSO1020 steps (1001-1020)785692785 🔴850 🔴1328 🔴19
WO1020 steps5210135210135210135210135210131
SLstream latency159117175 🔴187 🔴332 🔴30
📜 Previous results (3)

db0bb85

Fri, 17 Jul 2026 20:47:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep138713241408 🔴1454 🔴1614 🔴30
TTFSstream1351 (+6.7%)12961375 🔴1396 🔴1513 🔴30
TTFShook + stream1652 (+24%)15371681 🔴1766 🔴1830 🔴30
STSO1020 steps (1-20)290 (-3.1%)210314 🔴345 🔴629 🔴19
STSO1020 steps (101-120)270 (-13%)226280 🔴377 🔴380 🔴19
STSO1020 steps (1001-1020)737 (+18%)654822 🔴868 🔴871 🔴19
WO1020 steps5201435201435201435201435201431
SLstream latency223166234 🔴387 🔴430 🔴30

63ddeb4

Fri, 17 Jul 2026 20:00:22 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream983 (-22%)1310 🔴1327 🔴1358 🔴30
TTFShook + stream1233 (-7.2%)1580 🔴1604 🔴1633 🔴30
STSO1020 steps (1-20)290 (-3.0%)296 🔴422 🔴554 🔴19
STSO1020 steps (101-120)334 (+8.3%)342 🔴474 🔴621 🔴19
STSO1020 steps (1001-1020)742 (+19%)802 🔴951 🔴1024 🔴19
WO1020 steps5242035242035242035242031
SLstream latency200211 🔴280 🔴362 🔴30

9b712d5

Fri, 17 Jul 2026 17:11:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream950 (-12%)1350 🔴1386 🔴1622 🔴30
TTFShook + stream1283 (-2.8%)1569 🔴1635 🔴1653 🔴30
STSO1020 steps (1-20)321 (+15%)356 🔴433 🔴523 🔴19
STSO1020 steps (101-120)467 (+23%)541 🔴596 🔴769 🔴19
STSO1020 steps (1001-1020)726 (-40%)774 🔴815 🔴833 🔴19
WO1020 steps5989295989295989295989291
SLstream3215 (+194%)5882 🔴6011 🔴6263 🔴30
SLhook + stream4549 (+164%)5862 🔴5909 🔴6016 🔴30

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

Cold starts on the low-traffic workbench /flow invocation (~65% of runs on
preview) are the reason TTFS p75+ is ~1.3s despite warm dispatch being
~300ms. Keep them in the numbers (they are real bursty-workload latency) and
make the warm floor visible instead of hiding it.
- Add a p10 column to every metric (computeStats + render table). p10 shows
the warm-start floor next to the cold-start-inflated upper percentiles.
- Add benchStepWorkflow: one trivial no-op step, no stream, turbo mode — the
cleanest TTFS scenario ("step"), alongside the existing streaming-step
turbo scenario.
- Document in the file header and PR comment footnote that cold starts are
intentionally included and p10 is the warm reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four inline findings from the benchmark methodology review.

@@ -483,22 +574,11 @@ describe('workflow benchmarks', () => {
results.map((r) => r.ttfsMs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Version the new methodology before comparing baselines. TTFS keeps the existing ttfs/stream and ttfs/hook + stream keys while changing from the CI/proxy-inclusive clock window to an in-deployment window, and the result schema is still version: 1. annotateWithBaseline() matches only backend/app/metric/scenario, so the PR comment's TTFS deltas compare incompatible measurements; historical entries also inherit the new footer. Please add a methodology/schema version to baseline and history matching, or suppress those comparisons until main has produced a compatible baseline.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Results now carry methodologyVersion: 2 and annotateWithBaseline() keys deltas on methodologyVersion/backend/app/metric/scenario, so old proxy-inclusive main baselines no longer match the in-deployment runs — the delta column stays blank until main has produced a v2 baseline, then v2-vs-v2 comparisons resume. Added a suppresses deltas when the baseline methodology version differs test. (The footer legend is a single global block describing the current methodology; per-entry footers for collapsed history are out of scope here.)

namespace: SL_STREAM_NAMESPACE,
});
const writer = writable.getWriter();
await new Promise((resolve) => setTimeout(resolve, SL_WRITE_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace the fixed delay with an explicit reader-ready barrier. A 750 ms sleep cannot guarantee the parallel reader step has started and is blocked: under scheduler delay or load, the writer can publish first and the reader will consume a retained chunk. That measures scheduling/catch-up rather than live pub/sub propagation. Please initiate reader.read(), signal readiness through a separate hook/stream, and let the writer stamp/write only after observing that signal.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Replaced the 750ms sleep with an explicit reader-ready barrier over a second stream: the reader initiates its SL read() (establishing the stream GET) then writes a marker to bench-sl-ready; the writer blocks reading that marker before it writes+stamps the SL chunk. So the write always lands on an already-attached reader (live propagation), not a retained chunk consumed by a late reader. The ready handshake tolerates either attach order (a retained marker is fine — we don't measure its latency), so no deadlock.

ttfs: {
name: 'TTFS',
description:
'time to first step body (in-deployment start() → first step body, deployment clocks)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the renderer test with the new output shape..github/scripts/render-benchmark-comment.test.js still expects the old table header without P10 (line 106) and the old TTFS wording (lines 111-114), so node --test .github/scripts/render-benchmark-comment.test.js fails. Please update both assertions and add coverage for the P10 cell.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated render-benchmark-comment.test.js: the header assertion now includes P10 (ms), the TTFS wording matcher is updated, and there's a new P10-cell assertion (| 412 | 357 | 398 🔴 |). node --test passes (11/11).

* Vercel only.
* @workflow/world-postgres, local filesystem otherwise. Because SL is now
* measured inside the workflow (not by a reader in this process), it no longer
* depends on `run.getReadable()` working across processes; CI still runs this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Keep the benchmark workflow documentation aligned..github/workflows/benchmarks.yml:98-100 still says SL requires run.getReadable() from the test process and therefore cannot use the local world's in-process streamer. This PR moves the reader inside the workflow, so that comment now contradicts the implementation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated the benchmarks.yml comment — SL is now measured inside the workflow (benchSlWorkflow's parallel reader/writer steps), so it no longer needs run.getReadable() from the test process; the runner only polls returnValue.

…st/doc sync
- [P1] Add methodologyVersion (=2) to results; renderer keys baseline deltas
on it so the in-deployment numbers aren't diffed against old proxy-inclusive
main baselines (deltas stay blank until main has a v2 baseline). Covered by
a new "suppresses deltas when methodology differs" test.
- [P1] Replace the fixed 750ms SL writer delay with an explicit reader-ready
barrier: the reader initiates its SL read then signals on a second stream;
the writer blocks on that signal before writing. Measures live propagation,
not a late reader catching up on a retained chunk.
- [P2] Update render-benchmark-comment.test.js for the P10 column header and
new TTFS wording, and add P10-cell coverage.
- [P3] Fix the stale benchmarks.yml comment claiming SL needs run.getReadable()
from the test process — SL is now measured inside the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for d53b055 (AI decision).

This commit refines a benchmark harness that only exists on main: stable still uses the older vitest bench setup (bench.bench.ts, the render-benchmarks action, and a completely different 97_bench.ts), and lacks packages/core/e2e/benchmark.test.ts, .github/scripts/render-benchmark-comment.mjs, and the /api/bench route this change modifies/depends on. The change is CI-only, builds on main-only benchmark infrastructure, and has no published-package impact (no changeset), so it should not be backported.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

d53b055a2b1aadf54afe59fd122cb1d960043bcb

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[ci] Run benchmarks in-deployment to avoid proxy overhead - #2967

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps
Jul 17, 2026
Merged

[ci] Run benchmarks in-deployment to avoid proxy overhead#2967
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The CI benchmark drove load from a GitHub Actions runner through the public api.vercel.com ingress. Datadog traces of real runs proved the reported latencies were dominated by that external path and the CI runner's clock, not the runtime:

  • TTFS was steps[0].start − clientStart(GHA), so it included the CI→ingress leg, GHA↔deployment clock skew, and the full VQS dispatch. The earlier revision of this PR anchored TTFS/WO on the server run_created timestamp and added a flat +80ms RTT estimate — proxy-independent, but a fudge, and it still measured the external-trigger dispatch rather than what a real in-Vercel caller experiences.
  • SL read the stream from the GHA runner, so it was dominated by the api.vercel.com read path.

Goal (hard requirement): no api.vercel.com / CI-runner timing may appear in any measured window.

Approach

Trigger every run from inside the deployment and measure entirely from deployment-side clocks.

  • New POST /api/bench route on the nextjs-turbopack workbench app resolves a bench workflow from the generated registry, stamps clientStart with the deployment's clock immediately before start(), and returns { runId, clientStart }. The GHA→route request (and its proxy path) sits beforeclientStart, so it is outside every measured window. Calling start() in-deployment also engages the runtime's in-process fast path (optimisticStart), so turbo TTFS now reflects the inline path and non-turbo (hook) reflects the dispatch path — both proxy-free.
  • benchSlWorkflow measures SL entirely on the deployment: a reader step and a writer step run in parallel on a dedicated namespaced stream. The writer waits a fixed delay so the reader is attached and blocked on the first chunk first (measuring live pub/sub propagation, not a warm read), then writes writtenAt; the reader stamps readAt. SL = readAt − writtenAt, both deployment step-body clocks, excluding the api.vercel.com read path.
  • benchmark.test.ts triggers each run via the route (with trusted-sources headers) and polls returnValue by runId. TTFS/WO derive from the in-deployment clientStart + step timings; SL from the return value. Removed runCreatedServerMs, the flat RTT estimate, and the wall-clock diagnostic. STSO was already deployment-clock only and is unchanged.

Result

All four metrics (TTFS, STSO, WO, SL) are now computed purely from Vercel-side timestamps. The only residual skew is intra-Vercel (step-runner instances in the same region), NTP-bounded and small relative to the measured values.

Latency targets are provisional — now that the proxy leg and the external-dispatch framing are gone, they will be re-tightened once a few in-deployment baselines land on main.

Notes

  • Touches an e2e test + CI script + workbench (route + workflow) only — no published package output, so no changeset is required (pnpm changeset status --since=main is clean).
  • Bench matrix is nextjs-turbopack only; the route lives in that app.

The CI benchmark drives load from a GitHub Actions runner through the public
api.vercel.com ingress, so TTFS/WO were dominated by that external path rather
than the runtime. Datadog traces of a real run show server-side "queue pickup →
step body" is only ~170ms, yet the client-anchored TTFS was ~1192ms; ~800ms of
that is VQS dispatch on an idle deployment plus the create request's inbound
leg through api.vercel.com.
Anchor TTFS/WO on the Vercel-assigned run_created timestamp (server-stamped
createdAt, read via world.events.list with a world.runs.get fallback) and end
on the deployment's step-body clock, making both metrics independent of the CI
runner's clock and its network path to api.vercel.com. Add a flat +80ms
(BENCH_RTT_OVERHEAD_MS) as an estimate of the client→ingress request overhead
the server anchor doesn't capture, and note it in the PR comment. A diagnostic
log prints server-anchored vs client wall-clock TTFS so the 80ms can be tuned.
Move WO off the stream scenarios (where a single step makes it algebraically
identical to TTFS) to a server-anchored whole-run aggregate on the sequential
scenario. STSO is unchanged (already measured between step bodies on the
deployment); SL stays client-observed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 22:39
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6c7ede8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6c7ede8 · Fri, 17 Jul 2026 21:31:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep136213041368 🔴1396 🔴1726 🔴30
TTFSstream133112711350 🔴1373 🔴1418 🔴30
TTFShook + stream159115231615 🔴1671 🔴1853 🔴30
STSO1020 steps (1-20)294224328 🔴427 🔴511 🔴19
STSO1020 steps (101-120)290230340 🔴394 🔴401 🔴19
STSO1020 steps (1001-1020)785692785 🔴850 🔴1328 🔴19
WO1020 steps5210135210135210135210135210131
SLstream latency159117175 🔴187 🔴332 🔴30
📜 Previous results (3)

db0bb85

Fri, 17 Jul 2026 20:47:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep138713241408 🔴1454 🔴1614 🔴30
TTFSstream1351 (+6.7%)12961375 🔴1396 🔴1513 🔴30
TTFShook + stream1652 (+24%)15371681 🔴1766 🔴1830 🔴30
STSO1020 steps (1-20)290 (-3.1%)210314 🔴345 🔴629 🔴19
STSO1020 steps (101-120)270 (-13%)226280 🔴377 🔴380 🔴19
STSO1020 steps (1001-1020)737 (+18%)654822 🔴868 🔴871 🔴19
WO1020 steps5201435201435201435201435201431
SLstream latency223166234 🔴387 🔴430 🔴30

63ddeb4

Fri, 17 Jul 2026 20:00:22 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream983 (-22%)1310 🔴1327 🔴1358 🔴30
TTFShook + stream1233 (-7.2%)1580 🔴1604 🔴1633 🔴30
STSO1020 steps (1-20)290 (-3.0%)296 🔴422 🔴554 🔴19
STSO1020 steps (101-120)334 (+8.3%)342 🔴474 🔴621 🔴19
STSO1020 steps (1001-1020)742 (+19%)802 🔴951 🔴1024 🔴19
WO1020 steps5242035242035242035242031
SLstream latency200211 🔴280 🔴362 🔴30

9b712d5

Fri, 17 Jul 2026 17:11:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream950 (-12%)1350 🔴1386 🔴1622 🔴30
TTFShook + stream1283 (-2.8%)1569 🔴1635 🔴1653 🔴30
STSO1020 steps (1-20)321 (+15%)356 🔴433 🔴523 🔴19
STSO1020 steps (101-120)467 (+23%)541 🔴596 🔴769 🔴19
STSO1020 steps (1001-1020)726 (-40%)774 🔴815 🔴833 🔴19
WO1020 steps5989295989295989295989291
SLstream3215 (+194%)5882 🔴6011 🔴6263 🔴30
SLhook + stream4549 (+164%)5862 🔴5909 🔴6016 🔴30

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

Cold starts on the low-traffic workbench /flow invocation (~65% of runs on
preview) are the reason TTFS p75+ is ~1.3s despite warm dispatch being
~300ms. Keep them in the numbers (they are real bursty-workload latency) and
make the warm floor visible instead of hiding it.
- Add a p10 column to every metric (computeStats + render table). p10 shows
the warm-start floor next to the cold-start-inflated upper percentiles.
- Add benchStepWorkflow: one trivial no-op step, no stream, turbo mode — the
cleanest TTFS scenario ("step"), alongside the existing streaming-step
turbo scenario.
- Document in the file header and PR comment footnote that cold starts are
intentionally included and p10 is the warm reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four inline findings from the benchmark methodology review.

@@ -483,22 +574,11 @@ describe('workflow benchmarks', () => {
results.map((r) => r.ttfsMs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Version the new methodology before comparing baselines. TTFS keeps the existing ttfs/stream and ttfs/hook + stream keys while changing from the CI/proxy-inclusive clock window to an in-deployment window, and the result schema is still version: 1. annotateWithBaseline() matches only backend/app/metric/scenario, so the PR comment's TTFS deltas compare incompatible measurements; historical entries also inherit the new footer. Please add a methodology/schema version to baseline and history matching, or suppress those comparisons until main has produced a compatible baseline.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Results now carry methodologyVersion: 2 and annotateWithBaseline() keys deltas on methodologyVersion/backend/app/metric/scenario, so old proxy-inclusive main baselines no longer match the in-deployment runs — the delta column stays blank until main has produced a v2 baseline, then v2-vs-v2 comparisons resume. Added a suppresses deltas when the baseline methodology version differs test. (The footer legend is a single global block describing the current methodology; per-entry footers for collapsed history are out of scope here.)

namespace: SL_STREAM_NAMESPACE,
});
const writer = writable.getWriter();
await new Promise((resolve) => setTimeout(resolve, SL_WRITE_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace the fixed delay with an explicit reader-ready barrier. A 750 ms sleep cannot guarantee the parallel reader step has started and is blocked: under scheduler delay or load, the writer can publish first and the reader will consume a retained chunk. That measures scheduling/catch-up rather than live pub/sub propagation. Please initiate reader.read(), signal readiness through a separate hook/stream, and let the writer stamp/write only after observing that signal.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Replaced the 750ms sleep with an explicit reader-ready barrier over a second stream: the reader initiates its SL read() (establishing the stream GET) then writes a marker to bench-sl-ready; the writer blocks reading that marker before it writes+stamps the SL chunk. So the write always lands on an already-attached reader (live propagation), not a retained chunk consumed by a late reader. The ready handshake tolerates either attach order (a retained marker is fine — we don't measure its latency), so no deadlock.

ttfs: {
name: 'TTFS',
description:
'time to first step body (in-deployment start() → first step body, deployment clocks)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the renderer test with the new output shape..github/scripts/render-benchmark-comment.test.js still expects the old table header without P10 (line 106) and the old TTFS wording (lines 111-114), so node --test .github/scripts/render-benchmark-comment.test.js fails. Please update both assertions and add coverage for the P10 cell.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated render-benchmark-comment.test.js: the header assertion now includes P10 (ms), the TTFS wording matcher is updated, and there's a new P10-cell assertion (| 412 | 357 | 398 🔴 |). node --test passes (11/11).

* Vercel only.
* @workflow/world-postgres, local filesystem otherwise. Because SL is now
* measured inside the workflow (not by a reader in this process), it no longer
* depends on `run.getReadable()` working across processes; CI still runs this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Keep the benchmark workflow documentation aligned..github/workflows/benchmarks.yml:98-100 still says SL requires run.getReadable() from the test process and therefore cannot use the local world's in-process streamer. This PR moves the reader inside the workflow, so that comment now contradicts the implementation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated the benchmarks.yml comment — SL is now measured inside the workflow (benchSlWorkflow's parallel reader/writer steps), so it no longer needs run.getReadable() from the test process; the runner only polls returnValue.

…st/doc sync
- [P1] Add methodologyVersion (=2) to results; renderer keys baseline deltas
on it so the in-deployment numbers aren't diffed against old proxy-inclusive
main baselines (deltas stay blank until main has a v2 baseline). Covered by
a new "suppresses deltas when methodology differs" test.
- [P1] Replace the fixed 750ms SL writer delay with an explicit reader-ready
barrier: the reader initiates its SL read then signals on a second stream;
the writer blocks on that signal before writing. Measures live propagation,
not a late reader catching up on a retained chunk.
- [P2] Update render-benchmark-comment.test.js for the P10 column header and
new TTFS wording, and add P10-cell coverage.
- [P3] Fix the stale benchmarks.yml comment claiming SL needs run.getReadable()
from the test process — SL is now measured inside the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for d53b055 (AI decision).

This commit refines a benchmark harness that only exists on main: stable still uses the older vitest bench setup (bench.bench.ts, the render-benchmarks action, and a completely different 97_bench.ts), and lacks packages/core/e2e/benchmark.test.ts, .github/scripts/render-benchmark-comment.mjs, and the /api/bench route this change modifies/depends on. The change is CI-only, builds on main-only benchmark infrastructure, and has no published-package impact (no changeset), so it should not be backported.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

d53b055a2b1aadf54afe59fd122cb1d960043bcb

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[ci] Run benchmarks in-deployment to avoid proxy overhead - #2967

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps
Jul 17, 2026
Merged

[ci] Run benchmarks in-deployment to avoid proxy overhead#2967
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The CI benchmark drove load from a GitHub Actions runner through the public api.vercel.com ingress. Datadog traces of real runs proved the reported latencies were dominated by that external path and the CI runner's clock, not the runtime:

  • TTFS was steps[0].start − clientStart(GHA), so it included the CI→ingress leg, GHA↔deployment clock skew, and the full VQS dispatch. The earlier revision of this PR anchored TTFS/WO on the server run_created timestamp and added a flat +80ms RTT estimate — proxy-independent, but a fudge, and it still measured the external-trigger dispatch rather than what a real in-Vercel caller experiences.
  • SL read the stream from the GHA runner, so it was dominated by the api.vercel.com read path.

Goal (hard requirement): no api.vercel.com / CI-runner timing may appear in any measured window.

Approach

Trigger every run from inside the deployment and measure entirely from deployment-side clocks.

  • New POST /api/bench route on the nextjs-turbopack workbench app resolves a bench workflow from the generated registry, stamps clientStart with the deployment's clock immediately before start(), and returns { runId, clientStart }. The GHA→route request (and its proxy path) sits beforeclientStart, so it is outside every measured window. Calling start() in-deployment also engages the runtime's in-process fast path (optimisticStart), so turbo TTFS now reflects the inline path and non-turbo (hook) reflects the dispatch path — both proxy-free.
  • benchSlWorkflow measures SL entirely on the deployment: a reader step and a writer step run in parallel on a dedicated namespaced stream. The writer waits a fixed delay so the reader is attached and blocked on the first chunk first (measuring live pub/sub propagation, not a warm read), then writes writtenAt; the reader stamps readAt. SL = readAt − writtenAt, both deployment step-body clocks, excluding the api.vercel.com read path.
  • benchmark.test.ts triggers each run via the route (with trusted-sources headers) and polls returnValue by runId. TTFS/WO derive from the in-deployment clientStart + step timings; SL from the return value. Removed runCreatedServerMs, the flat RTT estimate, and the wall-clock diagnostic. STSO was already deployment-clock only and is unchanged.

Result

All four metrics (TTFS, STSO, WO, SL) are now computed purely from Vercel-side timestamps. The only residual skew is intra-Vercel (step-runner instances in the same region), NTP-bounded and small relative to the measured values.

Latency targets are provisional — now that the proxy leg and the external-dispatch framing are gone, they will be re-tightened once a few in-deployment baselines land on main.

Notes

  • Touches an e2e test + CI script + workbench (route + workflow) only — no published package output, so no changeset is required (pnpm changeset status --since=main is clean).
  • Bench matrix is nextjs-turbopack only; the route lives in that app.

The CI benchmark drives load from a GitHub Actions runner through the public
api.vercel.com ingress, so TTFS/WO were dominated by that external path rather
than the runtime. Datadog traces of a real run show server-side "queue pickup →
step body" is only ~170ms, yet the client-anchored TTFS was ~1192ms; ~800ms of
that is VQS dispatch on an idle deployment plus the create request's inbound
leg through api.vercel.com.
Anchor TTFS/WO on the Vercel-assigned run_created timestamp (server-stamped
createdAt, read via world.events.list with a world.runs.get fallback) and end
on the deployment's step-body clock, making both metrics independent of the CI
runner's clock and its network path to api.vercel.com. Add a flat +80ms
(BENCH_RTT_OVERHEAD_MS) as an estimate of the client→ingress request overhead
the server anchor doesn't capture, and note it in the PR comment. A diagnostic
log prints server-anchored vs client wall-clock TTFS so the 80ms can be tuned.
Move WO off the stream scenarios (where a single step makes it algebraically
identical to TTFS) to a server-anchored whole-run aggregate on the sequential
scenario. STSO is unchanged (already measured between step bodies on the
deployment); SL stays client-observed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 22:39
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6c7ede8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6c7ede8 · Fri, 17 Jul 2026 21:31:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep136213041368 🔴1396 🔴1726 🔴30
TTFSstream133112711350 🔴1373 🔴1418 🔴30
TTFShook + stream159115231615 🔴1671 🔴1853 🔴30
STSO1020 steps (1-20)294224328 🔴427 🔴511 🔴19
STSO1020 steps (101-120)290230340 🔴394 🔴401 🔴19
STSO1020 steps (1001-1020)785692785 🔴850 🔴1328 🔴19
WO1020 steps5210135210135210135210135210131
SLstream latency159117175 🔴187 🔴332 🔴30
📜 Previous results (3)

db0bb85

Fri, 17 Jul 2026 20:47:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep138713241408 🔴1454 🔴1614 🔴30
TTFSstream1351 (+6.7%)12961375 🔴1396 🔴1513 🔴30
TTFShook + stream1652 (+24%)15371681 🔴1766 🔴1830 🔴30
STSO1020 steps (1-20)290 (-3.1%)210314 🔴345 🔴629 🔴19
STSO1020 steps (101-120)270 (-13%)226280 🔴377 🔴380 🔴19
STSO1020 steps (1001-1020)737 (+18%)654822 🔴868 🔴871 🔴19
WO1020 steps5201435201435201435201435201431
SLstream latency223166234 🔴387 🔴430 🔴30

63ddeb4

Fri, 17 Jul 2026 20:00:22 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream983 (-22%)1310 🔴1327 🔴1358 🔴30
TTFShook + stream1233 (-7.2%)1580 🔴1604 🔴1633 🔴30
STSO1020 steps (1-20)290 (-3.0%)296 🔴422 🔴554 🔴19
STSO1020 steps (101-120)334 (+8.3%)342 🔴474 🔴621 🔴19
STSO1020 steps (1001-1020)742 (+19%)802 🔴951 🔴1024 🔴19
WO1020 steps5242035242035242035242031
SLstream latency200211 🔴280 🔴362 🔴30

9b712d5

Fri, 17 Jul 2026 17:11:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream950 (-12%)1350 🔴1386 🔴1622 🔴30
TTFShook + stream1283 (-2.8%)1569 🔴1635 🔴1653 🔴30
STSO1020 steps (1-20)321 (+15%)356 🔴433 🔴523 🔴19
STSO1020 steps (101-120)467 (+23%)541 🔴596 🔴769 🔴19
STSO1020 steps (1001-1020)726 (-40%)774 🔴815 🔴833 🔴19
WO1020 steps5989295989295989295989291
SLstream3215 (+194%)5882 🔴6011 🔴6263 🔴30
SLhook + stream4549 (+164%)5862 🔴5909 🔴6016 🔴30

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

Cold starts on the low-traffic workbench /flow invocation (~65% of runs on
preview) are the reason TTFS p75+ is ~1.3s despite warm dispatch being
~300ms. Keep them in the numbers (they are real bursty-workload latency) and
make the warm floor visible instead of hiding it.
- Add a p10 column to every metric (computeStats + render table). p10 shows
the warm-start floor next to the cold-start-inflated upper percentiles.
- Add benchStepWorkflow: one trivial no-op step, no stream, turbo mode — the
cleanest TTFS scenario ("step"), alongside the existing streaming-step
turbo scenario.
- Document in the file header and PR comment footnote that cold starts are
intentionally included and p10 is the warm reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four inline findings from the benchmark methodology review.

@@ -483,22 +574,11 @@ describe('workflow benchmarks', () => {
results.map((r) => r.ttfsMs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Version the new methodology before comparing baselines. TTFS keeps the existing ttfs/stream and ttfs/hook + stream keys while changing from the CI/proxy-inclusive clock window to an in-deployment window, and the result schema is still version: 1. annotateWithBaseline() matches only backend/app/metric/scenario, so the PR comment's TTFS deltas compare incompatible measurements; historical entries also inherit the new footer. Please add a methodology/schema version to baseline and history matching, or suppress those comparisons until main has produced a compatible baseline.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Results now carry methodologyVersion: 2 and annotateWithBaseline() keys deltas on methodologyVersion/backend/app/metric/scenario, so old proxy-inclusive main baselines no longer match the in-deployment runs — the delta column stays blank until main has produced a v2 baseline, then v2-vs-v2 comparisons resume. Added a suppresses deltas when the baseline methodology version differs test. (The footer legend is a single global block describing the current methodology; per-entry footers for collapsed history are out of scope here.)

namespace: SL_STREAM_NAMESPACE,
});
const writer = writable.getWriter();
await new Promise((resolve) => setTimeout(resolve, SL_WRITE_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace the fixed delay with an explicit reader-ready barrier. A 750 ms sleep cannot guarantee the parallel reader step has started and is blocked: under scheduler delay or load, the writer can publish first and the reader will consume a retained chunk. That measures scheduling/catch-up rather than live pub/sub propagation. Please initiate reader.read(), signal readiness through a separate hook/stream, and let the writer stamp/write only after observing that signal.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Replaced the 750ms sleep with an explicit reader-ready barrier over a second stream: the reader initiates its SL read() (establishing the stream GET) then writes a marker to bench-sl-ready; the writer blocks reading that marker before it writes+stamps the SL chunk. So the write always lands on an already-attached reader (live propagation), not a retained chunk consumed by a late reader. The ready handshake tolerates either attach order (a retained marker is fine — we don't measure its latency), so no deadlock.

ttfs: {
name: 'TTFS',
description:
'time to first step body (in-deployment start() → first step body, deployment clocks)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the renderer test with the new output shape..github/scripts/render-benchmark-comment.test.js still expects the old table header without P10 (line 106) and the old TTFS wording (lines 111-114), so node --test .github/scripts/render-benchmark-comment.test.js fails. Please update both assertions and add coverage for the P10 cell.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated render-benchmark-comment.test.js: the header assertion now includes P10 (ms), the TTFS wording matcher is updated, and there's a new P10-cell assertion (| 412 | 357 | 398 🔴 |). node --test passes (11/11).

* Vercel only.
* @workflow/world-postgres, local filesystem otherwise. Because SL is now
* measured inside the workflow (not by a reader in this process), it no longer
* depends on `run.getReadable()` working across processes; CI still runs this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Keep the benchmark workflow documentation aligned..github/workflows/benchmarks.yml:98-100 still says SL requires run.getReadable() from the test process and therefore cannot use the local world's in-process streamer. This PR moves the reader inside the workflow, so that comment now contradicts the implementation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated the benchmarks.yml comment — SL is now measured inside the workflow (benchSlWorkflow's parallel reader/writer steps), so it no longer needs run.getReadable() from the test process; the runner only polls returnValue.

…st/doc sync
- [P1] Add methodologyVersion (=2) to results; renderer keys baseline deltas
on it so the in-deployment numbers aren't diffed against old proxy-inclusive
main baselines (deltas stay blank until main has a v2 baseline). Covered by
a new "suppresses deltas when methodology differs" test.
- [P1] Replace the fixed 750ms SL writer delay with an explicit reader-ready
barrier: the reader initiates its SL read then signals on a second stream;
the writer blocks on that signal before writing. Measures live propagation,
not a late reader catching up on a retained chunk.
- [P2] Update render-benchmark-comment.test.js for the P10 column header and
new TTFS wording, and add P10-cell coverage.
- [P3] Fix the stale benchmarks.yml comment claiming SL needs run.getReadable()
from the test process — SL is now measured inside the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for d53b055 (AI decision).

This commit refines a benchmark harness that only exists on main: stable still uses the older vitest bench setup (bench.bench.ts, the render-benchmarks action, and a completely different 97_bench.ts), and lacks packages/core/e2e/benchmark.test.ts, .github/scripts/render-benchmark-comment.mjs, and the /api/bench route this change modifies/depends on. The change is CI-only, builds on main-only benchmark infrastructure, and has no published-package impact (no changeset), so it should not be backported.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

d53b055a2b1aadf54afe59fd122cb1d960043bcb

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[ci] Run benchmarks in-deployment to avoid proxy overhead - #2967

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps
Jul 17, 2026
Merged

[ci] Run benchmarks in-deployment to avoid proxy overhead#2967
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The CI benchmark drove load from a GitHub Actions runner through the public api.vercel.com ingress. Datadog traces of real runs proved the reported latencies were dominated by that external path and the CI runner's clock, not the runtime:

  • TTFS was steps[0].start − clientStart(GHA), so it included the CI→ingress leg, GHA↔deployment clock skew, and the full VQS dispatch. The earlier revision of this PR anchored TTFS/WO on the server run_created timestamp and added a flat +80ms RTT estimate — proxy-independent, but a fudge, and it still measured the external-trigger dispatch rather than what a real in-Vercel caller experiences.
  • SL read the stream from the GHA runner, so it was dominated by the api.vercel.com read path.

Goal (hard requirement): no api.vercel.com / CI-runner timing may appear in any measured window.

Approach

Trigger every run from inside the deployment and measure entirely from deployment-side clocks.

  • New POST /api/bench route on the nextjs-turbopack workbench app resolves a bench workflow from the generated registry, stamps clientStart with the deployment's clock immediately before start(), and returns { runId, clientStart }. The GHA→route request (and its proxy path) sits beforeclientStart, so it is outside every measured window. Calling start() in-deployment also engages the runtime's in-process fast path (optimisticStart), so turbo TTFS now reflects the inline path and non-turbo (hook) reflects the dispatch path — both proxy-free.
  • benchSlWorkflow measures SL entirely on the deployment: a reader step and a writer step run in parallel on a dedicated namespaced stream. The writer waits a fixed delay so the reader is attached and blocked on the first chunk first (measuring live pub/sub propagation, not a warm read), then writes writtenAt; the reader stamps readAt. SL = readAt − writtenAt, both deployment step-body clocks, excluding the api.vercel.com read path.
  • benchmark.test.ts triggers each run via the route (with trusted-sources headers) and polls returnValue by runId. TTFS/WO derive from the in-deployment clientStart + step timings; SL from the return value. Removed runCreatedServerMs, the flat RTT estimate, and the wall-clock diagnostic. STSO was already deployment-clock only and is unchanged.

Result

All four metrics (TTFS, STSO, WO, SL) are now computed purely from Vercel-side timestamps. The only residual skew is intra-Vercel (step-runner instances in the same region), NTP-bounded and small relative to the measured values.

Latency targets are provisional — now that the proxy leg and the external-dispatch framing are gone, they will be re-tightened once a few in-deployment baselines land on main.

Notes

  • Touches an e2e test + CI script + workbench (route + workflow) only — no published package output, so no changeset is required (pnpm changeset status --since=main is clean).
  • Bench matrix is nextjs-turbopack only; the route lives in that app.

The CI benchmark drives load from a GitHub Actions runner through the public
api.vercel.com ingress, so TTFS/WO were dominated by that external path rather
than the runtime. Datadog traces of a real run show server-side "queue pickup →
step body" is only ~170ms, yet the client-anchored TTFS was ~1192ms; ~800ms of
that is VQS dispatch on an idle deployment plus the create request's inbound
leg through api.vercel.com.
Anchor TTFS/WO on the Vercel-assigned run_created timestamp (server-stamped
createdAt, read via world.events.list with a world.runs.get fallback) and end
on the deployment's step-body clock, making both metrics independent of the CI
runner's clock and its network path to api.vercel.com. Add a flat +80ms
(BENCH_RTT_OVERHEAD_MS) as an estimate of the client→ingress request overhead
the server anchor doesn't capture, and note it in the PR comment. A diagnostic
log prints server-anchored vs client wall-clock TTFS so the 80ms can be tuned.
Move WO off the stream scenarios (where a single step makes it algebraically
identical to TTFS) to a server-anchored whole-run aggregate on the sequential
scenario. STSO is unchanged (already measured between step bodies on the
deployment); SL stays client-observed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 22:39
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6c7ede8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6c7ede8 · Fri, 17 Jul 2026 21:31:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep136213041368 🔴1396 🔴1726 🔴30
TTFSstream133112711350 🔴1373 🔴1418 🔴30
TTFShook + stream159115231615 🔴1671 🔴1853 🔴30
STSO1020 steps (1-20)294224328 🔴427 🔴511 🔴19
STSO1020 steps (101-120)290230340 🔴394 🔴401 🔴19
STSO1020 steps (1001-1020)785692785 🔴850 🔴1328 🔴19
WO1020 steps5210135210135210135210135210131
SLstream latency159117175 🔴187 🔴332 🔴30
📜 Previous results (3)

db0bb85

Fri, 17 Jul 2026 20:47:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep138713241408 🔴1454 🔴1614 🔴30
TTFSstream1351 (+6.7%)12961375 🔴1396 🔴1513 🔴30
TTFShook + stream1652 (+24%)15371681 🔴1766 🔴1830 🔴30
STSO1020 steps (1-20)290 (-3.1%)210314 🔴345 🔴629 🔴19
STSO1020 steps (101-120)270 (-13%)226280 🔴377 🔴380 🔴19
STSO1020 steps (1001-1020)737 (+18%)654822 🔴868 🔴871 🔴19
WO1020 steps5201435201435201435201435201431
SLstream latency223166234 🔴387 🔴430 🔴30

63ddeb4

Fri, 17 Jul 2026 20:00:22 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream983 (-22%)1310 🔴1327 🔴1358 🔴30
TTFShook + stream1233 (-7.2%)1580 🔴1604 🔴1633 🔴30
STSO1020 steps (1-20)290 (-3.0%)296 🔴422 🔴554 🔴19
STSO1020 steps (101-120)334 (+8.3%)342 🔴474 🔴621 🔴19
STSO1020 steps (1001-1020)742 (+19%)802 🔴951 🔴1024 🔴19
WO1020 steps5242035242035242035242031
SLstream latency200211 🔴280 🔴362 🔴30

9b712d5

Fri, 17 Jul 2026 17:11:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream950 (-12%)1350 🔴1386 🔴1622 🔴30
TTFShook + stream1283 (-2.8%)1569 🔴1635 🔴1653 🔴30
STSO1020 steps (1-20)321 (+15%)356 🔴433 🔴523 🔴19
STSO1020 steps (101-120)467 (+23%)541 🔴596 🔴769 🔴19
STSO1020 steps (1001-1020)726 (-40%)774 🔴815 🔴833 🔴19
WO1020 steps5989295989295989295989291
SLstream3215 (+194%)5882 🔴6011 🔴6263 🔴30
SLhook + stream4549 (+164%)5862 🔴5909 🔴6016 🔴30

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

Cold starts on the low-traffic workbench /flow invocation (~65% of runs on
preview) are the reason TTFS p75+ is ~1.3s despite warm dispatch being
~300ms. Keep them in the numbers (they are real bursty-workload latency) and
make the warm floor visible instead of hiding it.
- Add a p10 column to every metric (computeStats + render table). p10 shows
the warm-start floor next to the cold-start-inflated upper percentiles.
- Add benchStepWorkflow: one trivial no-op step, no stream, turbo mode — the
cleanest TTFS scenario ("step"), alongside the existing streaming-step
turbo scenario.
- Document in the file header and PR comment footnote that cold starts are
intentionally included and p10 is the warm reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four inline findings from the benchmark methodology review.

@@ -483,22 +574,11 @@ describe('workflow benchmarks', () => {
results.map((r) => r.ttfsMs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Version the new methodology before comparing baselines. TTFS keeps the existing ttfs/stream and ttfs/hook + stream keys while changing from the CI/proxy-inclusive clock window to an in-deployment window, and the result schema is still version: 1. annotateWithBaseline() matches only backend/app/metric/scenario, so the PR comment's TTFS deltas compare incompatible measurements; historical entries also inherit the new footer. Please add a methodology/schema version to baseline and history matching, or suppress those comparisons until main has produced a compatible baseline.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Results now carry methodologyVersion: 2 and annotateWithBaseline() keys deltas on methodologyVersion/backend/app/metric/scenario, so old proxy-inclusive main baselines no longer match the in-deployment runs — the delta column stays blank until main has produced a v2 baseline, then v2-vs-v2 comparisons resume. Added a suppresses deltas when the baseline methodology version differs test. (The footer legend is a single global block describing the current methodology; per-entry footers for collapsed history are out of scope here.)

namespace: SL_STREAM_NAMESPACE,
});
const writer = writable.getWriter();
await new Promise((resolve) => setTimeout(resolve, SL_WRITE_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace the fixed delay with an explicit reader-ready barrier. A 750 ms sleep cannot guarantee the parallel reader step has started and is blocked: under scheduler delay or load, the writer can publish first and the reader will consume a retained chunk. That measures scheduling/catch-up rather than live pub/sub propagation. Please initiate reader.read(), signal readiness through a separate hook/stream, and let the writer stamp/write only after observing that signal.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Replaced the 750ms sleep with an explicit reader-ready barrier over a second stream: the reader initiates its SL read() (establishing the stream GET) then writes a marker to bench-sl-ready; the writer blocks reading that marker before it writes+stamps the SL chunk. So the write always lands on an already-attached reader (live propagation), not a retained chunk consumed by a late reader. The ready handshake tolerates either attach order (a retained marker is fine — we don't measure its latency), so no deadlock.

ttfs: {
name: 'TTFS',
description:
'time to first step body (in-deployment start() → first step body, deployment clocks)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the renderer test with the new output shape..github/scripts/render-benchmark-comment.test.js still expects the old table header without P10 (line 106) and the old TTFS wording (lines 111-114), so node --test .github/scripts/render-benchmark-comment.test.js fails. Please update both assertions and add coverage for the P10 cell.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated render-benchmark-comment.test.js: the header assertion now includes P10 (ms), the TTFS wording matcher is updated, and there's a new P10-cell assertion (| 412 | 357 | 398 🔴 |). node --test passes (11/11).

* Vercel only.
* @workflow/world-postgres, local filesystem otherwise. Because SL is now
* measured inside the workflow (not by a reader in this process), it no longer
* depends on `run.getReadable()` working across processes; CI still runs this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Keep the benchmark workflow documentation aligned..github/workflows/benchmarks.yml:98-100 still says SL requires run.getReadable() from the test process and therefore cannot use the local world's in-process streamer. This PR moves the reader inside the workflow, so that comment now contradicts the implementation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated the benchmarks.yml comment — SL is now measured inside the workflow (benchSlWorkflow's parallel reader/writer steps), so it no longer needs run.getReadable() from the test process; the runner only polls returnValue.

…st/doc sync
- [P1] Add methodologyVersion (=2) to results; renderer keys baseline deltas
on it so the in-deployment numbers aren't diffed against old proxy-inclusive
main baselines (deltas stay blank until main has a v2 baseline). Covered by
a new "suppresses deltas when methodology differs" test.
- [P1] Replace the fixed 750ms SL writer delay with an explicit reader-ready
barrier: the reader initiates its SL read then signals on a second stream;
the writer blocks on that signal before writing. Measures live propagation,
not a late reader catching up on a retained chunk.
- [P2] Update render-benchmark-comment.test.js for the P10 column header and
new TTFS wording, and add P10-cell coverage.
- [P3] Fix the stale benchmarks.yml comment claiming SL needs run.getReadable()
from the test process — SL is now measured inside the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for d53b055 (AI decision).

This commit refines a benchmark harness that only exists on main: stable still uses the older vitest bench setup (bench.bench.ts, the render-benchmarks action, and a completely different 97_bench.ts), and lacks packages/core/e2e/benchmark.test.ts, .github/scripts/render-benchmark-comment.mjs, and the /api/bench route this change modifies/depends on. The change is CI-only, builds on main-only benchmark infrastructure, and has no published-package impact (no changeset), so it should not be backported.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

d53b055a2b1aadf54afe59fd122cb1d960043bcb

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[ci] Run benchmarks in-deployment to avoid proxy overhead - #2967

Merged
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps
Jul 17, 2026
Merged

[ci] Run benchmarks in-deployment to avoid proxy overhead#2967
VaguelySerious merged 6 commits into
mainfrom
peter/bench-server-timestamps

Conversation

@VaguelySerious

@VaguelySeriousVaguelySerious commented Jul 16, 2026

Copy link
Copy Markdown
Member

Problem

The CI benchmark drove load from a GitHub Actions runner through the public api.vercel.com ingress. Datadog traces of real runs proved the reported latencies were dominated by that external path and the CI runner's clock, not the runtime:

  • TTFS was steps[0].start − clientStart(GHA), so it included the CI→ingress leg, GHA↔deployment clock skew, and the full VQS dispatch. The earlier revision of this PR anchored TTFS/WO on the server run_created timestamp and added a flat +80ms RTT estimate — proxy-independent, but a fudge, and it still measured the external-trigger dispatch rather than what a real in-Vercel caller experiences.
  • SL read the stream from the GHA runner, so it was dominated by the api.vercel.com read path.

Goal (hard requirement): no api.vercel.com / CI-runner timing may appear in any measured window.

Approach

Trigger every run from inside the deployment and measure entirely from deployment-side clocks.

  • New POST /api/bench route on the nextjs-turbopack workbench app resolves a bench workflow from the generated registry, stamps clientStart with the deployment's clock immediately before start(), and returns { runId, clientStart }. The GHA→route request (and its proxy path) sits beforeclientStart, so it is outside every measured window. Calling start() in-deployment also engages the runtime's in-process fast path (optimisticStart), so turbo TTFS now reflects the inline path and non-turbo (hook) reflects the dispatch path — both proxy-free.
  • benchSlWorkflow measures SL entirely on the deployment: a reader step and a writer step run in parallel on a dedicated namespaced stream. The writer waits a fixed delay so the reader is attached and blocked on the first chunk first (measuring live pub/sub propagation, not a warm read), then writes writtenAt; the reader stamps readAt. SL = readAt − writtenAt, both deployment step-body clocks, excluding the api.vercel.com read path.
  • benchmark.test.ts triggers each run via the route (with trusted-sources headers) and polls returnValue by runId. TTFS/WO derive from the in-deployment clientStart + step timings; SL from the return value. Removed runCreatedServerMs, the flat RTT estimate, and the wall-clock diagnostic. STSO was already deployment-clock only and is unchanged.

Result

All four metrics (TTFS, STSO, WO, SL) are now computed purely from Vercel-side timestamps. The only residual skew is intra-Vercel (step-runner instances in the same region), NTP-bounded and small relative to the measured values.

Latency targets are provisional — now that the proxy leg and the external-dispatch framing are gone, they will be re-tightened once a few in-deployment baselines land on main.

Notes

  • Touches an e2e test + CI script + workbench (route + workflow) only — no published package output, so no changeset is required (pnpm changeset status --since=main is clean).
  • Bench matrix is nextjs-turbopack only; the route lives in that app.

The CI benchmark drives load from a GitHub Actions runner through the public
api.vercel.com ingress, so TTFS/WO were dominated by that external path rather
than the runtime. Datadog traces of a real run show server-side "queue pickup →
step body" is only ~170ms, yet the client-anchored TTFS was ~1192ms; ~800ms of
that is VQS dispatch on an idle deployment plus the create request's inbound
leg through api.vercel.com.
Anchor TTFS/WO on the Vercel-assigned run_created timestamp (server-stamped
createdAt, read via world.events.list with a world.runs.get fallback) and end
on the deployment's step-body clock, making both metrics independent of the CI
runner's clock and its network path to api.vercel.com. Add a flat +80ms
(BENCH_RTT_OVERHEAD_MS) as an estimate of the client→ingress request overhead
the server anchor doesn't capture, and note it in the PR comment. A diagnostic
log prints server-anchored vs client wall-clock TTFS so the 80ms can be tuned.
Move WO off the stream scenarios (where a single step makes it algebraically
identical to TTFS) to a server-anchored whole-run aggregate on the sequential
scenario. STSO is unchanged (already measured between step bodies on the
deployment); SL stays client-observed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@VaguelySerious
VaguelySerious requested review from a team and ijjk as code ownersJuly 16, 2026 22:39
@changeset-bot

changeset-botBot commented Jul 16, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 6c7ede8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions

github-actionsBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production145302301683
✅ 💻 Local Development161702191836
✅ 📦 Local Production161702191836
✅ 🐘 Local Postgres161702191836
✅ 🪟 Windows15300153
✅ 📋 Other89401771071
✅ vercel-multi-region270027
Total7378010648442

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro126027
✅ example126027
✅ express126027
✅ fastify126027
✅ hono126027
✅ nextjs-turbopack15003
✅ nextjs-webpack15003
✅ nitro126027
✅ nuxt126027
✅ sveltekit14508
✅ vite126027
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable128025
✅ express-stable128025
✅ fastify-stable128025
✅ hono-stable128025
✅ nextjs-turbopack-canary134019
✅ nextjs-turbopack-stable15300
✅ nextjs-webpack-canary134019
✅ nextjs-webpack-stable15300
✅ nitro-stable128025
✅ nuxt-stable128025
✅ sveltekit-stable14706
✅ vite-stable128025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack15300
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable128025
✅ e2e-local-dev-tanstack-start-128025
✅ e2e-local-postgres-nest-stable128025
✅ e2e-local-postgres-tanstack-start-128025
✅ e2e-local-prod-nest-stable128025
✅ e2e-local-prod-tanstack-start-128025
✅ e2e-vercel-prod-tanstack-start126027
✅ vercel-multi-region
AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@vercel

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6c7ede8 · Fri, 17 Jul 2026 21:31:05 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep136213041368 🔴1396 🔴1726 🔴30
TTFSstream133112711350 🔴1373 🔴1418 🔴30
TTFShook + stream159115231615 🔴1671 🔴1853 🔴30
STSO1020 steps (1-20)294224328 🔴427 🔴511 🔴19
STSO1020 steps (101-120)290230340 🔴394 🔴401 🔴19
STSO1020 steps (1001-1020)785692785 🔴850 🔴1328 🔴19
WO1020 steps5210135210135210135210135210131
SLstream latency159117175 🔴187 🔴332 🔴30
📜 Previous results (3)

db0bb85

Fri, 17 Jul 2026 20:47:55 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep138713241408 🔴1454 🔴1614 🔴30
TTFSstream1351 (+6.7%)12961375 🔴1396 🔴1513 🔴30
TTFShook + stream1652 (+24%)15371681 🔴1766 🔴1830 🔴30
STSO1020 steps (1-20)290 (-3.1%)210314 🔴345 🔴629 🔴19
STSO1020 steps (101-120)270 (-13%)226280 🔴377 🔴380 🔴19
STSO1020 steps (1001-1020)737 (+18%)654822 🔴868 🔴871 🔴19
WO1020 steps5201435201435201435201435201431
SLstream latency223166234 🔴387 🔴430 🔴30

63ddeb4

Fri, 17 Jul 2026 20:00:22 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream983 (-22%)1310 🔴1327 🔴1358 🔴30
TTFShook + stream1233 (-7.2%)1580 🔴1604 🔴1633 🔴30
STSO1020 steps (1-20)290 (-3.0%)296 🔴422 🔴554 🔴19
STSO1020 steps (101-120)334 (+8.3%)342 🔴474 🔴621 🔴19
STSO1020 steps (1001-1020)742 (+19%)802 🔴951 🔴1024 🔴19
WO1020 steps5242035242035242035242031
SLstream latency200211 🔴280 🔴362 🔴30

9b712d5

Fri, 17 Jul 2026 17:11:24 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioAvg (ms)P10 (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstream950 (-12%)1350 🔴1386 🔴1622 🔴30
TTFShook + stream1283 (-2.8%)1569 🔴1635 🔴1653 🔴30
STSO1020 steps (1-20)321 (+15%)356 🔴433 🔴523 🔴19
STSO1020 steps (101-120)467 (+23%)541 🔴596 🔴769 🔴19
STSO1020 steps (1001-1020)726 (-40%)774 🔴815 🔴833 🔴19
WO1020 steps5989295989295989295989291
SLstream3215 (+194%)5882 🔴6011 🔴6263 🔴30
SLhook + stream4549 (+164%)5862 🔴5909 🔴6016 🔴30

Metrics — TTFS: time to first step body (in-deployment start() → first step body, deployment clocks) · STSO: step-to-step overhead (gap between consecutive step bodies) · WO: workflow overhead (whole-run time outside step bodies, in-deployment anchored) · SL: stream latency (in-deployment write → read propagation, readAt - writtenAt)

Scenarios — step: one trivial no-op step, no stream; no hooks, so the run stays in turbo mode (in-process fast path) · stream: one streaming step; no hooks, so the run stays in turbo mode (in-process fast path) · hook + stream: registers a hook before one step, which exits turbo mode (dispatch path) · 1020 steps: 1020 trivial sequential steps; STSO is measured between consecutive steps in the given step ranges, and WO is the whole-run overhead outside step bodies · stream latency: parallel reader/writer steps on a dedicated stream; SL is the in-deployment write->read propagation (readAt - writtenAt)

🟢/🔴 mark percentiles within/above target. Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · STSO (1-20) 20/30/60 · STSO (101-120) 30/45/90 · STSO (1001-1020) 40/60/120

All metrics are measured from deployment-side timestamps only. Runs are triggered by an in-deployment route that stamps the anchor (clientStart) right before start(), so the CI runner’s request and its path through api.vercel.com sit outside every measured window. TTFS = in-deployment start() → first step body (turbo uses the in-process fast path, non-turbo the dispatch path), and includes the VQS dispatch hop plus any /flow cold start. STSO/WO are measured between step bodies on the deployment. SL is measured inside the workflow (parallel reader/writer steps), so it no longer includes the api.vercel.com read path.

Cold starts are kept in the numbers on purpose — they are part of real bursty-workload latency. The workbench deployment cold-starts the /flow invocation for a large fraction of runs, inflating P75+; the P10 column shows the warm-start floor for comparison.

Cold starts on the low-traffic workbench /flow invocation (~65% of runs on
preview) are the reason TTFS p75+ is ~1.3s despite warm dispatch being
~300ms. Keep them in the numbers (they are real bursty-workload latency) and
make the warm floor visible instead of hiding it.
- Add a p10 column to every metric (computeStats + render table). p10 shows
the warm-start floor next to the cold-start-inflated upper percentiles.
- Add benchStepWorkflow: one trivial no-op step, no stream, turbo mode — the
cleanest TTFS scenario ("step"), alongside the existing streaming-step
turbo scenario.
- Document in the file header and PR comment footnote that cold starts are
intentionally included and p10 is the warm reference.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@karthikscale3karthikscale3 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Four inline findings from the benchmark methodology review.

@@ -483,22 +574,11 @@ describe('workflow benchmarks', () => {
results.map((r) => r.ttfsMs),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Version the new methodology before comparing baselines. TTFS keeps the existing ttfs/stream and ttfs/hook + stream keys while changing from the CI/proxy-inclusive clock window to an in-deployment window, and the result schema is still version: 1. annotateWithBaseline() matches only backend/app/metric/scenario, so the PR comment's TTFS deltas compare incompatible measurements; historical entries also inherit the new footer. Please add a methodology/schema version to baseline and history matching, or suppress those comparisons until main has produced a compatible baseline.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Results now carry methodologyVersion: 2 and annotateWithBaseline() keys deltas on methodologyVersion/backend/app/metric/scenario, so old proxy-inclusive main baselines no longer match the in-deployment runs — the delta column stays blank until main has produced a v2 baseline, then v2-vs-v2 comparisons resume. Added a suppresses deltas when the baseline methodology version differs test. (The footer legend is a single global block describing the current methodology; per-entry footers for collapsed history are out of scope here.)

namespace: SL_STREAM_NAMESPACE,
});
const writer = writable.getWriter();
await new Promise((resolve) => setTimeout(resolve, SL_WRITE_DELAY_MS));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Replace the fixed delay with an explicit reader-ready barrier. A 750 ms sleep cannot guarantee the parallel reader step has started and is blocked: under scheduler delay or load, the writer can publish first and the reader will consume a retained chunk. That measures scheduling/catch-up rather than live pub/sub propagation. Please initiate reader.read(), signal readiness through a separate hook/stream, and let the writer stamp/write only after observing that signal.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Replaced the 750ms sleep with an explicit reader-ready barrier over a second stream: the reader initiates its SL read() (establishing the stream GET) then writes a marker to bench-sl-ready; the writer blocks reading that marker before it writes+stamps the SL chunk. So the write always lands on an already-attached reader (live propagation), not a retained chunk consumed by a late reader. The ready handshake tolerates either attach order (a retained marker is fine — we don't measure its latency), so no deadlock.

ttfs: {
name: 'TTFS',
description:
'time to first step body (in-deployment start() → first step body, deployment clocks)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Update the renderer test with the new output shape..github/scripts/render-benchmark-comment.test.js still expects the old table header without P10 (line 106) and the old TTFS wording (lines 111-114), so node --test .github/scripts/render-benchmark-comment.test.js fails. Please update both assertions and add coverage for the P10 cell.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated render-benchmark-comment.test.js: the header assertion now includes P10 (ms), the TTFS wording matcher is updated, and there's a new P10-cell assertion (| 412 | 357 | 398 🔴 |). node --test passes (11/11).

* Vercel only.
* @workflow/world-postgres, local filesystem otherwise. Because SL is now
* measured inside the workflow (not by a reader in this process), it no longer
* depends on `run.getReadable()` working across processes; CI still runs this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] Keep the benchmark workflow documentation aligned..github/workflows/benchmarks.yml:98-100 still says SL requires run.getReadable() from the test process and therefore cannot use the local world's in-process streamer. This PR moves the reader inside the workflow, so that comment now contradicts the implementation.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6c7ede8. Updated the benchmarks.yml comment — SL is now measured inside the workflow (benchSlWorkflow's parallel reader/writer steps), so it no longer needs run.getReadable() from the test process; the runner only polls returnValue.

…st/doc sync
- [P1] Add methodologyVersion (=2) to results; renderer keys baseline deltas
on it so the in-deployment numbers aren't diffed against old proxy-inclusive
main baselines (deltas stay blank until main has a v2 baseline). Covered by
a new "suppresses deltas when methodology differs" test.
- [P1] Replace the fixed 750ms SL writer delay with an explicit reader-ready
barrier: the reader initiates its SL read then signals on a second stream;
the writer blocks on that signal before writing. Measures live propagation,
not a late reader catching up on a retained chunk.
- [P2] Update render-benchmark-comment.test.js for the P10 column header and
new TTFS wording, and add P10-cell coverage.
- [P3] Fix the stale benchmarks.yml comment claiming SL needs run.getReadable()
from the test process — SL is now measured inside the workflow.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for d53b055 (AI decision).

This commit refines a benchmark harness that only exists on main: stable still uses the older vitest bench setup (bench.bench.ts, the render-benchmarks action, and a completely different 97_bench.ts), and lacks packages/core/e2e/benchmark.test.ts, .github/scripts/render-benchmark-comment.mjs, and the /api/bench route this change modifies/depends on. The change is CI-only, builds on main-only benchmark infrastructure, and has no published-package impact (no changeset), so it should not be backported.

To override, re-run the Backport to stable workflow manually via workflow_dispatch and paste this commit SHA into the ref input:

d53b055a2b1aadf54afe59fd122cb1d960043bcb

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@VaguelySerious@karthikscale3