[benchmarks] Log the run id and Datadog trace for each sequential-steps run - #3248

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links
Aug 3, 2026
Merged

[benchmarks] Log the run id and Datadog trace for each sequential-steps run#3248
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links

Conversation

@shalabhc

@shalabhcshalabhc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Logs the run id and Datadog APM links for each sequential-steps benchmark iteration, in the benchmark job's own output:

[bench] 1020 steps run wrun_41KY… — trace https://app.datadoghq.com/apm/trace/6f1a… — spans https://app.datadoghq.com/apm/traces?query=%40workflow.run.id%3Awrun_41KY…

Follow-up to #3213 (which added the STSO histograms). Uses the run-id/trace capture prototyped on the WIP variance branch, #3107.

Why

The 📈 STSO distribution section shows the shape of the sequential-steps run but not which run produced it. When a bucket looks wrong, the next step is always the same manual hunt: find the run by deployment id + time window, then find its trace. This puts both in the log, next to the run that produced them.

These are debugging aids rather than part of the benchmark's reported result, so they go in the job output rather than the PR comment body. That also covers two cases a comment can't: a local pnpm bench, and a job that fails before the comment step runs.

What

  • workbench/nextjs-turbopack/app/api/bench/route.ts — returns traceId from the span @vercel/otel opened for the trigger request. @opentelemetry/api was already a dependency; no lockfile change.
  • packages/core/e2e/benchmark.test.ts — threads traceId through triggerBenchRunrunSequentialIteration, and logs one line per iteration as soon as the scenario completes.

A deployment built before the route reported a trace id logs the run id and span search, with no trace permalink.

Why two links

This deployment runs the default WORKFLOW_TRACE_MODE=linked — nothing in the workbench or benchmarks.yml sets the mode. Under linked, each workflow/step invocation is its own trace root, and the trigger's trace holds only workflow.start plus span links out to those roots. So the trace permalink is an entry point to the run (one hop through the links, which Datadog renders), not a single trace containing every step's spans.

The @workflow.run.id:<runId> span search lands directly on the run's execution spans instead, which is where an STSO investigation actually goes. It depends on workflow.run.id being an indexed span tag in the org, so it's logged alongside the permalink rather than replacing it — still to be confirmed against a real run.

Notes

The comment renderer is untouched, and the result artifact gains no new fields — the run identities are logged, not serialized.

One thing this deliberately drops relative to putting the line in the comment: there's no link to the main baseline's run. Which run produced the baseline histogram is only knowable at comment-render time, where the two artifacts are matched up; the benchmark job only knows about its own run.

Empty changeset — no package behavior changes.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6896fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production146602391705
✅ 💻 Local Development163302271860
✅ 📦 Local Production163302271860
✅ 🐘 Local Postgres163302271860
✅ 🪟 Windows15500155
✅ 📋 Other102802121240
✅ vercel-multi-region270027
Total7575011328707
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro127028
✅ example127028
✅ express127028
✅ fastify127028
✅ hono127028
✅ nextjs-turbopack15203
✅ nextjs-webpack15203
✅ nitro127028
✅ nuxt127028
✅ sveltekit14609
✅ vite127028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15500

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable129026
✅ e2e-local-dev-tanstack-start-129026
✅ e2e-local-postgres-nest-stable129026
✅ e2e-local-postgres-tanstack-start-129026
✅ e2e-local-prod-nest-stable129026
✅ e2e-local-prod-tanstack-start-129026
✅ e2e-vercel-prod-nest127028
✅ e2e-vercel-prod-tanstack-start127028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6896fb3 · Sat, 01 Aug 2026 16:24:50 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep504 (-43%) 💚1368 🔴 (+36%) 🔻1393 🔴 (+30%) 🔻1499 🔴 (+21%) 🔻30
TTFSstream1301 (+544%) 🔻1402 🔴 (+39%) 🔻1414 🔴 (+39%) 🔻1432 🔴 (+24%) 🔻30
TTFShook + stream1539 (+172%) 🔻1665 🔴 (+30%) 🔻1692 🔴 (+28%) 🔻1889 🔴 (+27%) 🔻30
STSO1020 steps (inline)178 (+9.2%)475 (+3.5%)535 (+0.8%)724 (-2.2%)1016
STSO1020 steps (queue-hop)2394 (+23%) 🔻3757 (+5.7%)3757 (+5.7%)3757 (+5.7%)3
WO1020 steps407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)1
SLstream latency108 (+35%) 🔻167 🔴 (+38%) 🔻183 🔴 (+33%) 🔻3785 🔴 (+2281%) 🔻30
SOstream overhead (text)111 (+17%) 🔻170 (+2.4%)194 (+3.7%)203 (-44%) 💚30
SOstream overhead (structured)101 (+4.1%)245 (+35%) 🔻287 (+46%) 🔻452 (+71%) 🔻30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 385272ms → this run 397249ms (Δ +11977ms, +3%)

 150-200 ms █┃████ main 49 this 18 -31
200-250 ms ███████████┃█ main 99 this 92 -7
250-300 ms █████████████████░░┃ main 132 this 161 +29
300-350 ms █████████████████┃██████ main 189 this 139 -50
350-400 ms ████████████████┃█████ main 171 this 135 -36
400-450 ms ███████████░░░░░░░┃ main 90 this 148 +58
450-500 ms █████████████████┃ main 130 this 144 +14
500-550 ms ██████████░┃ main 80 this 97 +17
550-600 ms ████┃ main 40 this 42 +2
600-650 ms █┃ main 11 this 18 +7
650-700 ms ┃ main 9 this 8 -1
700-750 ms ┃ main 6 this 7 +1
750-800 ms ┃ main 4 this 3 -1
800-850 ms ┃ main 1 this 0 -1
850-900 ms ┃ main 1 this 1 +0
900-950 ms ┃ main 0 this 1 +1
1000-1050 ms ┃ main 1 this 1 +0
1050-1100 ms ┃ main 1 this 1 +0
1700-1750 ms ┃ main 1 this 0 -1
2600-2650 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 7646ms → this run 9252ms (Δ +1606ms, +21%)

1500-2000 ms ┃███████████████████████ main 1 this 0 -1
2000-2500 ms ███████████████████████┃ main 1 this 1 +0
3000-3500 ms ░░░░░░░░░░░░░░░░░░░░░░░┃ main 0 this 1 +1
3500-4000 ms ███████████████████████┃ main 1 this 1 +0
📜 Previous results (4)

08567fd

Fri, 31 Jul 2026 23:21:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1331 (+97%) 🔻1437 🔴 (+43%) 🔻1461 🔴 (+42%) 🔻1501 🔴 (-4.9%)30
TTFSstream1262 (+39%) 🔻1383 🔴 (+48%) 🔻1394 🔴 (+47%) 🔻1425 🔴 (+47%) 🔻30
TTFShook + stream486 (+2.7%)1626 🔴 (+25%) 🔻1665 🔴 (+24%) 🔻1775 🔴 (+29%) 🔻30
STSO1020 steps (inline)160 (+24%) 🔻452 (±0%)508 (±0%)695 (-3.1%)1016
STSO1020 steps (queue-hop)2396 (+61%) 🔻3482 (+10%)3482 (+10%)3482 (+10%)3
WO1020 steps389282 (±0%)389282 (±0%)389282 (±0%)389282 (±0%)1
SLstream latency90 (+20%) 🔻135 🔴 (+17%) 🔻137 🔴 (+7.9%)2905 🔴 (+754%) 🔻30

75e3abe

Fri, 31 Jul 2026 18:10:29 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep268 (+1.9%)1435 🔴 (±0%)1520 🔴 (-5.5%)1887 🔴 (-6.0%)30
TTFSstream260 (+5.3%)1466 🔴 (+4.6%)1512 🔴 (-3.9%)1812 🔴 (-11%)30
TTFShook + stream346 (-14%)1596 🔴 (+23%) 🔻1656 🔴 (+22%) 🔻1845 🔴 (+26%) 🔻30
STSO1020 steps (inline)179 (+1.1%)502 (-2.3%)571 (-1.2%)782 (±0%)1016
STSO1020 steps (queue-hop)2133 (+44%) 🔻2807 (+0.7%)2807 (+0.7%)2807 (+0.7%)3
WO1020 steps438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)1
SLstream latency128 (+45%) 🔻177 🔴 (+12%)197 🔴 (+15%)354 🔴 (+80%) 🔻30
SOstream overhead (text)111 (-21%) 💚221 (+3.3%)273 (+21%) 🔻390 (-21%) 💚30
SOstream overhead (structured)119 (-11%)211 (-22%) 💚242 (-31%) 💚364 (-23%) 💚30

c784f6c

Fri, 31 Jul 2026 16:59:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1248 (+139%) 🔻1298 🔴 (+25%) 🔻1316 🔴 (+23%) 🔻1333 🔴 (-18%) 💚30
TTFSstream1250 (+32%) 🔻1303 🔴 (+30%) 🔻1317 🔴 (+28%) 🔻1360 🔴 (+5.5%)30
TTFShook + stream445 (+5.7%)1576 🔴 (+19%) 🔻1584 🔴 (+17%) 🔻1629 🔴 (-8.0%)30
STSO1020 steps (inline)172 (+5.5%)465 (-1.9%)522 (-1.7%)676 (+0.7%)1016
STSO1020 steps (queue-hop)2432 (+17%) 🔻3265 (+2.1%)3265 (+2.1%)3265 (+2.1%)3
WO1020 steps399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)1
SLstream latency90 (+9.8%)139 🔴 (+5.3%)175 🔴 (+19%) 🔻196 🔴 (-8.8%)30
SOstream overhead (text)102 (+1.0%)149 (-13%)196 (-8.4%)344 (+38%) 🔻30
SOstream overhead (structured)101 (-8.2%)179 (+5.9%)215 (+13%)322 (+41%) 🔻30

fedfc7a

Fri, 31 Jul 2026 01:20:43 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep911 (-9.4%)1286 🔴 (+18%) 🔻1321 🔴 (+20%) 🔻1405 🔴 (-12%)30
TTFSstream1239 (+236%) 🔻1279 🔴 (+21%) 🔻1298 🔴 (+20%) 🔻1366 🔴 (+23%) 🔻30
TTFShook + stream1387 (+177%) 🔻1526 🔴 (+9.0%)1586 🔴 (+8.0%)1628 🔴 (+8.8%)30
STSO1020 steps (inline)168 (-8.7%)466 (-5.1%)525 (-5.9%)780 (+3.2%)1016
STSO1020 steps (queue-hop)2241 (+9.0%)3376 (+21%) 🔻3376 (+21%) 🔻3376 (+21%) 🔻3
WO1020 steps399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)1
SLstream latency79 (-8.1%)144 🔴 (-1.4%)155 🔴 (-16%) 💚411 🔴 (+61%) 🔻30
SOstream overhead (text)105 (-23%) 💚146 (-71%) 💚178 (-81%) 💚205 (-83%) 💚30
SOstream overhead (structured)96 (-14%)162 (-21%) 💚223 (-15%)1745 🔴 (+294%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

@shalabhc
shalabhc marked this pull request as ready for review July 31, 2026 01:34
@shalabhc
shalabhc requested review from a team and ijjk as code ownersJuly 31, 2026 01:34

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the capture, the threading, and the renderer. Approving — this closes a real gap in the histogram section (I've done the deployment-id + time-window hunt this replaces), and the degradation matrix is genuinely complete: every optional field falls back the way the table claims, and the three new renderer tests cover the full/degraded/omitted cases (40/40 pass locally).

One correction worth making before merge — the route comment overclaims what the trace contains.route.ts says the trigger request's span "propagates into the workflow's own spans". That's only true under WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or benchmarks.yml sets the mode, so the benchmark deployment runs the default linked mode — where each workflow/step invocation is its own trace root, and the trigger request's trace contains only the workflow.start span plus span links to the execution roots. The link is still useful (one hop via span links, and Datadog renders those), but:

  • the comment should say what's actually there, so nobody debugging a weird histogram concludes the run "has no spans" when they open a trigger-only trace; and
  • consider making the link a Datadog APM search on workflow.run.id:<runId> instead of (or alongside) the trace permalink — under linked mode that lands on the run's actual execution spans directly, which is where an STSO investigation actually goes. The run id is already in hand.

Fine to take the comment fix now and the search-link idea as a follow-up — the current link is strictly better than nothing either way.

Verified:

  • The capture is correctly optional end-to-end: optional-chained getActiveSpan() (undefined without OTEL → bare run id), type-guarded on the runner side (typeof data.traceId === 'string'), and sequentialRuns omitted rather than [] when the scenario didn't run — so old artifacts, old deployments, and history re-renders all degrade exactly per the table.
  • Recording all iterations' run refs matches the data: the histograms aggregate samples across iterations, so with BENCH_SEQUENTIAL_ITERATIONS > 1 the comma-joined list is the honest answer. The doc phrase "both histograms come out of the same iteration" is only true for the default single-iteration config — worth a "(iterations, when configured)" tweak since the code already handles N correctly.
  • Baseline matching reuses the same methodology/backend/app key the percentile baselines use (resultKeyFor = the metric key minus the row), so the two sides of the diff can't come from mismatched configurations.
  • The empty changeset is correct — e2e runner, CI scripts, and a workbench route; nothing published changes.

One robustness nit, non-blocking:runId/traceId are spliced into the markdown link unvalidated. The artifact-trust model is consistent with the rest of this renderer, so it's not a security gap — but a cheap shape check (/^[a-z0-9_]+$/i) before rendering the link would keep a malformed value from a buggy deployment from breaking the whole comment's markdown, which is this section's own failure story.

CI: the 2 failures are the nextjs-webpack canary HMR rebuild-count test and its required-check aggregator — the long-running baseline flake, unreachable from a benchmark-comment change.

shalabhc added a commit that referenced this pull request Jul 31, 2026
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 22:49 Inactive
vercelBotand others added 4 commits August 1, 2026 16:00
The STSO distribution section added in #3213 shows the shape of the
sequential-steps run but not which run produced it, so investigating an
odd-looking bucket meant hunting for the run by deployment id and time
window.
Capture the identity alongside the samples (the mechanism prototyped on
the WIP variance branch, #3107): `/api/bench` returns the trace id of the
span @vercel/otel opened for the trigger request, the runner threads it
through the sequential iteration and records `sequentialRuns` in the
result file, and the renderer prints one line under the histograms with
the run id + Datadog trace link for this run and for the `main` run it is
diffed against.
Every part is optional — a deployment predating the route change yields a
bare run id, and a `main` baseline predating this yields only this run's
side — so the section degrades instead of breaking on mixed-vintage
artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The run id and Datadog trace are debugging aids, not part of the
benchmark's reported result, so they belong in the job's own output
rather than in the PR comment body.
Logging them where the runs are produced also makes them available in
two cases the comment could never cover: a local `pnpm bench`, and a
job that fails before the comment step runs.
This drops the comment-rendering side entirely -- `renderSequentialRunLinks`,
the `baselineSequentialRuns` baseline plumbing in `annotateWithBaseline`,
and the `sequentialRuns` field on the result artifact, which existed only
to carry the data to the renderer.
The `main`-baseline side of the link goes away with it: which run produced
the baseline histogram is only knowable at comment-render time, where the
two artifacts are matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Under the default linked trace mode the trigger's trace holds only
`workflow.start` plus span links, so opening it lands one hop away from
the spans an STSO investigation needs. Log an APM search on
`@workflow.run.id:<runId>` next to it, which goes straight to the run's
execution spans.
Both links are logged rather than one replacing the other: the search
depends on `workflow.run.id` being an indexed span tag in the org, and
the permalink works regardless.
Suggested by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for ba2cddc (AI decision).

This commit adds a new debugging affordance to the benchmark harness — the /api/bench route now returns the OTel trace id and benchmark.test.ts logs a run id plus two Datadog APM links per sequential-steps iteration. Nothing here fixes a defect, a flaky test, or a build/release problem on stable; it is additive tooling (the changeset is deliberately empty, i.e. no package behavior change). Feature/enhancement work stays on main.

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

ba2cddc861380560cc2ae980c9cb4e52a1530cc8

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

@shalabhc@TooTallNate
, '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

[benchmarks] Log the run id and Datadog trace for each sequential-steps run - #3248

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links
Aug 3, 2026
Merged

[benchmarks] Log the run id and Datadog trace for each sequential-steps run#3248
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links

Conversation

@shalabhc

@shalabhcshalabhc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Logs the run id and Datadog APM links for each sequential-steps benchmark iteration, in the benchmark job's own output:

[bench] 1020 steps run wrun_41KY… — trace https://app.datadoghq.com/apm/trace/6f1a… — spans https://app.datadoghq.com/apm/traces?query=%40workflow.run.id%3Awrun_41KY…

Follow-up to #3213 (which added the STSO histograms). Uses the run-id/trace capture prototyped on the WIP variance branch, #3107.

Why

The 📈 STSO distribution section shows the shape of the sequential-steps run but not which run produced it. When a bucket looks wrong, the next step is always the same manual hunt: find the run by deployment id + time window, then find its trace. This puts both in the log, next to the run that produced them.

These are debugging aids rather than part of the benchmark's reported result, so they go in the job output rather than the PR comment body. That also covers two cases a comment can't: a local pnpm bench, and a job that fails before the comment step runs.

What

  • workbench/nextjs-turbopack/app/api/bench/route.ts — returns traceId from the span @vercel/otel opened for the trigger request. @opentelemetry/api was already a dependency; no lockfile change.
  • packages/core/e2e/benchmark.test.ts — threads traceId through triggerBenchRunrunSequentialIteration, and logs one line per iteration as soon as the scenario completes.

A deployment built before the route reported a trace id logs the run id and span search, with no trace permalink.

Why two links

This deployment runs the default WORKFLOW_TRACE_MODE=linked — nothing in the workbench or benchmarks.yml sets the mode. Under linked, each workflow/step invocation is its own trace root, and the trigger's trace holds only workflow.start plus span links out to those roots. So the trace permalink is an entry point to the run (one hop through the links, which Datadog renders), not a single trace containing every step's spans.

The @workflow.run.id:<runId> span search lands directly on the run's execution spans instead, which is where an STSO investigation actually goes. It depends on workflow.run.id being an indexed span tag in the org, so it's logged alongside the permalink rather than replacing it — still to be confirmed against a real run.

Notes

The comment renderer is untouched, and the result artifact gains no new fields — the run identities are logged, not serialized.

One thing this deliberately drops relative to putting the line in the comment: there's no link to the main baseline's run. Which run produced the baseline histogram is only knowable at comment-render time, where the two artifacts are matched up; the benchmark job only knows about its own run.

Empty changeset — no package behavior changes.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6896fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production146602391705
✅ 💻 Local Development163302271860
✅ 📦 Local Production163302271860
✅ 🐘 Local Postgres163302271860
✅ 🪟 Windows15500155
✅ 📋 Other102802121240
✅ vercel-multi-region270027
Total7575011328707
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro127028
✅ example127028
✅ express127028
✅ fastify127028
✅ hono127028
✅ nextjs-turbopack15203
✅ nextjs-webpack15203
✅ nitro127028
✅ nuxt127028
✅ sveltekit14609
✅ vite127028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15500

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable129026
✅ e2e-local-dev-tanstack-start-129026
✅ e2e-local-postgres-nest-stable129026
✅ e2e-local-postgres-tanstack-start-129026
✅ e2e-local-prod-nest-stable129026
✅ e2e-local-prod-tanstack-start-129026
✅ e2e-vercel-prod-nest127028
✅ e2e-vercel-prod-tanstack-start127028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6896fb3 · Sat, 01 Aug 2026 16:24:50 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep504 (-43%) 💚1368 🔴 (+36%) 🔻1393 🔴 (+30%) 🔻1499 🔴 (+21%) 🔻30
TTFSstream1301 (+544%) 🔻1402 🔴 (+39%) 🔻1414 🔴 (+39%) 🔻1432 🔴 (+24%) 🔻30
TTFShook + stream1539 (+172%) 🔻1665 🔴 (+30%) 🔻1692 🔴 (+28%) 🔻1889 🔴 (+27%) 🔻30
STSO1020 steps (inline)178 (+9.2%)475 (+3.5%)535 (+0.8%)724 (-2.2%)1016
STSO1020 steps (queue-hop)2394 (+23%) 🔻3757 (+5.7%)3757 (+5.7%)3757 (+5.7%)3
WO1020 steps407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)1
SLstream latency108 (+35%) 🔻167 🔴 (+38%) 🔻183 🔴 (+33%) 🔻3785 🔴 (+2281%) 🔻30
SOstream overhead (text)111 (+17%) 🔻170 (+2.4%)194 (+3.7%)203 (-44%) 💚30
SOstream overhead (structured)101 (+4.1%)245 (+35%) 🔻287 (+46%) 🔻452 (+71%) 🔻30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 385272ms → this run 397249ms (Δ +11977ms, +3%)

 150-200 ms █┃████ main 49 this 18 -31
200-250 ms ███████████┃█ main 99 this 92 -7
250-300 ms █████████████████░░┃ main 132 this 161 +29
300-350 ms █████████████████┃██████ main 189 this 139 -50
350-400 ms ████████████████┃█████ main 171 this 135 -36
400-450 ms ███████████░░░░░░░┃ main 90 this 148 +58
450-500 ms █████████████████┃ main 130 this 144 +14
500-550 ms ██████████░┃ main 80 this 97 +17
550-600 ms ████┃ main 40 this 42 +2
600-650 ms █┃ main 11 this 18 +7
650-700 ms ┃ main 9 this 8 -1
700-750 ms ┃ main 6 this 7 +1
750-800 ms ┃ main 4 this 3 -1
800-850 ms ┃ main 1 this 0 -1
850-900 ms ┃ main 1 this 1 +0
900-950 ms ┃ main 0 this 1 +1
1000-1050 ms ┃ main 1 this 1 +0
1050-1100 ms ┃ main 1 this 1 +0
1700-1750 ms ┃ main 1 this 0 -1
2600-2650 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 7646ms → this run 9252ms (Δ +1606ms, +21%)

1500-2000 ms ┃███████████████████████ main 1 this 0 -1
2000-2500 ms ███████████████████████┃ main 1 this 1 +0
3000-3500 ms ░░░░░░░░░░░░░░░░░░░░░░░┃ main 0 this 1 +1
3500-4000 ms ███████████████████████┃ main 1 this 1 +0
📜 Previous results (4)

08567fd

Fri, 31 Jul 2026 23:21:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1331 (+97%) 🔻1437 🔴 (+43%) 🔻1461 🔴 (+42%) 🔻1501 🔴 (-4.9%)30
TTFSstream1262 (+39%) 🔻1383 🔴 (+48%) 🔻1394 🔴 (+47%) 🔻1425 🔴 (+47%) 🔻30
TTFShook + stream486 (+2.7%)1626 🔴 (+25%) 🔻1665 🔴 (+24%) 🔻1775 🔴 (+29%) 🔻30
STSO1020 steps (inline)160 (+24%) 🔻452 (±0%)508 (±0%)695 (-3.1%)1016
STSO1020 steps (queue-hop)2396 (+61%) 🔻3482 (+10%)3482 (+10%)3482 (+10%)3
WO1020 steps389282 (±0%)389282 (±0%)389282 (±0%)389282 (±0%)1
SLstream latency90 (+20%) 🔻135 🔴 (+17%) 🔻137 🔴 (+7.9%)2905 🔴 (+754%) 🔻30

75e3abe

Fri, 31 Jul 2026 18:10:29 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep268 (+1.9%)1435 🔴 (±0%)1520 🔴 (-5.5%)1887 🔴 (-6.0%)30
TTFSstream260 (+5.3%)1466 🔴 (+4.6%)1512 🔴 (-3.9%)1812 🔴 (-11%)30
TTFShook + stream346 (-14%)1596 🔴 (+23%) 🔻1656 🔴 (+22%) 🔻1845 🔴 (+26%) 🔻30
STSO1020 steps (inline)179 (+1.1%)502 (-2.3%)571 (-1.2%)782 (±0%)1016
STSO1020 steps (queue-hop)2133 (+44%) 🔻2807 (+0.7%)2807 (+0.7%)2807 (+0.7%)3
WO1020 steps438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)1
SLstream latency128 (+45%) 🔻177 🔴 (+12%)197 🔴 (+15%)354 🔴 (+80%) 🔻30
SOstream overhead (text)111 (-21%) 💚221 (+3.3%)273 (+21%) 🔻390 (-21%) 💚30
SOstream overhead (structured)119 (-11%)211 (-22%) 💚242 (-31%) 💚364 (-23%) 💚30

c784f6c

Fri, 31 Jul 2026 16:59:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1248 (+139%) 🔻1298 🔴 (+25%) 🔻1316 🔴 (+23%) 🔻1333 🔴 (-18%) 💚30
TTFSstream1250 (+32%) 🔻1303 🔴 (+30%) 🔻1317 🔴 (+28%) 🔻1360 🔴 (+5.5%)30
TTFShook + stream445 (+5.7%)1576 🔴 (+19%) 🔻1584 🔴 (+17%) 🔻1629 🔴 (-8.0%)30
STSO1020 steps (inline)172 (+5.5%)465 (-1.9%)522 (-1.7%)676 (+0.7%)1016
STSO1020 steps (queue-hop)2432 (+17%) 🔻3265 (+2.1%)3265 (+2.1%)3265 (+2.1%)3
WO1020 steps399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)1
SLstream latency90 (+9.8%)139 🔴 (+5.3%)175 🔴 (+19%) 🔻196 🔴 (-8.8%)30
SOstream overhead (text)102 (+1.0%)149 (-13%)196 (-8.4%)344 (+38%) 🔻30
SOstream overhead (structured)101 (-8.2%)179 (+5.9%)215 (+13%)322 (+41%) 🔻30

fedfc7a

Fri, 31 Jul 2026 01:20:43 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep911 (-9.4%)1286 🔴 (+18%) 🔻1321 🔴 (+20%) 🔻1405 🔴 (-12%)30
TTFSstream1239 (+236%) 🔻1279 🔴 (+21%) 🔻1298 🔴 (+20%) 🔻1366 🔴 (+23%) 🔻30
TTFShook + stream1387 (+177%) 🔻1526 🔴 (+9.0%)1586 🔴 (+8.0%)1628 🔴 (+8.8%)30
STSO1020 steps (inline)168 (-8.7%)466 (-5.1%)525 (-5.9%)780 (+3.2%)1016
STSO1020 steps (queue-hop)2241 (+9.0%)3376 (+21%) 🔻3376 (+21%) 🔻3376 (+21%) 🔻3
WO1020 steps399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)1
SLstream latency79 (-8.1%)144 🔴 (-1.4%)155 🔴 (-16%) 💚411 🔴 (+61%) 🔻30
SOstream overhead (text)105 (-23%) 💚146 (-71%) 💚178 (-81%) 💚205 (-83%) 💚30
SOstream overhead (structured)96 (-14%)162 (-21%) 💚223 (-15%)1745 🔴 (+294%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

@shalabhc
shalabhc marked this pull request as ready for review July 31, 2026 01:34
@shalabhc
shalabhc requested review from a team and ijjk as code ownersJuly 31, 2026 01:34

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the capture, the threading, and the renderer. Approving — this closes a real gap in the histogram section (I've done the deployment-id + time-window hunt this replaces), and the degradation matrix is genuinely complete: every optional field falls back the way the table claims, and the three new renderer tests cover the full/degraded/omitted cases (40/40 pass locally).

One correction worth making before merge — the route comment overclaims what the trace contains.route.ts says the trigger request's span "propagates into the workflow's own spans". That's only true under WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or benchmarks.yml sets the mode, so the benchmark deployment runs the default linked mode — where each workflow/step invocation is its own trace root, and the trigger request's trace contains only the workflow.start span plus span links to the execution roots. The link is still useful (one hop via span links, and Datadog renders those), but:

  • the comment should say what's actually there, so nobody debugging a weird histogram concludes the run "has no spans" when they open a trigger-only trace; and
  • consider making the link a Datadog APM search on workflow.run.id:<runId> instead of (or alongside) the trace permalink — under linked mode that lands on the run's actual execution spans directly, which is where an STSO investigation actually goes. The run id is already in hand.

Fine to take the comment fix now and the search-link idea as a follow-up — the current link is strictly better than nothing either way.

Verified:

  • The capture is correctly optional end-to-end: optional-chained getActiveSpan() (undefined without OTEL → bare run id), type-guarded on the runner side (typeof data.traceId === 'string'), and sequentialRuns omitted rather than [] when the scenario didn't run — so old artifacts, old deployments, and history re-renders all degrade exactly per the table.
  • Recording all iterations' run refs matches the data: the histograms aggregate samples across iterations, so with BENCH_SEQUENTIAL_ITERATIONS > 1 the comma-joined list is the honest answer. The doc phrase "both histograms come out of the same iteration" is only true for the default single-iteration config — worth a "(iterations, when configured)" tweak since the code already handles N correctly.
  • Baseline matching reuses the same methodology/backend/app key the percentile baselines use (resultKeyFor = the metric key minus the row), so the two sides of the diff can't come from mismatched configurations.
  • The empty changeset is correct — e2e runner, CI scripts, and a workbench route; nothing published changes.

One robustness nit, non-blocking:runId/traceId are spliced into the markdown link unvalidated. The artifact-trust model is consistent with the rest of this renderer, so it's not a security gap — but a cheap shape check (/^[a-z0-9_]+$/i) before rendering the link would keep a malformed value from a buggy deployment from breaking the whole comment's markdown, which is this section's own failure story.

CI: the 2 failures are the nextjs-webpack canary HMR rebuild-count test and its required-check aggregator — the long-running baseline flake, unreachable from a benchmark-comment change.

shalabhc added a commit that referenced this pull request Jul 31, 2026
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 22:49 Inactive
vercelBotand others added 4 commits August 1, 2026 16:00
The STSO distribution section added in #3213 shows the shape of the
sequential-steps run but not which run produced it, so investigating an
odd-looking bucket meant hunting for the run by deployment id and time
window.
Capture the identity alongside the samples (the mechanism prototyped on
the WIP variance branch, #3107): `/api/bench` returns the trace id of the
span @vercel/otel opened for the trigger request, the runner threads it
through the sequential iteration and records `sequentialRuns` in the
result file, and the renderer prints one line under the histograms with
the run id + Datadog trace link for this run and for the `main` run it is
diffed against.
Every part is optional — a deployment predating the route change yields a
bare run id, and a `main` baseline predating this yields only this run's
side — so the section degrades instead of breaking on mixed-vintage
artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The run id and Datadog trace are debugging aids, not part of the
benchmark's reported result, so they belong in the job's own output
rather than in the PR comment body.
Logging them where the runs are produced also makes them available in
two cases the comment could never cover: a local `pnpm bench`, and a
job that fails before the comment step runs.
This drops the comment-rendering side entirely -- `renderSequentialRunLinks`,
the `baselineSequentialRuns` baseline plumbing in `annotateWithBaseline`,
and the `sequentialRuns` field on the result artifact, which existed only
to carry the data to the renderer.
The `main`-baseline side of the link goes away with it: which run produced
the baseline histogram is only knowable at comment-render time, where the
two artifacts are matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Under the default linked trace mode the trigger's trace holds only
`workflow.start` plus span links, so opening it lands one hop away from
the spans an STSO investigation needs. Log an APM search on
`@workflow.run.id:<runId>` next to it, which goes straight to the run's
execution spans.
Both links are logged rather than one replacing the other: the search
depends on `workflow.run.id` being an indexed span tag in the org, and
the permalink works regardless.
Suggested by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for ba2cddc (AI decision).

This commit adds a new debugging affordance to the benchmark harness — the /api/bench route now returns the OTel trace id and benchmark.test.ts logs a run id plus two Datadog APM links per sequential-steps iteration. Nothing here fixes a defect, a flaky test, or a build/release problem on stable; it is additive tooling (the changeset is deliberately empty, i.e. no package behavior change). Feature/enhancement work stays on main.

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

ba2cddc861380560cc2ae980c9cb4e52a1530cc8

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

@shalabhc@TooTallNate
, '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

[benchmarks] Log the run id and Datadog trace for each sequential-steps run - #3248

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links
Aug 3, 2026
Merged

[benchmarks] Log the run id and Datadog trace for each sequential-steps run#3248
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links

Conversation

@shalabhc

@shalabhcshalabhc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Logs the run id and Datadog APM links for each sequential-steps benchmark iteration, in the benchmark job's own output:

[bench] 1020 steps run wrun_41KY… — trace https://app.datadoghq.com/apm/trace/6f1a… — spans https://app.datadoghq.com/apm/traces?query=%40workflow.run.id%3Awrun_41KY…

Follow-up to #3213 (which added the STSO histograms). Uses the run-id/trace capture prototyped on the WIP variance branch, #3107.

Why

The 📈 STSO distribution section shows the shape of the sequential-steps run but not which run produced it. When a bucket looks wrong, the next step is always the same manual hunt: find the run by deployment id + time window, then find its trace. This puts both in the log, next to the run that produced them.

These are debugging aids rather than part of the benchmark's reported result, so they go in the job output rather than the PR comment body. That also covers two cases a comment can't: a local pnpm bench, and a job that fails before the comment step runs.

What

  • workbench/nextjs-turbopack/app/api/bench/route.ts — returns traceId from the span @vercel/otel opened for the trigger request. @opentelemetry/api was already a dependency; no lockfile change.
  • packages/core/e2e/benchmark.test.ts — threads traceId through triggerBenchRunrunSequentialIteration, and logs one line per iteration as soon as the scenario completes.

A deployment built before the route reported a trace id logs the run id and span search, with no trace permalink.

Why two links

This deployment runs the default WORKFLOW_TRACE_MODE=linked — nothing in the workbench or benchmarks.yml sets the mode. Under linked, each workflow/step invocation is its own trace root, and the trigger's trace holds only workflow.start plus span links out to those roots. So the trace permalink is an entry point to the run (one hop through the links, which Datadog renders), not a single trace containing every step's spans.

The @workflow.run.id:<runId> span search lands directly on the run's execution spans instead, which is where an STSO investigation actually goes. It depends on workflow.run.id being an indexed span tag in the org, so it's logged alongside the permalink rather than replacing it — still to be confirmed against a real run.

Notes

The comment renderer is untouched, and the result artifact gains no new fields — the run identities are logged, not serialized.

One thing this deliberately drops relative to putting the line in the comment: there's no link to the main baseline's run. Which run produced the baseline histogram is only knowable at comment-render time, where the two artifacts are matched up; the benchmark job only knows about its own run.

Empty changeset — no package behavior changes.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6896fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production146602391705
✅ 💻 Local Development163302271860
✅ 📦 Local Production163302271860
✅ 🐘 Local Postgres163302271860
✅ 🪟 Windows15500155
✅ 📋 Other102802121240
✅ vercel-multi-region270027
Total7575011328707
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro127028
✅ example127028
✅ express127028
✅ fastify127028
✅ hono127028
✅ nextjs-turbopack15203
✅ nextjs-webpack15203
✅ nitro127028
✅ nuxt127028
✅ sveltekit14609
✅ vite127028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15500

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable129026
✅ e2e-local-dev-tanstack-start-129026
✅ e2e-local-postgres-nest-stable129026
✅ e2e-local-postgres-tanstack-start-129026
✅ e2e-local-prod-nest-stable129026
✅ e2e-local-prod-tanstack-start-129026
✅ e2e-vercel-prod-nest127028
✅ e2e-vercel-prod-tanstack-start127028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6896fb3 · Sat, 01 Aug 2026 16:24:50 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep504 (-43%) 💚1368 🔴 (+36%) 🔻1393 🔴 (+30%) 🔻1499 🔴 (+21%) 🔻30
TTFSstream1301 (+544%) 🔻1402 🔴 (+39%) 🔻1414 🔴 (+39%) 🔻1432 🔴 (+24%) 🔻30
TTFShook + stream1539 (+172%) 🔻1665 🔴 (+30%) 🔻1692 🔴 (+28%) 🔻1889 🔴 (+27%) 🔻30
STSO1020 steps (inline)178 (+9.2%)475 (+3.5%)535 (+0.8%)724 (-2.2%)1016
STSO1020 steps (queue-hop)2394 (+23%) 🔻3757 (+5.7%)3757 (+5.7%)3757 (+5.7%)3
WO1020 steps407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)1
SLstream latency108 (+35%) 🔻167 🔴 (+38%) 🔻183 🔴 (+33%) 🔻3785 🔴 (+2281%) 🔻30
SOstream overhead (text)111 (+17%) 🔻170 (+2.4%)194 (+3.7%)203 (-44%) 💚30
SOstream overhead (structured)101 (+4.1%)245 (+35%) 🔻287 (+46%) 🔻452 (+71%) 🔻30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 385272ms → this run 397249ms (Δ +11977ms, +3%)

 150-200 ms █┃████ main 49 this 18 -31
200-250 ms ███████████┃█ main 99 this 92 -7
250-300 ms █████████████████░░┃ main 132 this 161 +29
300-350 ms █████████████████┃██████ main 189 this 139 -50
350-400 ms ████████████████┃█████ main 171 this 135 -36
400-450 ms ███████████░░░░░░░┃ main 90 this 148 +58
450-500 ms █████████████████┃ main 130 this 144 +14
500-550 ms ██████████░┃ main 80 this 97 +17
550-600 ms ████┃ main 40 this 42 +2
600-650 ms █┃ main 11 this 18 +7
650-700 ms ┃ main 9 this 8 -1
700-750 ms ┃ main 6 this 7 +1
750-800 ms ┃ main 4 this 3 -1
800-850 ms ┃ main 1 this 0 -1
850-900 ms ┃ main 1 this 1 +0
900-950 ms ┃ main 0 this 1 +1
1000-1050 ms ┃ main 1 this 1 +0
1050-1100 ms ┃ main 1 this 1 +0
1700-1750 ms ┃ main 1 this 0 -1
2600-2650 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 7646ms → this run 9252ms (Δ +1606ms, +21%)

1500-2000 ms ┃███████████████████████ main 1 this 0 -1
2000-2500 ms ███████████████████████┃ main 1 this 1 +0
3000-3500 ms ░░░░░░░░░░░░░░░░░░░░░░░┃ main 0 this 1 +1
3500-4000 ms ███████████████████████┃ main 1 this 1 +0
📜 Previous results (4)

08567fd

Fri, 31 Jul 2026 23:21:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1331 (+97%) 🔻1437 🔴 (+43%) 🔻1461 🔴 (+42%) 🔻1501 🔴 (-4.9%)30
TTFSstream1262 (+39%) 🔻1383 🔴 (+48%) 🔻1394 🔴 (+47%) 🔻1425 🔴 (+47%) 🔻30
TTFShook + stream486 (+2.7%)1626 🔴 (+25%) 🔻1665 🔴 (+24%) 🔻1775 🔴 (+29%) 🔻30
STSO1020 steps (inline)160 (+24%) 🔻452 (±0%)508 (±0%)695 (-3.1%)1016
STSO1020 steps (queue-hop)2396 (+61%) 🔻3482 (+10%)3482 (+10%)3482 (+10%)3
WO1020 steps389282 (±0%)389282 (±0%)389282 (±0%)389282 (±0%)1
SLstream latency90 (+20%) 🔻135 🔴 (+17%) 🔻137 🔴 (+7.9%)2905 🔴 (+754%) 🔻30

75e3abe

Fri, 31 Jul 2026 18:10:29 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep268 (+1.9%)1435 🔴 (±0%)1520 🔴 (-5.5%)1887 🔴 (-6.0%)30
TTFSstream260 (+5.3%)1466 🔴 (+4.6%)1512 🔴 (-3.9%)1812 🔴 (-11%)30
TTFShook + stream346 (-14%)1596 🔴 (+23%) 🔻1656 🔴 (+22%) 🔻1845 🔴 (+26%) 🔻30
STSO1020 steps (inline)179 (+1.1%)502 (-2.3%)571 (-1.2%)782 (±0%)1016
STSO1020 steps (queue-hop)2133 (+44%) 🔻2807 (+0.7%)2807 (+0.7%)2807 (+0.7%)3
WO1020 steps438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)1
SLstream latency128 (+45%) 🔻177 🔴 (+12%)197 🔴 (+15%)354 🔴 (+80%) 🔻30
SOstream overhead (text)111 (-21%) 💚221 (+3.3%)273 (+21%) 🔻390 (-21%) 💚30
SOstream overhead (structured)119 (-11%)211 (-22%) 💚242 (-31%) 💚364 (-23%) 💚30

c784f6c

Fri, 31 Jul 2026 16:59:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1248 (+139%) 🔻1298 🔴 (+25%) 🔻1316 🔴 (+23%) 🔻1333 🔴 (-18%) 💚30
TTFSstream1250 (+32%) 🔻1303 🔴 (+30%) 🔻1317 🔴 (+28%) 🔻1360 🔴 (+5.5%)30
TTFShook + stream445 (+5.7%)1576 🔴 (+19%) 🔻1584 🔴 (+17%) 🔻1629 🔴 (-8.0%)30
STSO1020 steps (inline)172 (+5.5%)465 (-1.9%)522 (-1.7%)676 (+0.7%)1016
STSO1020 steps (queue-hop)2432 (+17%) 🔻3265 (+2.1%)3265 (+2.1%)3265 (+2.1%)3
WO1020 steps399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)1
SLstream latency90 (+9.8%)139 🔴 (+5.3%)175 🔴 (+19%) 🔻196 🔴 (-8.8%)30
SOstream overhead (text)102 (+1.0%)149 (-13%)196 (-8.4%)344 (+38%) 🔻30
SOstream overhead (structured)101 (-8.2%)179 (+5.9%)215 (+13%)322 (+41%) 🔻30

fedfc7a

Fri, 31 Jul 2026 01:20:43 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep911 (-9.4%)1286 🔴 (+18%) 🔻1321 🔴 (+20%) 🔻1405 🔴 (-12%)30
TTFSstream1239 (+236%) 🔻1279 🔴 (+21%) 🔻1298 🔴 (+20%) 🔻1366 🔴 (+23%) 🔻30
TTFShook + stream1387 (+177%) 🔻1526 🔴 (+9.0%)1586 🔴 (+8.0%)1628 🔴 (+8.8%)30
STSO1020 steps (inline)168 (-8.7%)466 (-5.1%)525 (-5.9%)780 (+3.2%)1016
STSO1020 steps (queue-hop)2241 (+9.0%)3376 (+21%) 🔻3376 (+21%) 🔻3376 (+21%) 🔻3
WO1020 steps399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)1
SLstream latency79 (-8.1%)144 🔴 (-1.4%)155 🔴 (-16%) 💚411 🔴 (+61%) 🔻30
SOstream overhead (text)105 (-23%) 💚146 (-71%) 💚178 (-81%) 💚205 (-83%) 💚30
SOstream overhead (structured)96 (-14%)162 (-21%) 💚223 (-15%)1745 🔴 (+294%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

@shalabhc
shalabhc marked this pull request as ready for review July 31, 2026 01:34
@shalabhc
shalabhc requested review from a team and ijjk as code ownersJuly 31, 2026 01:34

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the capture, the threading, and the renderer. Approving — this closes a real gap in the histogram section (I've done the deployment-id + time-window hunt this replaces), and the degradation matrix is genuinely complete: every optional field falls back the way the table claims, and the three new renderer tests cover the full/degraded/omitted cases (40/40 pass locally).

One correction worth making before merge — the route comment overclaims what the trace contains.route.ts says the trigger request's span "propagates into the workflow's own spans". That's only true under WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or benchmarks.yml sets the mode, so the benchmark deployment runs the default linked mode — where each workflow/step invocation is its own trace root, and the trigger request's trace contains only the workflow.start span plus span links to the execution roots. The link is still useful (one hop via span links, and Datadog renders those), but:

  • the comment should say what's actually there, so nobody debugging a weird histogram concludes the run "has no spans" when they open a trigger-only trace; and
  • consider making the link a Datadog APM search on workflow.run.id:<runId> instead of (or alongside) the trace permalink — under linked mode that lands on the run's actual execution spans directly, which is where an STSO investigation actually goes. The run id is already in hand.

Fine to take the comment fix now and the search-link idea as a follow-up — the current link is strictly better than nothing either way.

Verified:

  • The capture is correctly optional end-to-end: optional-chained getActiveSpan() (undefined without OTEL → bare run id), type-guarded on the runner side (typeof data.traceId === 'string'), and sequentialRuns omitted rather than [] when the scenario didn't run — so old artifacts, old deployments, and history re-renders all degrade exactly per the table.
  • Recording all iterations' run refs matches the data: the histograms aggregate samples across iterations, so with BENCH_SEQUENTIAL_ITERATIONS > 1 the comma-joined list is the honest answer. The doc phrase "both histograms come out of the same iteration" is only true for the default single-iteration config — worth a "(iterations, when configured)" tweak since the code already handles N correctly.
  • Baseline matching reuses the same methodology/backend/app key the percentile baselines use (resultKeyFor = the metric key minus the row), so the two sides of the diff can't come from mismatched configurations.
  • The empty changeset is correct — e2e runner, CI scripts, and a workbench route; nothing published changes.

One robustness nit, non-blocking:runId/traceId are spliced into the markdown link unvalidated. The artifact-trust model is consistent with the rest of this renderer, so it's not a security gap — but a cheap shape check (/^[a-z0-9_]+$/i) before rendering the link would keep a malformed value from a buggy deployment from breaking the whole comment's markdown, which is this section's own failure story.

CI: the 2 failures are the nextjs-webpack canary HMR rebuild-count test and its required-check aggregator — the long-running baseline flake, unreachable from a benchmark-comment change.

shalabhc added a commit that referenced this pull request Jul 31, 2026
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 22:49 Inactive
vercelBotand others added 4 commits August 1, 2026 16:00
The STSO distribution section added in #3213 shows the shape of the
sequential-steps run but not which run produced it, so investigating an
odd-looking bucket meant hunting for the run by deployment id and time
window.
Capture the identity alongside the samples (the mechanism prototyped on
the WIP variance branch, #3107): `/api/bench` returns the trace id of the
span @vercel/otel opened for the trigger request, the runner threads it
through the sequential iteration and records `sequentialRuns` in the
result file, and the renderer prints one line under the histograms with
the run id + Datadog trace link for this run and for the `main` run it is
diffed against.
Every part is optional — a deployment predating the route change yields a
bare run id, and a `main` baseline predating this yields only this run's
side — so the section degrades instead of breaking on mixed-vintage
artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The run id and Datadog trace are debugging aids, not part of the
benchmark's reported result, so they belong in the job's own output
rather than in the PR comment body.
Logging them where the runs are produced also makes them available in
two cases the comment could never cover: a local `pnpm bench`, and a
job that fails before the comment step runs.
This drops the comment-rendering side entirely -- `renderSequentialRunLinks`,
the `baselineSequentialRuns` baseline plumbing in `annotateWithBaseline`,
and the `sequentialRuns` field on the result artifact, which existed only
to carry the data to the renderer.
The `main`-baseline side of the link goes away with it: which run produced
the baseline histogram is only knowable at comment-render time, where the
two artifacts are matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Under the default linked trace mode the trigger's trace holds only
`workflow.start` plus span links, so opening it lands one hop away from
the spans an STSO investigation needs. Log an APM search on
`@workflow.run.id:<runId>` next to it, which goes straight to the run's
execution spans.
Both links are logged rather than one replacing the other: the search
depends on `workflow.run.id` being an indexed span tag in the org, and
the permalink works regardless.
Suggested by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for ba2cddc (AI decision).

This commit adds a new debugging affordance to the benchmark harness — the /api/bench route now returns the OTel trace id and benchmark.test.ts logs a run id plus two Datadog APM links per sequential-steps iteration. Nothing here fixes a defect, a flaky test, or a build/release problem on stable; it is additive tooling (the changeset is deliberately empty, i.e. no package behavior change). Feature/enhancement work stays on main.

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

ba2cddc861380560cc2ae980c9cb4e52a1530cc8

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

@shalabhc@TooTallNate
, '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

[benchmarks] Log the run id and Datadog trace for each sequential-steps run - #3248

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links
Aug 3, 2026
Merged

[benchmarks] Log the run id and Datadog trace for each sequential-steps run#3248
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links

Conversation

@shalabhc

@shalabhcshalabhc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Logs the run id and Datadog APM links for each sequential-steps benchmark iteration, in the benchmark job's own output:

[bench] 1020 steps run wrun_41KY… — trace https://app.datadoghq.com/apm/trace/6f1a… — spans https://app.datadoghq.com/apm/traces?query=%40workflow.run.id%3Awrun_41KY…

Follow-up to #3213 (which added the STSO histograms). Uses the run-id/trace capture prototyped on the WIP variance branch, #3107.

Why

The 📈 STSO distribution section shows the shape of the sequential-steps run but not which run produced it. When a bucket looks wrong, the next step is always the same manual hunt: find the run by deployment id + time window, then find its trace. This puts both in the log, next to the run that produced them.

These are debugging aids rather than part of the benchmark's reported result, so they go in the job output rather than the PR comment body. That also covers two cases a comment can't: a local pnpm bench, and a job that fails before the comment step runs.

What

  • workbench/nextjs-turbopack/app/api/bench/route.ts — returns traceId from the span @vercel/otel opened for the trigger request. @opentelemetry/api was already a dependency; no lockfile change.
  • packages/core/e2e/benchmark.test.ts — threads traceId through triggerBenchRunrunSequentialIteration, and logs one line per iteration as soon as the scenario completes.

A deployment built before the route reported a trace id logs the run id and span search, with no trace permalink.

Why two links

This deployment runs the default WORKFLOW_TRACE_MODE=linked — nothing in the workbench or benchmarks.yml sets the mode. Under linked, each workflow/step invocation is its own trace root, and the trigger's trace holds only workflow.start plus span links out to those roots. So the trace permalink is an entry point to the run (one hop through the links, which Datadog renders), not a single trace containing every step's spans.

The @workflow.run.id:<runId> span search lands directly on the run's execution spans instead, which is where an STSO investigation actually goes. It depends on workflow.run.id being an indexed span tag in the org, so it's logged alongside the permalink rather than replacing it — still to be confirmed against a real run.

Notes

The comment renderer is untouched, and the result artifact gains no new fields — the run identities are logged, not serialized.

One thing this deliberately drops relative to putting the line in the comment: there's no link to the main baseline's run. Which run produced the baseline histogram is only knowable at comment-render time, where the two artifacts are matched up; the benchmark job only knows about its own run.

Empty changeset — no package behavior changes.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6896fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production146602391705
✅ 💻 Local Development163302271860
✅ 📦 Local Production163302271860
✅ 🐘 Local Postgres163302271860
✅ 🪟 Windows15500155
✅ 📋 Other102802121240
✅ vercel-multi-region270027
Total7575011328707
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro127028
✅ example127028
✅ express127028
✅ fastify127028
✅ hono127028
✅ nextjs-turbopack15203
✅ nextjs-webpack15203
✅ nitro127028
✅ nuxt127028
✅ sveltekit14609
✅ vite127028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15500

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable129026
✅ e2e-local-dev-tanstack-start-129026
✅ e2e-local-postgres-nest-stable129026
✅ e2e-local-postgres-tanstack-start-129026
✅ e2e-local-prod-nest-stable129026
✅ e2e-local-prod-tanstack-start-129026
✅ e2e-vercel-prod-nest127028
✅ e2e-vercel-prod-tanstack-start127028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6896fb3 · Sat, 01 Aug 2026 16:24:50 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep504 (-43%) 💚1368 🔴 (+36%) 🔻1393 🔴 (+30%) 🔻1499 🔴 (+21%) 🔻30
TTFSstream1301 (+544%) 🔻1402 🔴 (+39%) 🔻1414 🔴 (+39%) 🔻1432 🔴 (+24%) 🔻30
TTFShook + stream1539 (+172%) 🔻1665 🔴 (+30%) 🔻1692 🔴 (+28%) 🔻1889 🔴 (+27%) 🔻30
STSO1020 steps (inline)178 (+9.2%)475 (+3.5%)535 (+0.8%)724 (-2.2%)1016
STSO1020 steps (queue-hop)2394 (+23%) 🔻3757 (+5.7%)3757 (+5.7%)3757 (+5.7%)3
WO1020 steps407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)1
SLstream latency108 (+35%) 🔻167 🔴 (+38%) 🔻183 🔴 (+33%) 🔻3785 🔴 (+2281%) 🔻30
SOstream overhead (text)111 (+17%) 🔻170 (+2.4%)194 (+3.7%)203 (-44%) 💚30
SOstream overhead (structured)101 (+4.1%)245 (+35%) 🔻287 (+46%) 🔻452 (+71%) 🔻30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 385272ms → this run 397249ms (Δ +11977ms, +3%)

 150-200 ms █┃████ main 49 this 18 -31
200-250 ms ███████████┃█ main 99 this 92 -7
250-300 ms █████████████████░░┃ main 132 this 161 +29
300-350 ms █████████████████┃██████ main 189 this 139 -50
350-400 ms ████████████████┃█████ main 171 this 135 -36
400-450 ms ███████████░░░░░░░┃ main 90 this 148 +58
450-500 ms █████████████████┃ main 130 this 144 +14
500-550 ms ██████████░┃ main 80 this 97 +17
550-600 ms ████┃ main 40 this 42 +2
600-650 ms █┃ main 11 this 18 +7
650-700 ms ┃ main 9 this 8 -1
700-750 ms ┃ main 6 this 7 +1
750-800 ms ┃ main 4 this 3 -1
800-850 ms ┃ main 1 this 0 -1
850-900 ms ┃ main 1 this 1 +0
900-950 ms ┃ main 0 this 1 +1
1000-1050 ms ┃ main 1 this 1 +0
1050-1100 ms ┃ main 1 this 1 +0
1700-1750 ms ┃ main 1 this 0 -1
2600-2650 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 7646ms → this run 9252ms (Δ +1606ms, +21%)

1500-2000 ms ┃███████████████████████ main 1 this 0 -1
2000-2500 ms ███████████████████████┃ main 1 this 1 +0
3000-3500 ms ░░░░░░░░░░░░░░░░░░░░░░░┃ main 0 this 1 +1
3500-4000 ms ███████████████████████┃ main 1 this 1 +0
📜 Previous results (4)

08567fd

Fri, 31 Jul 2026 23:21:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1331 (+97%) 🔻1437 🔴 (+43%) 🔻1461 🔴 (+42%) 🔻1501 🔴 (-4.9%)30
TTFSstream1262 (+39%) 🔻1383 🔴 (+48%) 🔻1394 🔴 (+47%) 🔻1425 🔴 (+47%) 🔻30
TTFShook + stream486 (+2.7%)1626 🔴 (+25%) 🔻1665 🔴 (+24%) 🔻1775 🔴 (+29%) 🔻30
STSO1020 steps (inline)160 (+24%) 🔻452 (±0%)508 (±0%)695 (-3.1%)1016
STSO1020 steps (queue-hop)2396 (+61%) 🔻3482 (+10%)3482 (+10%)3482 (+10%)3
WO1020 steps389282 (±0%)389282 (±0%)389282 (±0%)389282 (±0%)1
SLstream latency90 (+20%) 🔻135 🔴 (+17%) 🔻137 🔴 (+7.9%)2905 🔴 (+754%) 🔻30

75e3abe

Fri, 31 Jul 2026 18:10:29 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep268 (+1.9%)1435 🔴 (±0%)1520 🔴 (-5.5%)1887 🔴 (-6.0%)30
TTFSstream260 (+5.3%)1466 🔴 (+4.6%)1512 🔴 (-3.9%)1812 🔴 (-11%)30
TTFShook + stream346 (-14%)1596 🔴 (+23%) 🔻1656 🔴 (+22%) 🔻1845 🔴 (+26%) 🔻30
STSO1020 steps (inline)179 (+1.1%)502 (-2.3%)571 (-1.2%)782 (±0%)1016
STSO1020 steps (queue-hop)2133 (+44%) 🔻2807 (+0.7%)2807 (+0.7%)2807 (+0.7%)3
WO1020 steps438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)1
SLstream latency128 (+45%) 🔻177 🔴 (+12%)197 🔴 (+15%)354 🔴 (+80%) 🔻30
SOstream overhead (text)111 (-21%) 💚221 (+3.3%)273 (+21%) 🔻390 (-21%) 💚30
SOstream overhead (structured)119 (-11%)211 (-22%) 💚242 (-31%) 💚364 (-23%) 💚30

c784f6c

Fri, 31 Jul 2026 16:59:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1248 (+139%) 🔻1298 🔴 (+25%) 🔻1316 🔴 (+23%) 🔻1333 🔴 (-18%) 💚30
TTFSstream1250 (+32%) 🔻1303 🔴 (+30%) 🔻1317 🔴 (+28%) 🔻1360 🔴 (+5.5%)30
TTFShook + stream445 (+5.7%)1576 🔴 (+19%) 🔻1584 🔴 (+17%) 🔻1629 🔴 (-8.0%)30
STSO1020 steps (inline)172 (+5.5%)465 (-1.9%)522 (-1.7%)676 (+0.7%)1016
STSO1020 steps (queue-hop)2432 (+17%) 🔻3265 (+2.1%)3265 (+2.1%)3265 (+2.1%)3
WO1020 steps399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)1
SLstream latency90 (+9.8%)139 🔴 (+5.3%)175 🔴 (+19%) 🔻196 🔴 (-8.8%)30
SOstream overhead (text)102 (+1.0%)149 (-13%)196 (-8.4%)344 (+38%) 🔻30
SOstream overhead (structured)101 (-8.2%)179 (+5.9%)215 (+13%)322 (+41%) 🔻30

fedfc7a

Fri, 31 Jul 2026 01:20:43 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep911 (-9.4%)1286 🔴 (+18%) 🔻1321 🔴 (+20%) 🔻1405 🔴 (-12%)30
TTFSstream1239 (+236%) 🔻1279 🔴 (+21%) 🔻1298 🔴 (+20%) 🔻1366 🔴 (+23%) 🔻30
TTFShook + stream1387 (+177%) 🔻1526 🔴 (+9.0%)1586 🔴 (+8.0%)1628 🔴 (+8.8%)30
STSO1020 steps (inline)168 (-8.7%)466 (-5.1%)525 (-5.9%)780 (+3.2%)1016
STSO1020 steps (queue-hop)2241 (+9.0%)3376 (+21%) 🔻3376 (+21%) 🔻3376 (+21%) 🔻3
WO1020 steps399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)1
SLstream latency79 (-8.1%)144 🔴 (-1.4%)155 🔴 (-16%) 💚411 🔴 (+61%) 🔻30
SOstream overhead (text)105 (-23%) 💚146 (-71%) 💚178 (-81%) 💚205 (-83%) 💚30
SOstream overhead (structured)96 (-14%)162 (-21%) 💚223 (-15%)1745 🔴 (+294%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

@shalabhc
shalabhc marked this pull request as ready for review July 31, 2026 01:34
@shalabhc
shalabhc requested review from a team and ijjk as code ownersJuly 31, 2026 01:34

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the capture, the threading, and the renderer. Approving — this closes a real gap in the histogram section (I've done the deployment-id + time-window hunt this replaces), and the degradation matrix is genuinely complete: every optional field falls back the way the table claims, and the three new renderer tests cover the full/degraded/omitted cases (40/40 pass locally).

One correction worth making before merge — the route comment overclaims what the trace contains.route.ts says the trigger request's span "propagates into the workflow's own spans". That's only true under WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or benchmarks.yml sets the mode, so the benchmark deployment runs the default linked mode — where each workflow/step invocation is its own trace root, and the trigger request's trace contains only the workflow.start span plus span links to the execution roots. The link is still useful (one hop via span links, and Datadog renders those), but:

  • the comment should say what's actually there, so nobody debugging a weird histogram concludes the run "has no spans" when they open a trigger-only trace; and
  • consider making the link a Datadog APM search on workflow.run.id:<runId> instead of (or alongside) the trace permalink — under linked mode that lands on the run's actual execution spans directly, which is where an STSO investigation actually goes. The run id is already in hand.

Fine to take the comment fix now and the search-link idea as a follow-up — the current link is strictly better than nothing either way.

Verified:

  • The capture is correctly optional end-to-end: optional-chained getActiveSpan() (undefined without OTEL → bare run id), type-guarded on the runner side (typeof data.traceId === 'string'), and sequentialRuns omitted rather than [] when the scenario didn't run — so old artifacts, old deployments, and history re-renders all degrade exactly per the table.
  • Recording all iterations' run refs matches the data: the histograms aggregate samples across iterations, so with BENCH_SEQUENTIAL_ITERATIONS > 1 the comma-joined list is the honest answer. The doc phrase "both histograms come out of the same iteration" is only true for the default single-iteration config — worth a "(iterations, when configured)" tweak since the code already handles N correctly.
  • Baseline matching reuses the same methodology/backend/app key the percentile baselines use (resultKeyFor = the metric key minus the row), so the two sides of the diff can't come from mismatched configurations.
  • The empty changeset is correct — e2e runner, CI scripts, and a workbench route; nothing published changes.

One robustness nit, non-blocking:runId/traceId are spliced into the markdown link unvalidated. The artifact-trust model is consistent with the rest of this renderer, so it's not a security gap — but a cheap shape check (/^[a-z0-9_]+$/i) before rendering the link would keep a malformed value from a buggy deployment from breaking the whole comment's markdown, which is this section's own failure story.

CI: the 2 failures are the nextjs-webpack canary HMR rebuild-count test and its required-check aggregator — the long-running baseline flake, unreachable from a benchmark-comment change.

shalabhc added a commit that referenced this pull request Jul 31, 2026
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 22:49 Inactive
vercelBotand others added 4 commits August 1, 2026 16:00
The STSO distribution section added in #3213 shows the shape of the
sequential-steps run but not which run produced it, so investigating an
odd-looking bucket meant hunting for the run by deployment id and time
window.
Capture the identity alongside the samples (the mechanism prototyped on
the WIP variance branch, #3107): `/api/bench` returns the trace id of the
span @vercel/otel opened for the trigger request, the runner threads it
through the sequential iteration and records `sequentialRuns` in the
result file, and the renderer prints one line under the histograms with
the run id + Datadog trace link for this run and for the `main` run it is
diffed against.
Every part is optional — a deployment predating the route change yields a
bare run id, and a `main` baseline predating this yields only this run's
side — so the section degrades instead of breaking on mixed-vintage
artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The run id and Datadog trace are debugging aids, not part of the
benchmark's reported result, so they belong in the job's own output
rather than in the PR comment body.
Logging them where the runs are produced also makes them available in
two cases the comment could never cover: a local `pnpm bench`, and a
job that fails before the comment step runs.
This drops the comment-rendering side entirely -- `renderSequentialRunLinks`,
the `baselineSequentialRuns` baseline plumbing in `annotateWithBaseline`,
and the `sequentialRuns` field on the result artifact, which existed only
to carry the data to the renderer.
The `main`-baseline side of the link goes away with it: which run produced
the baseline histogram is only knowable at comment-render time, where the
two artifacts are matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Under the default linked trace mode the trigger's trace holds only
`workflow.start` plus span links, so opening it lands one hop away from
the spans an STSO investigation needs. Log an APM search on
`@workflow.run.id:<runId>` next to it, which goes straight to the run's
execution spans.
Both links are logged rather than one replacing the other: the search
depends on `workflow.run.id` being an indexed span tag in the org, and
the permalink works regardless.
Suggested by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for ba2cddc (AI decision).

This commit adds a new debugging affordance to the benchmark harness — the /api/bench route now returns the OTel trace id and benchmark.test.ts logs a run id plus two Datadog APM links per sequential-steps iteration. Nothing here fixes a defect, a flaky test, or a build/release problem on stable; it is additive tooling (the changeset is deliberately empty, i.e. no package behavior change). Feature/enhancement work stays on main.

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

ba2cddc861380560cc2ae980c9cb4e52a1530cc8

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

@shalabhc@TooTallNate
, '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

[benchmarks] Log the run id and Datadog trace for each sequential-steps run - #3248

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links
Aug 3, 2026
Merged

[benchmarks] Log the run id and Datadog trace for each sequential-steps run#3248
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links

Conversation

@shalabhc

@shalabhcshalabhc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Logs the run id and Datadog APM links for each sequential-steps benchmark iteration, in the benchmark job's own output:

[bench] 1020 steps run wrun_41KY… — trace https://app.datadoghq.com/apm/trace/6f1a… — spans https://app.datadoghq.com/apm/traces?query=%40workflow.run.id%3Awrun_41KY…

Follow-up to #3213 (which added the STSO histograms). Uses the run-id/trace capture prototyped on the WIP variance branch, #3107.

Why

The 📈 STSO distribution section shows the shape of the sequential-steps run but not which run produced it. When a bucket looks wrong, the next step is always the same manual hunt: find the run by deployment id + time window, then find its trace. This puts both in the log, next to the run that produced them.

These are debugging aids rather than part of the benchmark's reported result, so they go in the job output rather than the PR comment body. That also covers two cases a comment can't: a local pnpm bench, and a job that fails before the comment step runs.

What

  • workbench/nextjs-turbopack/app/api/bench/route.ts — returns traceId from the span @vercel/otel opened for the trigger request. @opentelemetry/api was already a dependency; no lockfile change.
  • packages/core/e2e/benchmark.test.ts — threads traceId through triggerBenchRunrunSequentialIteration, and logs one line per iteration as soon as the scenario completes.

A deployment built before the route reported a trace id logs the run id and span search, with no trace permalink.

Why two links

This deployment runs the default WORKFLOW_TRACE_MODE=linked — nothing in the workbench or benchmarks.yml sets the mode. Under linked, each workflow/step invocation is its own trace root, and the trigger's trace holds only workflow.start plus span links out to those roots. So the trace permalink is an entry point to the run (one hop through the links, which Datadog renders), not a single trace containing every step's spans.

The @workflow.run.id:<runId> span search lands directly on the run's execution spans instead, which is where an STSO investigation actually goes. It depends on workflow.run.id being an indexed span tag in the org, so it's logged alongside the permalink rather than replacing it — still to be confirmed against a real run.

Notes

The comment renderer is untouched, and the result artifact gains no new fields — the run identities are logged, not serialized.

One thing this deliberately drops relative to putting the line in the comment: there's no link to the main baseline's run. Which run produced the baseline histogram is only knowable at comment-render time, where the two artifacts are matched up; the benchmark job only knows about its own run.

Empty changeset — no package behavior changes.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6896fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production146602391705
✅ 💻 Local Development163302271860
✅ 📦 Local Production163302271860
✅ 🐘 Local Postgres163302271860
✅ 🪟 Windows15500155
✅ 📋 Other102802121240
✅ vercel-multi-region270027
Total7575011328707
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro127028
✅ example127028
✅ express127028
✅ fastify127028
✅ hono127028
✅ nextjs-turbopack15203
✅ nextjs-webpack15203
✅ nitro127028
✅ nuxt127028
✅ sveltekit14609
✅ vite127028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15500

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable129026
✅ e2e-local-dev-tanstack-start-129026
✅ e2e-local-postgres-nest-stable129026
✅ e2e-local-postgres-tanstack-start-129026
✅ e2e-local-prod-nest-stable129026
✅ e2e-local-prod-tanstack-start-129026
✅ e2e-vercel-prod-nest127028
✅ e2e-vercel-prod-tanstack-start127028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6896fb3 · Sat, 01 Aug 2026 16:24:50 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep504 (-43%) 💚1368 🔴 (+36%) 🔻1393 🔴 (+30%) 🔻1499 🔴 (+21%) 🔻30
TTFSstream1301 (+544%) 🔻1402 🔴 (+39%) 🔻1414 🔴 (+39%) 🔻1432 🔴 (+24%) 🔻30
TTFShook + stream1539 (+172%) 🔻1665 🔴 (+30%) 🔻1692 🔴 (+28%) 🔻1889 🔴 (+27%) 🔻30
STSO1020 steps (inline)178 (+9.2%)475 (+3.5%)535 (+0.8%)724 (-2.2%)1016
STSO1020 steps (queue-hop)2394 (+23%) 🔻3757 (+5.7%)3757 (+5.7%)3757 (+5.7%)3
WO1020 steps407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)1
SLstream latency108 (+35%) 🔻167 🔴 (+38%) 🔻183 🔴 (+33%) 🔻3785 🔴 (+2281%) 🔻30
SOstream overhead (text)111 (+17%) 🔻170 (+2.4%)194 (+3.7%)203 (-44%) 💚30
SOstream overhead (structured)101 (+4.1%)245 (+35%) 🔻287 (+46%) 🔻452 (+71%) 🔻30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 385272ms → this run 397249ms (Δ +11977ms, +3%)

 150-200 ms █┃████ main 49 this 18 -31
200-250 ms ███████████┃█ main 99 this 92 -7
250-300 ms █████████████████░░┃ main 132 this 161 +29
300-350 ms █████████████████┃██████ main 189 this 139 -50
350-400 ms ████████████████┃█████ main 171 this 135 -36
400-450 ms ███████████░░░░░░░┃ main 90 this 148 +58
450-500 ms █████████████████┃ main 130 this 144 +14
500-550 ms ██████████░┃ main 80 this 97 +17
550-600 ms ████┃ main 40 this 42 +2
600-650 ms █┃ main 11 this 18 +7
650-700 ms ┃ main 9 this 8 -1
700-750 ms ┃ main 6 this 7 +1
750-800 ms ┃ main 4 this 3 -1
800-850 ms ┃ main 1 this 0 -1
850-900 ms ┃ main 1 this 1 +0
900-950 ms ┃ main 0 this 1 +1
1000-1050 ms ┃ main 1 this 1 +0
1050-1100 ms ┃ main 1 this 1 +0
1700-1750 ms ┃ main 1 this 0 -1
2600-2650 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 7646ms → this run 9252ms (Δ +1606ms, +21%)

1500-2000 ms ┃███████████████████████ main 1 this 0 -1
2000-2500 ms ███████████████████████┃ main 1 this 1 +0
3000-3500 ms ░░░░░░░░░░░░░░░░░░░░░░░┃ main 0 this 1 +1
3500-4000 ms ███████████████████████┃ main 1 this 1 +0
📜 Previous results (4)

08567fd

Fri, 31 Jul 2026 23:21:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1331 (+97%) 🔻1437 🔴 (+43%) 🔻1461 🔴 (+42%) 🔻1501 🔴 (-4.9%)30
TTFSstream1262 (+39%) 🔻1383 🔴 (+48%) 🔻1394 🔴 (+47%) 🔻1425 🔴 (+47%) 🔻30
TTFShook + stream486 (+2.7%)1626 🔴 (+25%) 🔻1665 🔴 (+24%) 🔻1775 🔴 (+29%) 🔻30
STSO1020 steps (inline)160 (+24%) 🔻452 (±0%)508 (±0%)695 (-3.1%)1016
STSO1020 steps (queue-hop)2396 (+61%) 🔻3482 (+10%)3482 (+10%)3482 (+10%)3
WO1020 steps389282 (±0%)389282 (±0%)389282 (±0%)389282 (±0%)1
SLstream latency90 (+20%) 🔻135 🔴 (+17%) 🔻137 🔴 (+7.9%)2905 🔴 (+754%) 🔻30

75e3abe

Fri, 31 Jul 2026 18:10:29 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep268 (+1.9%)1435 🔴 (±0%)1520 🔴 (-5.5%)1887 🔴 (-6.0%)30
TTFSstream260 (+5.3%)1466 🔴 (+4.6%)1512 🔴 (-3.9%)1812 🔴 (-11%)30
TTFShook + stream346 (-14%)1596 🔴 (+23%) 🔻1656 🔴 (+22%) 🔻1845 🔴 (+26%) 🔻30
STSO1020 steps (inline)179 (+1.1%)502 (-2.3%)571 (-1.2%)782 (±0%)1016
STSO1020 steps (queue-hop)2133 (+44%) 🔻2807 (+0.7%)2807 (+0.7%)2807 (+0.7%)3
WO1020 steps438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)1
SLstream latency128 (+45%) 🔻177 🔴 (+12%)197 🔴 (+15%)354 🔴 (+80%) 🔻30
SOstream overhead (text)111 (-21%) 💚221 (+3.3%)273 (+21%) 🔻390 (-21%) 💚30
SOstream overhead (structured)119 (-11%)211 (-22%) 💚242 (-31%) 💚364 (-23%) 💚30

c784f6c

Fri, 31 Jul 2026 16:59:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1248 (+139%) 🔻1298 🔴 (+25%) 🔻1316 🔴 (+23%) 🔻1333 🔴 (-18%) 💚30
TTFSstream1250 (+32%) 🔻1303 🔴 (+30%) 🔻1317 🔴 (+28%) 🔻1360 🔴 (+5.5%)30
TTFShook + stream445 (+5.7%)1576 🔴 (+19%) 🔻1584 🔴 (+17%) 🔻1629 🔴 (-8.0%)30
STSO1020 steps (inline)172 (+5.5%)465 (-1.9%)522 (-1.7%)676 (+0.7%)1016
STSO1020 steps (queue-hop)2432 (+17%) 🔻3265 (+2.1%)3265 (+2.1%)3265 (+2.1%)3
WO1020 steps399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)1
SLstream latency90 (+9.8%)139 🔴 (+5.3%)175 🔴 (+19%) 🔻196 🔴 (-8.8%)30
SOstream overhead (text)102 (+1.0%)149 (-13%)196 (-8.4%)344 (+38%) 🔻30
SOstream overhead (structured)101 (-8.2%)179 (+5.9%)215 (+13%)322 (+41%) 🔻30

fedfc7a

Fri, 31 Jul 2026 01:20:43 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep911 (-9.4%)1286 🔴 (+18%) 🔻1321 🔴 (+20%) 🔻1405 🔴 (-12%)30
TTFSstream1239 (+236%) 🔻1279 🔴 (+21%) 🔻1298 🔴 (+20%) 🔻1366 🔴 (+23%) 🔻30
TTFShook + stream1387 (+177%) 🔻1526 🔴 (+9.0%)1586 🔴 (+8.0%)1628 🔴 (+8.8%)30
STSO1020 steps (inline)168 (-8.7%)466 (-5.1%)525 (-5.9%)780 (+3.2%)1016
STSO1020 steps (queue-hop)2241 (+9.0%)3376 (+21%) 🔻3376 (+21%) 🔻3376 (+21%) 🔻3
WO1020 steps399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)1
SLstream latency79 (-8.1%)144 🔴 (-1.4%)155 🔴 (-16%) 💚411 🔴 (+61%) 🔻30
SOstream overhead (text)105 (-23%) 💚146 (-71%) 💚178 (-81%) 💚205 (-83%) 💚30
SOstream overhead (structured)96 (-14%)162 (-21%) 💚223 (-15%)1745 🔴 (+294%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

@shalabhc
shalabhc marked this pull request as ready for review July 31, 2026 01:34
@shalabhc
shalabhc requested review from a team and ijjk as code ownersJuly 31, 2026 01:34

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the capture, the threading, and the renderer. Approving — this closes a real gap in the histogram section (I've done the deployment-id + time-window hunt this replaces), and the degradation matrix is genuinely complete: every optional field falls back the way the table claims, and the three new renderer tests cover the full/degraded/omitted cases (40/40 pass locally).

One correction worth making before merge — the route comment overclaims what the trace contains.route.ts says the trigger request's span "propagates into the workflow's own spans". That's only true under WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or benchmarks.yml sets the mode, so the benchmark deployment runs the default linked mode — where each workflow/step invocation is its own trace root, and the trigger request's trace contains only the workflow.start span plus span links to the execution roots. The link is still useful (one hop via span links, and Datadog renders those), but:

  • the comment should say what's actually there, so nobody debugging a weird histogram concludes the run "has no spans" when they open a trigger-only trace; and
  • consider making the link a Datadog APM search on workflow.run.id:<runId> instead of (or alongside) the trace permalink — under linked mode that lands on the run's actual execution spans directly, which is where an STSO investigation actually goes. The run id is already in hand.

Fine to take the comment fix now and the search-link idea as a follow-up — the current link is strictly better than nothing either way.

Verified:

  • The capture is correctly optional end-to-end: optional-chained getActiveSpan() (undefined without OTEL → bare run id), type-guarded on the runner side (typeof data.traceId === 'string'), and sequentialRuns omitted rather than [] when the scenario didn't run — so old artifacts, old deployments, and history re-renders all degrade exactly per the table.
  • Recording all iterations' run refs matches the data: the histograms aggregate samples across iterations, so with BENCH_SEQUENTIAL_ITERATIONS > 1 the comma-joined list is the honest answer. The doc phrase "both histograms come out of the same iteration" is only true for the default single-iteration config — worth a "(iterations, when configured)" tweak since the code already handles N correctly.
  • Baseline matching reuses the same methodology/backend/app key the percentile baselines use (resultKeyFor = the metric key minus the row), so the two sides of the diff can't come from mismatched configurations.
  • The empty changeset is correct — e2e runner, CI scripts, and a workbench route; nothing published changes.

One robustness nit, non-blocking:runId/traceId are spliced into the markdown link unvalidated. The artifact-trust model is consistent with the rest of this renderer, so it's not a security gap — but a cheap shape check (/^[a-z0-9_]+$/i) before rendering the link would keep a malformed value from a buggy deployment from breaking the whole comment's markdown, which is this section's own failure story.

CI: the 2 failures are the nextjs-webpack canary HMR rebuild-count test and its required-check aggregator — the long-running baseline flake, unreachable from a benchmark-comment change.

shalabhc added a commit that referenced this pull request Jul 31, 2026
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 22:49 Inactive
vercelBotand others added 4 commits August 1, 2026 16:00
The STSO distribution section added in #3213 shows the shape of the
sequential-steps run but not which run produced it, so investigating an
odd-looking bucket meant hunting for the run by deployment id and time
window.
Capture the identity alongside the samples (the mechanism prototyped on
the WIP variance branch, #3107): `/api/bench` returns the trace id of the
span @vercel/otel opened for the trigger request, the runner threads it
through the sequential iteration and records `sequentialRuns` in the
result file, and the renderer prints one line under the histograms with
the run id + Datadog trace link for this run and for the `main` run it is
diffed against.
Every part is optional — a deployment predating the route change yields a
bare run id, and a `main` baseline predating this yields only this run's
side — so the section degrades instead of breaking on mixed-vintage
artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The run id and Datadog trace are debugging aids, not part of the
benchmark's reported result, so they belong in the job's own output
rather than in the PR comment body.
Logging them where the runs are produced also makes them available in
two cases the comment could never cover: a local `pnpm bench`, and a
job that fails before the comment step runs.
This drops the comment-rendering side entirely -- `renderSequentialRunLinks`,
the `baselineSequentialRuns` baseline plumbing in `annotateWithBaseline`,
and the `sequentialRuns` field on the result artifact, which existed only
to carry the data to the renderer.
The `main`-baseline side of the link goes away with it: which run produced
the baseline histogram is only knowable at comment-render time, where the
two artifacts are matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Under the default linked trace mode the trigger's trace holds only
`workflow.start` plus span links, so opening it lands one hop away from
the spans an STSO investigation needs. Log an APM search on
`@workflow.run.id:<runId>` next to it, which goes straight to the run's
execution spans.
Both links are logged rather than one replacing the other: the search
depends on `workflow.run.id` being an indexed span tag in the org, and
the permalink works regardless.
Suggested by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for ba2cddc (AI decision).

This commit adds a new debugging affordance to the benchmark harness — the /api/bench route now returns the OTel trace id and benchmark.test.ts logs a run id plus two Datadog APM links per sequential-steps iteration. Nothing here fixes a defect, a flaky test, or a build/release problem on stable; it is additive tooling (the changeset is deliberately empty, i.e. no package behavior change). Feature/enhancement work stays on main.

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

ba2cddc861380560cc2ae980c9cb4e52a1530cc8

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

@shalabhc@TooTallNate
, '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

[benchmarks] Log the run id and Datadog trace for each sequential-steps run - #3248

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links
Aug 3, 2026
Merged

[benchmarks] Log the run id and Datadog trace for each sequential-steps run#3248
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links

Conversation

@shalabhc

@shalabhcshalabhc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Logs the run id and Datadog APM links for each sequential-steps benchmark iteration, in the benchmark job's own output:

[bench] 1020 steps run wrun_41KY… — trace https://app.datadoghq.com/apm/trace/6f1a… — spans https://app.datadoghq.com/apm/traces?query=%40workflow.run.id%3Awrun_41KY…

Follow-up to #3213 (which added the STSO histograms). Uses the run-id/trace capture prototyped on the WIP variance branch, #3107.

Why

The 📈 STSO distribution section shows the shape of the sequential-steps run but not which run produced it. When a bucket looks wrong, the next step is always the same manual hunt: find the run by deployment id + time window, then find its trace. This puts both in the log, next to the run that produced them.

These are debugging aids rather than part of the benchmark's reported result, so they go in the job output rather than the PR comment body. That also covers two cases a comment can't: a local pnpm bench, and a job that fails before the comment step runs.

What

  • workbench/nextjs-turbopack/app/api/bench/route.ts — returns traceId from the span @vercel/otel opened for the trigger request. @opentelemetry/api was already a dependency; no lockfile change.
  • packages/core/e2e/benchmark.test.ts — threads traceId through triggerBenchRunrunSequentialIteration, and logs one line per iteration as soon as the scenario completes.

A deployment built before the route reported a trace id logs the run id and span search, with no trace permalink.

Why two links

This deployment runs the default WORKFLOW_TRACE_MODE=linked — nothing in the workbench or benchmarks.yml sets the mode. Under linked, each workflow/step invocation is its own trace root, and the trigger's trace holds only workflow.start plus span links out to those roots. So the trace permalink is an entry point to the run (one hop through the links, which Datadog renders), not a single trace containing every step's spans.

The @workflow.run.id:<runId> span search lands directly on the run's execution spans instead, which is where an STSO investigation actually goes. It depends on workflow.run.id being an indexed span tag in the org, so it's logged alongside the permalink rather than replacing it — still to be confirmed against a real run.

Notes

The comment renderer is untouched, and the result artifact gains no new fields — the run identities are logged, not serialized.

One thing this deliberately drops relative to putting the line in the comment: there's no link to the main baseline's run. Which run produced the baseline histogram is only knowable at comment-render time, where the two artifacts are matched up; the benchmark job only knows about its own run.

Empty changeset — no package behavior changes.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6896fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production146602391705
✅ 💻 Local Development163302271860
✅ 📦 Local Production163302271860
✅ 🐘 Local Postgres163302271860
✅ 🪟 Windows15500155
✅ 📋 Other102802121240
✅ vercel-multi-region270027
Total7575011328707
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro127028
✅ example127028
✅ express127028
✅ fastify127028
✅ hono127028
✅ nextjs-turbopack15203
✅ nextjs-webpack15203
✅ nitro127028
✅ nuxt127028
✅ sveltekit14609
✅ vite127028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15500

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable129026
✅ e2e-local-dev-tanstack-start-129026
✅ e2e-local-postgres-nest-stable129026
✅ e2e-local-postgres-tanstack-start-129026
✅ e2e-local-prod-nest-stable129026
✅ e2e-local-prod-tanstack-start-129026
✅ e2e-vercel-prod-nest127028
✅ e2e-vercel-prod-tanstack-start127028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6896fb3 · Sat, 01 Aug 2026 16:24:50 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep504 (-43%) 💚1368 🔴 (+36%) 🔻1393 🔴 (+30%) 🔻1499 🔴 (+21%) 🔻30
TTFSstream1301 (+544%) 🔻1402 🔴 (+39%) 🔻1414 🔴 (+39%) 🔻1432 🔴 (+24%) 🔻30
TTFShook + stream1539 (+172%) 🔻1665 🔴 (+30%) 🔻1692 🔴 (+28%) 🔻1889 🔴 (+27%) 🔻30
STSO1020 steps (inline)178 (+9.2%)475 (+3.5%)535 (+0.8%)724 (-2.2%)1016
STSO1020 steps (queue-hop)2394 (+23%) 🔻3757 (+5.7%)3757 (+5.7%)3757 (+5.7%)3
WO1020 steps407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)1
SLstream latency108 (+35%) 🔻167 🔴 (+38%) 🔻183 🔴 (+33%) 🔻3785 🔴 (+2281%) 🔻30
SOstream overhead (text)111 (+17%) 🔻170 (+2.4%)194 (+3.7%)203 (-44%) 💚30
SOstream overhead (structured)101 (+4.1%)245 (+35%) 🔻287 (+46%) 🔻452 (+71%) 🔻30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 385272ms → this run 397249ms (Δ +11977ms, +3%)

 150-200 ms █┃████ main 49 this 18 -31
200-250 ms ███████████┃█ main 99 this 92 -7
250-300 ms █████████████████░░┃ main 132 this 161 +29
300-350 ms █████████████████┃██████ main 189 this 139 -50
350-400 ms ████████████████┃█████ main 171 this 135 -36
400-450 ms ███████████░░░░░░░┃ main 90 this 148 +58
450-500 ms █████████████████┃ main 130 this 144 +14
500-550 ms ██████████░┃ main 80 this 97 +17
550-600 ms ████┃ main 40 this 42 +2
600-650 ms █┃ main 11 this 18 +7
650-700 ms ┃ main 9 this 8 -1
700-750 ms ┃ main 6 this 7 +1
750-800 ms ┃ main 4 this 3 -1
800-850 ms ┃ main 1 this 0 -1
850-900 ms ┃ main 1 this 1 +0
900-950 ms ┃ main 0 this 1 +1
1000-1050 ms ┃ main 1 this 1 +0
1050-1100 ms ┃ main 1 this 1 +0
1700-1750 ms ┃ main 1 this 0 -1
2600-2650 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 7646ms → this run 9252ms (Δ +1606ms, +21%)

1500-2000 ms ┃███████████████████████ main 1 this 0 -1
2000-2500 ms ███████████████████████┃ main 1 this 1 +0
3000-3500 ms ░░░░░░░░░░░░░░░░░░░░░░░┃ main 0 this 1 +1
3500-4000 ms ███████████████████████┃ main 1 this 1 +0
📜 Previous results (4)

08567fd

Fri, 31 Jul 2026 23:21:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1331 (+97%) 🔻1437 🔴 (+43%) 🔻1461 🔴 (+42%) 🔻1501 🔴 (-4.9%)30
TTFSstream1262 (+39%) 🔻1383 🔴 (+48%) 🔻1394 🔴 (+47%) 🔻1425 🔴 (+47%) 🔻30
TTFShook + stream486 (+2.7%)1626 🔴 (+25%) 🔻1665 🔴 (+24%) 🔻1775 🔴 (+29%) 🔻30
STSO1020 steps (inline)160 (+24%) 🔻452 (±0%)508 (±0%)695 (-3.1%)1016
STSO1020 steps (queue-hop)2396 (+61%) 🔻3482 (+10%)3482 (+10%)3482 (+10%)3
WO1020 steps389282 (±0%)389282 (±0%)389282 (±0%)389282 (±0%)1
SLstream latency90 (+20%) 🔻135 🔴 (+17%) 🔻137 🔴 (+7.9%)2905 🔴 (+754%) 🔻30

75e3abe

Fri, 31 Jul 2026 18:10:29 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep268 (+1.9%)1435 🔴 (±0%)1520 🔴 (-5.5%)1887 🔴 (-6.0%)30
TTFSstream260 (+5.3%)1466 🔴 (+4.6%)1512 🔴 (-3.9%)1812 🔴 (-11%)30
TTFShook + stream346 (-14%)1596 🔴 (+23%) 🔻1656 🔴 (+22%) 🔻1845 🔴 (+26%) 🔻30
STSO1020 steps (inline)179 (+1.1%)502 (-2.3%)571 (-1.2%)782 (±0%)1016
STSO1020 steps (queue-hop)2133 (+44%) 🔻2807 (+0.7%)2807 (+0.7%)2807 (+0.7%)3
WO1020 steps438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)1
SLstream latency128 (+45%) 🔻177 🔴 (+12%)197 🔴 (+15%)354 🔴 (+80%) 🔻30
SOstream overhead (text)111 (-21%) 💚221 (+3.3%)273 (+21%) 🔻390 (-21%) 💚30
SOstream overhead (structured)119 (-11%)211 (-22%) 💚242 (-31%) 💚364 (-23%) 💚30

c784f6c

Fri, 31 Jul 2026 16:59:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1248 (+139%) 🔻1298 🔴 (+25%) 🔻1316 🔴 (+23%) 🔻1333 🔴 (-18%) 💚30
TTFSstream1250 (+32%) 🔻1303 🔴 (+30%) 🔻1317 🔴 (+28%) 🔻1360 🔴 (+5.5%)30
TTFShook + stream445 (+5.7%)1576 🔴 (+19%) 🔻1584 🔴 (+17%) 🔻1629 🔴 (-8.0%)30
STSO1020 steps (inline)172 (+5.5%)465 (-1.9%)522 (-1.7%)676 (+0.7%)1016
STSO1020 steps (queue-hop)2432 (+17%) 🔻3265 (+2.1%)3265 (+2.1%)3265 (+2.1%)3
WO1020 steps399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)1
SLstream latency90 (+9.8%)139 🔴 (+5.3%)175 🔴 (+19%) 🔻196 🔴 (-8.8%)30
SOstream overhead (text)102 (+1.0%)149 (-13%)196 (-8.4%)344 (+38%) 🔻30
SOstream overhead (structured)101 (-8.2%)179 (+5.9%)215 (+13%)322 (+41%) 🔻30

fedfc7a

Fri, 31 Jul 2026 01:20:43 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep911 (-9.4%)1286 🔴 (+18%) 🔻1321 🔴 (+20%) 🔻1405 🔴 (-12%)30
TTFSstream1239 (+236%) 🔻1279 🔴 (+21%) 🔻1298 🔴 (+20%) 🔻1366 🔴 (+23%) 🔻30
TTFShook + stream1387 (+177%) 🔻1526 🔴 (+9.0%)1586 🔴 (+8.0%)1628 🔴 (+8.8%)30
STSO1020 steps (inline)168 (-8.7%)466 (-5.1%)525 (-5.9%)780 (+3.2%)1016
STSO1020 steps (queue-hop)2241 (+9.0%)3376 (+21%) 🔻3376 (+21%) 🔻3376 (+21%) 🔻3
WO1020 steps399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)1
SLstream latency79 (-8.1%)144 🔴 (-1.4%)155 🔴 (-16%) 💚411 🔴 (+61%) 🔻30
SOstream overhead (text)105 (-23%) 💚146 (-71%) 💚178 (-81%) 💚205 (-83%) 💚30
SOstream overhead (structured)96 (-14%)162 (-21%) 💚223 (-15%)1745 🔴 (+294%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

@shalabhc
shalabhc marked this pull request as ready for review July 31, 2026 01:34
@shalabhc
shalabhc requested review from a team and ijjk as code ownersJuly 31, 2026 01:34

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the capture, the threading, and the renderer. Approving — this closes a real gap in the histogram section (I've done the deployment-id + time-window hunt this replaces), and the degradation matrix is genuinely complete: every optional field falls back the way the table claims, and the three new renderer tests cover the full/degraded/omitted cases (40/40 pass locally).

One correction worth making before merge — the route comment overclaims what the trace contains.route.ts says the trigger request's span "propagates into the workflow's own spans". That's only true under WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or benchmarks.yml sets the mode, so the benchmark deployment runs the default linked mode — where each workflow/step invocation is its own trace root, and the trigger request's trace contains only the workflow.start span plus span links to the execution roots. The link is still useful (one hop via span links, and Datadog renders those), but:

  • the comment should say what's actually there, so nobody debugging a weird histogram concludes the run "has no spans" when they open a trigger-only trace; and
  • consider making the link a Datadog APM search on workflow.run.id:<runId> instead of (or alongside) the trace permalink — under linked mode that lands on the run's actual execution spans directly, which is where an STSO investigation actually goes. The run id is already in hand.

Fine to take the comment fix now and the search-link idea as a follow-up — the current link is strictly better than nothing either way.

Verified:

  • The capture is correctly optional end-to-end: optional-chained getActiveSpan() (undefined without OTEL → bare run id), type-guarded on the runner side (typeof data.traceId === 'string'), and sequentialRuns omitted rather than [] when the scenario didn't run — so old artifacts, old deployments, and history re-renders all degrade exactly per the table.
  • Recording all iterations' run refs matches the data: the histograms aggregate samples across iterations, so with BENCH_SEQUENTIAL_ITERATIONS > 1 the comma-joined list is the honest answer. The doc phrase "both histograms come out of the same iteration" is only true for the default single-iteration config — worth a "(iterations, when configured)" tweak since the code already handles N correctly.
  • Baseline matching reuses the same methodology/backend/app key the percentile baselines use (resultKeyFor = the metric key minus the row), so the two sides of the diff can't come from mismatched configurations.
  • The empty changeset is correct — e2e runner, CI scripts, and a workbench route; nothing published changes.

One robustness nit, non-blocking:runId/traceId are spliced into the markdown link unvalidated. The artifact-trust model is consistent with the rest of this renderer, so it's not a security gap — but a cheap shape check (/^[a-z0-9_]+$/i) before rendering the link would keep a malformed value from a buggy deployment from breaking the whole comment's markdown, which is this section's own failure story.

CI: the 2 failures are the nextjs-webpack canary HMR rebuild-count test and its required-check aggregator — the long-running baseline flake, unreachable from a benchmark-comment change.

shalabhc added a commit that referenced this pull request Jul 31, 2026
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 22:49 Inactive
vercelBotand others added 4 commits August 1, 2026 16:00
The STSO distribution section added in #3213 shows the shape of the
sequential-steps run but not which run produced it, so investigating an
odd-looking bucket meant hunting for the run by deployment id and time
window.
Capture the identity alongside the samples (the mechanism prototyped on
the WIP variance branch, #3107): `/api/bench` returns the trace id of the
span @vercel/otel opened for the trigger request, the runner threads it
through the sequential iteration and records `sequentialRuns` in the
result file, and the renderer prints one line under the histograms with
the run id + Datadog trace link for this run and for the `main` run it is
diffed against.
Every part is optional — a deployment predating the route change yields a
bare run id, and a `main` baseline predating this yields only this run's
side — so the section degrades instead of breaking on mixed-vintage
artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The run id and Datadog trace are debugging aids, not part of the
benchmark's reported result, so they belong in the job's own output
rather than in the PR comment body.
Logging them where the runs are produced also makes them available in
two cases the comment could never cover: a local `pnpm bench`, and a
job that fails before the comment step runs.
This drops the comment-rendering side entirely -- `renderSequentialRunLinks`,
the `baselineSequentialRuns` baseline plumbing in `annotateWithBaseline`,
and the `sequentialRuns` field on the result artifact, which existed only
to carry the data to the renderer.
The `main`-baseline side of the link goes away with it: which run produced
the baseline histogram is only knowable at comment-render time, where the
two artifacts are matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Under the default linked trace mode the trigger's trace holds only
`workflow.start` plus span links, so opening it lands one hop away from
the spans an STSO investigation needs. Log an APM search on
`@workflow.run.id:<runId>` next to it, which goes straight to the run's
execution spans.
Both links are logged rather than one replacing the other: the search
depends on `workflow.run.id` being an indexed span tag in the org, and
the permalink works regardless.
Suggested by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for ba2cddc (AI decision).

This commit adds a new debugging affordance to the benchmark harness — the /api/bench route now returns the OTel trace id and benchmark.test.ts logs a run id plus two Datadog APM links per sequential-steps iteration. Nothing here fixes a defect, a flaky test, or a build/release problem on stable; it is additive tooling (the changeset is deliberately empty, i.e. no package behavior change). Feature/enhancement work stays on main.

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

ba2cddc861380560cc2ae980c9cb4e52a1530cc8

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

@shalabhc@TooTallNate
, '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

[benchmarks] Log the run id and Datadog trace for each sequential-steps run - #3248

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links
Aug 3, 2026
Merged

[benchmarks] Log the run id and Datadog trace for each sequential-steps run#3248
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links

Conversation

@shalabhc

@shalabhcshalabhc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Logs the run id and Datadog APM links for each sequential-steps benchmark iteration, in the benchmark job's own output:

[bench] 1020 steps run wrun_41KY… — trace https://app.datadoghq.com/apm/trace/6f1a… — spans https://app.datadoghq.com/apm/traces?query=%40workflow.run.id%3Awrun_41KY…

Follow-up to #3213 (which added the STSO histograms). Uses the run-id/trace capture prototyped on the WIP variance branch, #3107.

Why

The 📈 STSO distribution section shows the shape of the sequential-steps run but not which run produced it. When a bucket looks wrong, the next step is always the same manual hunt: find the run by deployment id + time window, then find its trace. This puts both in the log, next to the run that produced them.

These are debugging aids rather than part of the benchmark's reported result, so they go in the job output rather than the PR comment body. That also covers two cases a comment can't: a local pnpm bench, and a job that fails before the comment step runs.

What

  • workbench/nextjs-turbopack/app/api/bench/route.ts — returns traceId from the span @vercel/otel opened for the trigger request. @opentelemetry/api was already a dependency; no lockfile change.
  • packages/core/e2e/benchmark.test.ts — threads traceId through triggerBenchRunrunSequentialIteration, and logs one line per iteration as soon as the scenario completes.

A deployment built before the route reported a trace id logs the run id and span search, with no trace permalink.

Why two links

This deployment runs the default WORKFLOW_TRACE_MODE=linked — nothing in the workbench or benchmarks.yml sets the mode. Under linked, each workflow/step invocation is its own trace root, and the trigger's trace holds only workflow.start plus span links out to those roots. So the trace permalink is an entry point to the run (one hop through the links, which Datadog renders), not a single trace containing every step's spans.

The @workflow.run.id:<runId> span search lands directly on the run's execution spans instead, which is where an STSO investigation actually goes. It depends on workflow.run.id being an indexed span tag in the org, so it's logged alongside the permalink rather than replacing it — still to be confirmed against a real run.

Notes

The comment renderer is untouched, and the result artifact gains no new fields — the run identities are logged, not serialized.

One thing this deliberately drops relative to putting the line in the comment: there's no link to the main baseline's run. Which run produced the baseline histogram is only knowable at comment-render time, where the two artifacts are matched up; the benchmark job only knows about its own run.

Empty changeset — no package behavior changes.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6896fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production146602391705
✅ 💻 Local Development163302271860
✅ 📦 Local Production163302271860
✅ 🐘 Local Postgres163302271860
✅ 🪟 Windows15500155
✅ 📋 Other102802121240
✅ vercel-multi-region270027
Total7575011328707
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro127028
✅ example127028
✅ express127028
✅ fastify127028
✅ hono127028
✅ nextjs-turbopack15203
✅ nextjs-webpack15203
✅ nitro127028
✅ nuxt127028
✅ sveltekit14609
✅ vite127028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15500

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable129026
✅ e2e-local-dev-tanstack-start-129026
✅ e2e-local-postgres-nest-stable129026
✅ e2e-local-postgres-tanstack-start-129026
✅ e2e-local-prod-nest-stable129026
✅ e2e-local-prod-tanstack-start-129026
✅ e2e-vercel-prod-nest127028
✅ e2e-vercel-prod-tanstack-start127028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6896fb3 · Sat, 01 Aug 2026 16:24:50 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep504 (-43%) 💚1368 🔴 (+36%) 🔻1393 🔴 (+30%) 🔻1499 🔴 (+21%) 🔻30
TTFSstream1301 (+544%) 🔻1402 🔴 (+39%) 🔻1414 🔴 (+39%) 🔻1432 🔴 (+24%) 🔻30
TTFShook + stream1539 (+172%) 🔻1665 🔴 (+30%) 🔻1692 🔴 (+28%) 🔻1889 🔴 (+27%) 🔻30
STSO1020 steps (inline)178 (+9.2%)475 (+3.5%)535 (+0.8%)724 (-2.2%)1016
STSO1020 steps (queue-hop)2394 (+23%) 🔻3757 (+5.7%)3757 (+5.7%)3757 (+5.7%)3
WO1020 steps407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)1
SLstream latency108 (+35%) 🔻167 🔴 (+38%) 🔻183 🔴 (+33%) 🔻3785 🔴 (+2281%) 🔻30
SOstream overhead (text)111 (+17%) 🔻170 (+2.4%)194 (+3.7%)203 (-44%) 💚30
SOstream overhead (structured)101 (+4.1%)245 (+35%) 🔻287 (+46%) 🔻452 (+71%) 🔻30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 385272ms → this run 397249ms (Δ +11977ms, +3%)

 150-200 ms █┃████ main 49 this 18 -31
200-250 ms ███████████┃█ main 99 this 92 -7
250-300 ms █████████████████░░┃ main 132 this 161 +29
300-350 ms █████████████████┃██████ main 189 this 139 -50
350-400 ms ████████████████┃█████ main 171 this 135 -36
400-450 ms ███████████░░░░░░░┃ main 90 this 148 +58
450-500 ms █████████████████┃ main 130 this 144 +14
500-550 ms ██████████░┃ main 80 this 97 +17
550-600 ms ████┃ main 40 this 42 +2
600-650 ms █┃ main 11 this 18 +7
650-700 ms ┃ main 9 this 8 -1
700-750 ms ┃ main 6 this 7 +1
750-800 ms ┃ main 4 this 3 -1
800-850 ms ┃ main 1 this 0 -1
850-900 ms ┃ main 1 this 1 +0
900-950 ms ┃ main 0 this 1 +1
1000-1050 ms ┃ main 1 this 1 +0
1050-1100 ms ┃ main 1 this 1 +0
1700-1750 ms ┃ main 1 this 0 -1
2600-2650 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 7646ms → this run 9252ms (Δ +1606ms, +21%)

1500-2000 ms ┃███████████████████████ main 1 this 0 -1
2000-2500 ms ███████████████████████┃ main 1 this 1 +0
3000-3500 ms ░░░░░░░░░░░░░░░░░░░░░░░┃ main 0 this 1 +1
3500-4000 ms ███████████████████████┃ main 1 this 1 +0
📜 Previous results (4)

08567fd

Fri, 31 Jul 2026 23:21:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1331 (+97%) 🔻1437 🔴 (+43%) 🔻1461 🔴 (+42%) 🔻1501 🔴 (-4.9%)30
TTFSstream1262 (+39%) 🔻1383 🔴 (+48%) 🔻1394 🔴 (+47%) 🔻1425 🔴 (+47%) 🔻30
TTFShook + stream486 (+2.7%)1626 🔴 (+25%) 🔻1665 🔴 (+24%) 🔻1775 🔴 (+29%) 🔻30
STSO1020 steps (inline)160 (+24%) 🔻452 (±0%)508 (±0%)695 (-3.1%)1016
STSO1020 steps (queue-hop)2396 (+61%) 🔻3482 (+10%)3482 (+10%)3482 (+10%)3
WO1020 steps389282 (±0%)389282 (±0%)389282 (±0%)389282 (±0%)1
SLstream latency90 (+20%) 🔻135 🔴 (+17%) 🔻137 🔴 (+7.9%)2905 🔴 (+754%) 🔻30

75e3abe

Fri, 31 Jul 2026 18:10:29 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep268 (+1.9%)1435 🔴 (±0%)1520 🔴 (-5.5%)1887 🔴 (-6.0%)30
TTFSstream260 (+5.3%)1466 🔴 (+4.6%)1512 🔴 (-3.9%)1812 🔴 (-11%)30
TTFShook + stream346 (-14%)1596 🔴 (+23%) 🔻1656 🔴 (+22%) 🔻1845 🔴 (+26%) 🔻30
STSO1020 steps (inline)179 (+1.1%)502 (-2.3%)571 (-1.2%)782 (±0%)1016
STSO1020 steps (queue-hop)2133 (+44%) 🔻2807 (+0.7%)2807 (+0.7%)2807 (+0.7%)3
WO1020 steps438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)1
SLstream latency128 (+45%) 🔻177 🔴 (+12%)197 🔴 (+15%)354 🔴 (+80%) 🔻30
SOstream overhead (text)111 (-21%) 💚221 (+3.3%)273 (+21%) 🔻390 (-21%) 💚30
SOstream overhead (structured)119 (-11%)211 (-22%) 💚242 (-31%) 💚364 (-23%) 💚30

c784f6c

Fri, 31 Jul 2026 16:59:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1248 (+139%) 🔻1298 🔴 (+25%) 🔻1316 🔴 (+23%) 🔻1333 🔴 (-18%) 💚30
TTFSstream1250 (+32%) 🔻1303 🔴 (+30%) 🔻1317 🔴 (+28%) 🔻1360 🔴 (+5.5%)30
TTFShook + stream445 (+5.7%)1576 🔴 (+19%) 🔻1584 🔴 (+17%) 🔻1629 🔴 (-8.0%)30
STSO1020 steps (inline)172 (+5.5%)465 (-1.9%)522 (-1.7%)676 (+0.7%)1016
STSO1020 steps (queue-hop)2432 (+17%) 🔻3265 (+2.1%)3265 (+2.1%)3265 (+2.1%)3
WO1020 steps399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)1
SLstream latency90 (+9.8%)139 🔴 (+5.3%)175 🔴 (+19%) 🔻196 🔴 (-8.8%)30
SOstream overhead (text)102 (+1.0%)149 (-13%)196 (-8.4%)344 (+38%) 🔻30
SOstream overhead (structured)101 (-8.2%)179 (+5.9%)215 (+13%)322 (+41%) 🔻30

fedfc7a

Fri, 31 Jul 2026 01:20:43 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep911 (-9.4%)1286 🔴 (+18%) 🔻1321 🔴 (+20%) 🔻1405 🔴 (-12%)30
TTFSstream1239 (+236%) 🔻1279 🔴 (+21%) 🔻1298 🔴 (+20%) 🔻1366 🔴 (+23%) 🔻30
TTFShook + stream1387 (+177%) 🔻1526 🔴 (+9.0%)1586 🔴 (+8.0%)1628 🔴 (+8.8%)30
STSO1020 steps (inline)168 (-8.7%)466 (-5.1%)525 (-5.9%)780 (+3.2%)1016
STSO1020 steps (queue-hop)2241 (+9.0%)3376 (+21%) 🔻3376 (+21%) 🔻3376 (+21%) 🔻3
WO1020 steps399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)1
SLstream latency79 (-8.1%)144 🔴 (-1.4%)155 🔴 (-16%) 💚411 🔴 (+61%) 🔻30
SOstream overhead (text)105 (-23%) 💚146 (-71%) 💚178 (-81%) 💚205 (-83%) 💚30
SOstream overhead (structured)96 (-14%)162 (-21%) 💚223 (-15%)1745 🔴 (+294%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

@shalabhc
shalabhc marked this pull request as ready for review July 31, 2026 01:34
@shalabhc
shalabhc requested review from a team and ijjk as code ownersJuly 31, 2026 01:34

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the capture, the threading, and the renderer. Approving — this closes a real gap in the histogram section (I've done the deployment-id + time-window hunt this replaces), and the degradation matrix is genuinely complete: every optional field falls back the way the table claims, and the three new renderer tests cover the full/degraded/omitted cases (40/40 pass locally).

One correction worth making before merge — the route comment overclaims what the trace contains.route.ts says the trigger request's span "propagates into the workflow's own spans". That's only true under WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or benchmarks.yml sets the mode, so the benchmark deployment runs the default linked mode — where each workflow/step invocation is its own trace root, and the trigger request's trace contains only the workflow.start span plus span links to the execution roots. The link is still useful (one hop via span links, and Datadog renders those), but:

  • the comment should say what's actually there, so nobody debugging a weird histogram concludes the run "has no spans" when they open a trigger-only trace; and
  • consider making the link a Datadog APM search on workflow.run.id:<runId> instead of (or alongside) the trace permalink — under linked mode that lands on the run's actual execution spans directly, which is where an STSO investigation actually goes. The run id is already in hand.

Fine to take the comment fix now and the search-link idea as a follow-up — the current link is strictly better than nothing either way.

Verified:

  • The capture is correctly optional end-to-end: optional-chained getActiveSpan() (undefined without OTEL → bare run id), type-guarded on the runner side (typeof data.traceId === 'string'), and sequentialRuns omitted rather than [] when the scenario didn't run — so old artifacts, old deployments, and history re-renders all degrade exactly per the table.
  • Recording all iterations' run refs matches the data: the histograms aggregate samples across iterations, so with BENCH_SEQUENTIAL_ITERATIONS > 1 the comma-joined list is the honest answer. The doc phrase "both histograms come out of the same iteration" is only true for the default single-iteration config — worth a "(iterations, when configured)" tweak since the code already handles N correctly.
  • Baseline matching reuses the same methodology/backend/app key the percentile baselines use (resultKeyFor = the metric key minus the row), so the two sides of the diff can't come from mismatched configurations.
  • The empty changeset is correct — e2e runner, CI scripts, and a workbench route; nothing published changes.

One robustness nit, non-blocking:runId/traceId are spliced into the markdown link unvalidated. The artifact-trust model is consistent with the rest of this renderer, so it's not a security gap — but a cheap shape check (/^[a-z0-9_]+$/i) before rendering the link would keep a malformed value from a buggy deployment from breaking the whole comment's markdown, which is this section's own failure story.

CI: the 2 failures are the nextjs-webpack canary HMR rebuild-count test and its required-check aggregator — the long-running baseline flake, unreachable from a benchmark-comment change.

shalabhc added a commit that referenced this pull request Jul 31, 2026
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 22:49 Inactive
vercelBotand others added 4 commits August 1, 2026 16:00
The STSO distribution section added in #3213 shows the shape of the
sequential-steps run but not which run produced it, so investigating an
odd-looking bucket meant hunting for the run by deployment id and time
window.
Capture the identity alongside the samples (the mechanism prototyped on
the WIP variance branch, #3107): `/api/bench` returns the trace id of the
span @vercel/otel opened for the trigger request, the runner threads it
through the sequential iteration and records `sequentialRuns` in the
result file, and the renderer prints one line under the histograms with
the run id + Datadog trace link for this run and for the `main` run it is
diffed against.
Every part is optional — a deployment predating the route change yields a
bare run id, and a `main` baseline predating this yields only this run's
side — so the section degrades instead of breaking on mixed-vintage
artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The run id and Datadog trace are debugging aids, not part of the
benchmark's reported result, so they belong in the job's own output
rather than in the PR comment body.
Logging them where the runs are produced also makes them available in
two cases the comment could never cover: a local `pnpm bench`, and a
job that fails before the comment step runs.
This drops the comment-rendering side entirely -- `renderSequentialRunLinks`,
the `baselineSequentialRuns` baseline plumbing in `annotateWithBaseline`,
and the `sequentialRuns` field on the result artifact, which existed only
to carry the data to the renderer.
The `main`-baseline side of the link goes away with it: which run produced
the baseline histogram is only knowable at comment-render time, where the
two artifacts are matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Under the default linked trace mode the trigger's trace holds only
`workflow.start` plus span links, so opening it lands one hop away from
the spans an STSO investigation needs. Log an APM search on
`@workflow.run.id:<runId>` next to it, which goes straight to the run's
execution spans.
Both links are logged rather than one replacing the other: the search
depends on `workflow.run.id` being an indexed span tag in the org, and
the permalink works regardless.
Suggested by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for ba2cddc (AI decision).

This commit adds a new debugging affordance to the benchmark harness — the /api/bench route now returns the OTel trace id and benchmark.test.ts logs a run id plus two Datadog APM links per sequential-steps iteration. Nothing here fixes a defect, a flaky test, or a build/release problem on stable; it is additive tooling (the changeset is deliberately empty, i.e. no package behavior change). Feature/enhancement work stays on main.

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

ba2cddc861380560cc2ae980c9cb4e52a1530cc8

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

@shalabhc@TooTallNate
, '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

[benchmarks] Log the run id and Datadog trace for each sequential-steps run - #3248

Merged
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links
Aug 3, 2026
Merged

[benchmarks] Log the run id and Datadog trace for each sequential-steps run#3248
shalabhc merged 4 commits into
mainfrom
shalabhc/bench-histogram-run-trace-links

Conversation

@shalabhc

@shalabhcshalabhc commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Logs the run id and Datadog APM links for each sequential-steps benchmark iteration, in the benchmark job's own output:

[bench] 1020 steps run wrun_41KY… — trace https://app.datadoghq.com/apm/trace/6f1a… — spans https://app.datadoghq.com/apm/traces?query=%40workflow.run.id%3Awrun_41KY…

Follow-up to #3213 (which added the STSO histograms). Uses the run-id/trace capture prototyped on the WIP variance branch, #3107.

Why

The 📈 STSO distribution section shows the shape of the sequential-steps run but not which run produced it. When a bucket looks wrong, the next step is always the same manual hunt: find the run by deployment id + time window, then find its trace. This puts both in the log, next to the run that produced them.

These are debugging aids rather than part of the benchmark's reported result, so they go in the job output rather than the PR comment body. That also covers two cases a comment can't: a local pnpm bench, and a job that fails before the comment step runs.

What

  • workbench/nextjs-turbopack/app/api/bench/route.ts — returns traceId from the span @vercel/otel opened for the trigger request. @opentelemetry/api was already a dependency; no lockfile change.
  • packages/core/e2e/benchmark.test.ts — threads traceId through triggerBenchRunrunSequentialIteration, and logs one line per iteration as soon as the scenario completes.

A deployment built before the route reported a trace id logs the run id and span search, with no trace permalink.

Why two links

This deployment runs the default WORKFLOW_TRACE_MODE=linked — nothing in the workbench or benchmarks.yml sets the mode. Under linked, each workflow/step invocation is its own trace root, and the trigger's trace holds only workflow.start plus span links out to those roots. So the trace permalink is an entry point to the run (one hop through the links, which Datadog renders), not a single trace containing every step's spans.

The @workflow.run.id:<runId> span search lands directly on the run's execution spans instead, which is where an STSO investigation actually goes. It depends on workflow.run.id being an indexed span tag in the org, so it's logged alongside the permalink rather than replacing it — still to be confirmed against a real run.

Notes

The comment renderer is untouched, and the result artifact gains no new fields — the run identities are logged, not serialized.

One thing this deliberately drops relative to putting the line in the comment: there's no link to the main baseline's run. Which run produced the baseline histogram is only knowable at comment-render time, where the two artifacts are matched up; the benchmark job only knows about its own run.

Empty changeset — no package behavior changes.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

@changeset-bot

changeset-botBot commented Jul 31, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6896fb3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 0 packages

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

Not sure what this means? Click here to learn what changesets are.

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

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

E2E Test Summary

Summary
PassedFailedSkippedTotal
✅ ▲ Vercel Production146602391705
✅ 💻 Local Development163302271860
✅ 📦 Local Production163302271860
✅ 🐘 Local Postgres163302271860
✅ 🪟 Windows15500155
✅ 📋 Other102802121240
✅ vercel-multi-region270027
Total7575011328707
Details by Category

✅ ▲ Vercel Production

AppPassedFailedSkipped
✅ astro127028
✅ example127028
✅ express127028
✅ fastify127028
✅ hono127028
✅ nextjs-turbopack15203
✅ nextjs-webpack15203
✅ nitro127028
✅ nuxt127028
✅ sveltekit14609
✅ vite127028

✅ 💻 Local Development

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 📦 Local Production

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🐘 Local Postgres

AppPassedFailedSkipped
✅ astro-stable129026
✅ express-stable129026
✅ fastify-stable129026
✅ hono-stable129026
✅ nextjs-turbopack-canary136019
✅ nextjs-turbopack-stable15500
✅ nextjs-webpack-canary136019
✅ nextjs-webpack-stable15500
✅ nitro-stable129026
✅ nuxt-stable129026
✅ sveltekit-stable14807
✅ vite-stable129026

✅ 🪟 Windows

AppPassedFailedSkipped
✅ nextjs-turbopack15500

✅ 📋 Other

AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable129026
✅ e2e-local-dev-tanstack-start-129026
✅ e2e-local-postgres-nest-stable129026
✅ e2e-local-postgres-tanstack-start-129026
✅ e2e-local-prod-nest-stable129026
✅ e2e-local-prod-tanstack-start-129026
✅ e2e-vercel-prod-nest127028
✅ e2e-vercel-prod-tanstack-start127028

✅ vercel-multi-region

AppPassedFailedSkipped
✅ nextjs-turbopack2700

📋 View full workflow run

@github-actions

github-actionsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

📊 Workflow Benchmarks

commit 6896fb3 · Sat, 01 Aug 2026 16:24:50 GMT · run logs

Backend: vercel · app: nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep504 (-43%) 💚1368 🔴 (+36%) 🔻1393 🔴 (+30%) 🔻1499 🔴 (+21%) 🔻30
TTFSstream1301 (+544%) 🔻1402 🔴 (+39%) 🔻1414 🔴 (+39%) 🔻1432 🔴 (+24%) 🔻30
TTFShook + stream1539 (+172%) 🔻1665 🔴 (+30%) 🔻1692 🔴 (+28%) 🔻1889 🔴 (+27%) 🔻30
STSO1020 steps (inline)178 (+9.2%)475 (+3.5%)535 (+0.8%)724 (-2.2%)1016
STSO1020 steps (queue-hop)2394 (+23%) 🔻3757 (+5.7%)3757 (+5.7%)3757 (+5.7%)3
WO1020 steps407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)407889 (+3.5%)1
SLstream latency108 (+35%) 🔻167 🔴 (+38%) 🔻183 🔴 (+33%) 🔻3785 🔴 (+2281%) 🔻30
SOstream overhead (text)111 (+17%) 🔻170 (+2.4%)194 (+3.7%)203 (-44%) 💚30
SOstream overhead (structured)101 (+4.1%)245 (+35%) 🔻287 (+46%) 🔻452 (+71%) 🔻30
📈 STSO distribution vs main (inline / queue-hop histograms)

1020 steps (inline)

Cumulative STSO time: main 385272ms → this run 397249ms (Δ +11977ms, +3%)

 150-200 ms █┃████ main 49 this 18 -31
200-250 ms ███████████┃█ main 99 this 92 -7
250-300 ms █████████████████░░┃ main 132 this 161 +29
300-350 ms █████████████████┃██████ main 189 this 139 -50
350-400 ms ████████████████┃█████ main 171 this 135 -36
400-450 ms ███████████░░░░░░░┃ main 90 this 148 +58
450-500 ms █████████████████┃ main 130 this 144 +14
500-550 ms ██████████░┃ main 80 this 97 +17
550-600 ms ████┃ main 40 this 42 +2
600-650 ms █┃ main 11 this 18 +7
650-700 ms ┃ main 9 this 8 -1
700-750 ms ┃ main 6 this 7 +1
750-800 ms ┃ main 4 this 3 -1
800-850 ms ┃ main 1 this 0 -1
850-900 ms ┃ main 1 this 1 +0
900-950 ms ┃ main 0 this 1 +1
1000-1050 ms ┃ main 1 this 1 +0
1050-1100 ms ┃ main 1 this 1 +0
1700-1750 ms ┃ main 1 this 0 -1
2600-2650 ms ┃ main 1 this 0 -1

1020 steps (queue-hop)

Cumulative STSO time: main 7646ms → this run 9252ms (Δ +1606ms, +21%)

1500-2000 ms ┃███████████████████████ main 1 this 0 -1
2000-2500 ms ███████████████████████┃ main 1 this 1 +0
3000-3500 ms ░░░░░░░░░░░░░░░░░░░░░░░┃ main 0 this 1 +1
3500-4000 ms ███████████████████████┃ main 1 this 1 +0
📜 Previous results (4)

08567fd

Fri, 31 Jul 2026 23:21:18 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1331 (+97%) 🔻1437 🔴 (+43%) 🔻1461 🔴 (+42%) 🔻1501 🔴 (-4.9%)30
TTFSstream1262 (+39%) 🔻1383 🔴 (+48%) 🔻1394 🔴 (+47%) 🔻1425 🔴 (+47%) 🔻30
TTFShook + stream486 (+2.7%)1626 🔴 (+25%) 🔻1665 🔴 (+24%) 🔻1775 🔴 (+29%) 🔻30
STSO1020 steps (inline)160 (+24%) 🔻452 (±0%)508 (±0%)695 (-3.1%)1016
STSO1020 steps (queue-hop)2396 (+61%) 🔻3482 (+10%)3482 (+10%)3482 (+10%)3
WO1020 steps389282 (±0%)389282 (±0%)389282 (±0%)389282 (±0%)1
SLstream latency90 (+20%) 🔻135 🔴 (+17%) 🔻137 🔴 (+7.9%)2905 🔴 (+754%) 🔻30

75e3abe

Fri, 31 Jul 2026 18:10:29 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep268 (+1.9%)1435 🔴 (±0%)1520 🔴 (-5.5%)1887 🔴 (-6.0%)30
TTFSstream260 (+5.3%)1466 🔴 (+4.6%)1512 🔴 (-3.9%)1812 🔴 (-11%)30
TTFShook + stream346 (-14%)1596 🔴 (+23%) 🔻1656 🔴 (+22%) 🔻1845 🔴 (+26%) 🔻30
STSO1020 steps (inline)179 (+1.1%)502 (-2.3%)571 (-1.2%)782 (±0%)1016
STSO1020 steps (queue-hop)2133 (+44%) 🔻2807 (+0.7%)2807 (+0.7%)2807 (+0.7%)3
WO1020 steps438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)438384 (+1.5%)1
SLstream latency128 (+45%) 🔻177 🔴 (+12%)197 🔴 (+15%)354 🔴 (+80%) 🔻30
SOstream overhead (text)111 (-21%) 💚221 (+3.3%)273 (+21%) 🔻390 (-21%) 💚30
SOstream overhead (structured)119 (-11%)211 (-22%) 💚242 (-31%) 💚364 (-23%) 💚30

c784f6c

Fri, 31 Jul 2026 16:59:19 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep1248 (+139%) 🔻1298 🔴 (+25%) 🔻1316 🔴 (+23%) 🔻1333 🔴 (-18%) 💚30
TTFSstream1250 (+32%) 🔻1303 🔴 (+30%) 🔻1317 🔴 (+28%) 🔻1360 🔴 (+5.5%)30
TTFShook + stream445 (+5.7%)1576 🔴 (+19%) 🔻1584 🔴 (+17%) 🔻1629 🔴 (-8.0%)30
STSO1020 steps (inline)172 (+5.5%)465 (-1.9%)522 (-1.7%)676 (+0.7%)1016
STSO1020 steps (queue-hop)2432 (+17%) 🔻3265 (+2.1%)3265 (+2.1%)3265 (+2.1%)3
WO1020 steps399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)399054 (-1.4%)1
SLstream latency90 (+9.8%)139 🔴 (+5.3%)175 🔴 (+19%) 🔻196 🔴 (-8.8%)30
SOstream overhead (text)102 (+1.0%)149 (-13%)196 (-8.4%)344 (+38%) 🔻30
SOstream overhead (structured)101 (-8.2%)179 (+5.9%)215 (+13%)322 (+41%) 🔻30

fedfc7a

Fri, 31 Jul 2026 01:20:43 GMT · run logs

vercel / nextjs-turbopack

MetricScenarioBest (ms)P75 (ms)P90 (ms)P99 (ms)Samples
TTFSstep911 (-9.4%)1286 🔴 (+18%) 🔻1321 🔴 (+20%) 🔻1405 🔴 (-12%)30
TTFSstream1239 (+236%) 🔻1279 🔴 (+21%) 🔻1298 🔴 (+20%) 🔻1366 🔴 (+23%) 🔻30
TTFShook + stream1387 (+177%) 🔻1526 🔴 (+9.0%)1586 🔴 (+8.0%)1628 🔴 (+8.8%)30
STSO1020 steps (inline)168 (-8.7%)466 (-5.1%)525 (-5.9%)780 (+3.2%)1016
STSO1020 steps (queue-hop)2241 (+9.0%)3376 (+21%) 🔻3376 (+21%) 🔻3376 (+21%) 🔻3
WO1020 steps399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)399756 (-7.5%)1
SLstream latency79 (-8.1%)144 🔴 (-1.4%)155 🔴 (-16%) 💚411 🔴 (+61%) 🔻30
SOstream overhead (text)105 (-23%) 💚146 (-71%) 💚178 (-81%) 💚205 (-83%) 💚30
SOstream overhead (structured)96 (-14%)162 (-21%) 💚223 (-15%)1745 🔴 (+294%) 🔻30
ℹ️ Metric definitions & methodology

The collapsed STSO distribution section above buckets every step gap of the sequential-steps run (not a sampled window), split by whether the step ending the gap ran inline — in the same warm process as the step before it, so the gap is pure framework overhead — or after a queue-hop — the first step of a fresh process, which pays queue dispatch, client reinit and event-log replay. Bars overlay the two runs: is main, marks where this run lands, bridges the gap when this run has more samples in a bucket.

Best/P75/P90/P99 deltas compare against the most recent benchmark run on main at the time of this run. 🔻 flags a delta worse than +15%, 💚 one better than −15%.

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) · SO: stream overhead (end-to-end write+consume time beyond the modelled generation window)

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) · stream overhead (text): writer streams 300 variable-length text token deltas paced at 100/s for 3s (a haiku-size LLM's token throughput) while a parallel reader drains the whole stream; SO is the end-to-end write+consume time beyond the 3s generation window (overhead/backpressure) · stream overhead (structured): same workload as stream overhead (text), but each delta is an AI-SDK-style structured object ({ type: 'text-delta', id, text }) instead of a raw string, so the SO gap vs the text scenario is the added serialization cost

🔴 marks a percentile over its target (within target is left unmarked). Targets (p75/p90/p99, ms) — TTFS 200/300/600 · SL 50/60/125 · SO 250/500/1000

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 Best column shows the fastest (warm-start) sample for comparison.

@shalabhc
shalabhc marked this pull request as ready for review July 31, 2026 01:34
@shalabhc
shalabhc requested review from a team and ijjk as code ownersJuly 31, 2026 01:34

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed the capture, the threading, and the renderer. Approving — this closes a real gap in the histogram section (I've done the deployment-id + time-window hunt this replaces), and the degradation matrix is genuinely complete: every optional field falls back the way the table claims, and the three new renderer tests cover the full/degraded/omitted cases (40/40 pass locally).

One correction worth making before merge — the route comment overclaims what the trace contains.route.ts says the trigger request's span "propagates into the workflow's own spans". That's only true under WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or benchmarks.yml sets the mode, so the benchmark deployment runs the default linked mode — where each workflow/step invocation is its own trace root, and the trigger request's trace contains only the workflow.start span plus span links to the execution roots. The link is still useful (one hop via span links, and Datadog renders those), but:

  • the comment should say what's actually there, so nobody debugging a weird histogram concludes the run "has no spans" when they open a trigger-only trace; and
  • consider making the link a Datadog APM search on workflow.run.id:<runId> instead of (or alongside) the trace permalink — under linked mode that lands on the run's actual execution spans directly, which is where an STSO investigation actually goes. The run id is already in hand.

Fine to take the comment fix now and the search-link idea as a follow-up — the current link is strictly better than nothing either way.

Verified:

  • The capture is correctly optional end-to-end: optional-chained getActiveSpan() (undefined without OTEL → bare run id), type-guarded on the runner side (typeof data.traceId === 'string'), and sequentialRuns omitted rather than [] when the scenario didn't run — so old artifacts, old deployments, and history re-renders all degrade exactly per the table.
  • Recording all iterations' run refs matches the data: the histograms aggregate samples across iterations, so with BENCH_SEQUENTIAL_ITERATIONS > 1 the comma-joined list is the honest answer. The doc phrase "both histograms come out of the same iteration" is only true for the default single-iteration config — worth a "(iterations, when configured)" tweak since the code already handles N correctly.
  • Baseline matching reuses the same methodology/backend/app key the percentile baselines use (resultKeyFor = the metric key minus the row), so the two sides of the diff can't come from mismatched configurations.
  • The empty changeset is correct — e2e runner, CI scripts, and a workbench route; nothing published changes.

One robustness nit, non-blocking:runId/traceId are spliced into the markdown link unvalidated. The artifact-trust model is consistent with the rest of this renderer, so it's not a security gap — but a cheap shape check (/^[a-z0-9_]+$/i) before rendering the link would keep a malformed value from a buggy deployment from breaking the whole comment's markdown, which is this section's own failure story.

CI: the 2 failures are the nextjs-webpack canary HMR rebuild-count test and its required-check aggregator — the long-running baseline flake, unreachable from a benchmark-comment change.

shalabhc added a commit that referenced this pull request Jul 31, 2026
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
@vercel
vercelBottemporarily deployed to Preview – workflow-docs July 31, 2026 22:49 Inactive
vercelBotand others added 4 commits August 1, 2026 16:00
The STSO distribution section added in #3213 shows the shape of the
sequential-steps run but not which run produced it, so investigating an
odd-looking bucket meant hunting for the run by deployment id and time
window.
Capture the identity alongside the samples (the mechanism prototyped on
the WIP variance branch, #3107): `/api/bench` returns the trace id of the
span @vercel/otel opened for the trigger request, the runner threads it
through the sequential iteration and records `sequentialRuns` in the
result file, and the renderer prints one line under the histograms with
the run id + Datadog trace link for this run and for the `main` run it is
diffed against.
Every part is optional — a deployment predating the route change yields a
bare run id, and a `main` baseline predating this yields only this run's
side — so the section degrades instead of breaking on mixed-vintage
artifacts.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The run id and Datadog trace are debugging aids, not part of the
benchmark's reported result, so they belong in the job's own output
rather than in the PR comment body.
Logging them where the runs are produced also makes them available in
two cases the comment could never cover: a local `pnpm bench`, and a
job that fails before the comment step runs.
This drops the comment-rendering side entirely -- `renderSequentialRunLinks`,
the `baselineSequentialRuns` baseline plumbing in `annotateWithBaseline`,
and the `sequentialRuns` field on the result artifact, which existed only
to carry the data to the renderer.
The `main`-baseline side of the link goes away with it: which run produced
the baseline histogram is only knowable at comment-render time, where the
two artifacts are matched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
The route comment claimed the trigger request's span "propagates into
the workflow's own spans". That only holds under
WORKFLOW_TRACE_MODE=continuous. Nothing in the workbench or
benchmarks.yml sets the mode, so the benchmark deployment runs the
default `linked` (packages/core/src/telemetry.ts), where each
workflow/step invocation is its own trace root and the trigger's trace
carries `workflow.start` plus span links out to those roots.
The logged link is still the right entry point -- one hop through the
links, which Datadog renders -- but the comment should describe that,
so nobody opening a trigger-only trace while debugging a histogram
concludes the run produced no spans.
Raised by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
Under the default linked trace mode the trigger's trace holds only
`workflow.start` plus span links, so opening it lands one hop away from
the spans an STSO investigation needs. Log an APM search on
`@workflow.run.id:<runId>` next to it, which goes straight to the run's
execution spans.
Both links are logged rather than one replacing the other: the search
depends on `workflow.run.id` being an indexed span tag in the org, and
the permalink works regardless.
Suggested by @TooTallNate in review of #3248.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Shalabh Chaturvedi <7066873+shalabhc@users.noreply.github.com>
Co-Authored-By: shalabhc <shalabh.chaturvedi@vercel.com>
@github-actions

Copy link
Copy Markdown
Contributor

No backport to stable for ba2cddc (AI decision).

This commit adds a new debugging affordance to the benchmark harness — the /api/bench route now returns the OTel trace id and benchmark.test.ts logs a run id plus two Datadog APM links per sequential-steps iteration. Nothing here fixes a defect, a flaky test, or a build/release problem on stable; it is additive tooling (the changeset is deliberately empty, i.e. no package behavior change). Feature/enhancement work stays on main.

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

ba2cddc861380560cc2ae980c9cb4e52a1530cc8

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

@shalabhc@TooTallNate