mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
af91cc2582
## Summary & Motivation - **CRTT (chunk round-trip time)** — per-chunk write→read latency for a paced stream, aggregated inside the reader step on the deployment (one clock domain) into a fixed log-bin histogram plus index buckets and mean-RTT profiles over stream progress and chunk size. Fills the gap between SL (first chunk only) and SO (whole-stream throughput), where a mid-stream delivery regression was invisible. It is deliberately a *round*-trip name: the future production one-way write→read metric is CTT, with its own skew caveats. - **CDV (chunk delay variation)** — inter-arrival gap minus inter-write gap per seq-adjacent pair, so each gap subtracts same-clock stamps and the stat stays skew-free and measurable in production later. Reported as each run's max positive value, since a 1-in-300 delivery stall dilutes out of pooled percentiles. - **Replay scenarios** — two real captured cadences (eve envelope protocol via gpt-5.6-sol; raw gateway SSE via gpt-5.4-nano) replayed through the same rig on an absolute open-loop schedule, so the workload is measured rather than invented; the 2x speed multiplier is the only chosen number, and matches how real fast-tier models behave (same chunk sizes, compressed time). Each capture carries a semantic sha256 over canonical `(offsetMs, bytes)` tuples so durabench's independent copy can be checked for drift. - **Streams table** — stream scenarios render in their own table with writer/reader sustained rates, CRTT percentiles, and median worst stall. No pass/fail targets yet: numbers and vs-main deltas only. - **SL/SO report rows retired** — CRTT's seq-0 slice reproduces SL and its aggregate reproduces SO's signal at ~100x the samples; write slip stays as artifact-only data, the only guard for producer stalls that neither CRTT nor CDV can see. ## Test Plan - [x] Unit tests for the bucketing/merge/CDV helpers and the renderer; the full benchmarks job ran green against real preview deployments, and the first Streams numbers separated workload strain (eve 2x: read 173 < write 181 c/s, CRTT p75 1278ms) from the transport floor (the paced control and the 1x reality row both clean). --------- Signed-off-by: Alex Langenfeld <alex.langenfeld@vercel.com>
284 lines
12 KiB
YAML
284 lines
12 KiB
YAML
name: Performance Benchmarks
|
|
|
|
# Measures the workflow runtime's core latency metrics (TTFS, STSO, WO, CRTT,
|
|
# CDV — see packages/core/e2e/benchmark.test.ts for definitions) against a
|
|
# deployed workbench app and posts the results as a sticky PR comment. Re-runs update
|
|
# the same comment; previous results stay available in a collapsed history
|
|
# section (state is embedded in the comment body itself).
|
|
#
|
|
# Runs on pushes to main (against the production deployment) purely to produce
|
|
# baseline artifacts; PR runs download the most recent main baseline and show
|
|
# avg-latency deltas against it in the comment.
|
|
|
|
on:
|
|
pull_request:
|
|
branches: [main]
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch: # Allow manual triggers
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
|
|
|
jobs:
|
|
ci-scope:
|
|
name: Detect CI Scope
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
fast-path: ${{ steps.scope.outputs.runtime-fast-path }}
|
|
steps:
|
|
- name: Checkout Repo
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Classify changed files
|
|
id: scope
|
|
uses: ./.github/actions/detect-ci-scope
|
|
with:
|
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# Phase 0: Update the PR comment to show benchmarks are running. Previous
|
|
# results (and the collapsed history) are preserved from the existing
|
|
# comment body.
|
|
pr-comment-start:
|
|
name: Create PR Comment
|
|
runs-on: ubuntu-latest
|
|
needs: ci-scope
|
|
if: >-
|
|
github.event_name == 'pull_request' &&
|
|
!startsWith(github.head_ref, 'changeset-release/') &&
|
|
!contains(github.event.pull_request.labels.*.name, 'workflow-server-test') &&
|
|
needs.ci-scope.outputs.fast-path != 'true'
|
|
timeout-minutes: 5
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Find existing benchmark comment
|
|
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
|
id: find-comment
|
|
with:
|
|
issue-number: ${{ github.event.pull_request.number }}
|
|
comment-author: 'github-actions[bot]'
|
|
body-includes: '<!-- benchmark-results -->'
|
|
|
|
- name: Save existing comment body
|
|
if: steps.find-comment.outputs.comment-id != ''
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
const comment = await github.rest.issues.getComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: ${{ steps.find-comment.outputs.comment-id }}
|
|
});
|
|
fs.writeFileSync(`${process.env.RUNNER_TEMP}/previous-comment.md`, comment.data.body ?? '');
|
|
|
|
- name: Render running comment
|
|
env:
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
node .github/scripts/render-benchmark-comment.mjs \
|
|
--status running \
|
|
--previous-body "$RUNNER_TEMP/previous-comment.md" \
|
|
--commit "$HEAD_SHA" \
|
|
--run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
--output "$RUNNER_TEMP/comment.md"
|
|
|
|
- name: Update PR comment
|
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
|
with:
|
|
header: benchmark-results
|
|
path: ${{ runner.temp }}/comment.md
|
|
|
|
# Phase 1: Run the benchmarks. Currently Vercel-only; to add another
|
|
# backend (e.g. postgres or local), add a matrix entry with
|
|
# `world: postgres` / `world: local` and gate the Vercel-specific steps —
|
|
# the runner (packages/core/e2e/benchmark.test.ts) already selects its
|
|
# backend from the same env vars as the e2e tests. CRTT is measured inside
|
|
# the workflow (benchCrttWorkflow's parallel reader/writer steps read/write
|
|
# on the deployment), so it does not need `run.getReadable()` to work from
|
|
# the test process; the runner only polls returnValue for the aggregates.
|
|
benchmark:
|
|
name: Benchmark (${{ matrix.target.world }}, ${{ matrix.target.app }})
|
|
runs-on: ubuntu-latest
|
|
needs: ci-scope
|
|
# `changeset-release/*` PRs are not deployed (every project's vercel.json
|
|
# sets `git.deploymentEnabled` false for that branch, because the branch is
|
|
# force-pushed to main's HEAD SHA and a same-SHA preview deployment
|
|
# clobbers the per-SHA Vercel commit status), so there is nothing to wait
|
|
# for below. Benchmarking them would also be meaningless: the code is
|
|
# main's, so it would only compare main's baseline against itself.
|
|
if: >-
|
|
!startsWith(github.head_ref, 'changeset-release/') &&
|
|
!contains(github.event.pull_request.labels.*.name, 'workflow-server-test') &&
|
|
(needs.ci-scope.outputs.fast-path != 'true' ||
|
|
github.event_name == 'workflow_dispatch')
|
|
timeout-minutes: 60
|
|
permissions:
|
|
id-token: write
|
|
contents: read
|
|
deployments: read
|
|
statuses: read
|
|
env:
|
|
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
|
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
target:
|
|
- world: vercel
|
|
app: nextjs-turbopack
|
|
project-id: "prj_yjkM7UdHliv8bfxZ1sMJQf1pMpdi"
|
|
project-slug: "example-nextjs-workflow-turbopack"
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup environment
|
|
uses: ./.github/actions/setup-workflow-dev
|
|
with:
|
|
build-packages: 'false'
|
|
|
|
# Builds the packages the benchmark runner imports (same pattern as the
|
|
# e2e-vercel jobs in tests.yml); hits the turbo remote cache in CI.
|
|
- name: Build packages
|
|
run: pnpm turbo run build --filter='@workflow/cli'
|
|
|
|
- name: Wait for Vercel deployment
|
|
id: waitForDeployment
|
|
if: matrix.target.world == 'vercel'
|
|
uses: vercel/wait-for-deployment-action@0e2b0c5c5cce31f1648108aeec56467187aca037
|
|
with:
|
|
project-slug: ${{ matrix.target.project-slug }}
|
|
timeout: 1000
|
|
check-interval: 15
|
|
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
|
|
|
|
- name: Run benchmarks
|
|
env:
|
|
DEPLOYMENT_URL: ${{ steps.waitForDeployment.outputs.deployment-url }}
|
|
APP_NAME: ${{ matrix.target.app }}
|
|
VERCEL_DEPLOYMENT_ID: ${{ steps.waitForDeployment.outputs.deployment-id }}
|
|
WORKFLOW_VERCEL_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'preview' }}
|
|
VERCEL_WORKFLOW_SERVER_URL: ${{ github.ref != 'refs/heads/main' && secrets.VERCEL_WORKFLOW_SERVER_URL || '' }}
|
|
WORKFLOW_VERCEL_AUTH_TOKEN: ${{ secrets.VERCEL_LABS_TOKEN }}
|
|
WORKFLOW_VERCEL_TEAM: "team_nO2mCG4W8IxPIeKoSsqwAxxB"
|
|
WORKFLOW_VERCEL_PROJECT: ${{ matrix.target.project-id }}
|
|
WORKFLOW_VERCEL_PROJECT_SLUG: ${{ matrix.target.project-slug }}
|
|
# Report the PR head SHA (not the synthetic merge commit) in results
|
|
GITHUB_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
# Trusted-sources OIDC token is minted on demand by
|
|
# scripts/trusted-sources-headers.mjs via the runner's
|
|
# ACTIONS_ID_TOKEN_REQUEST_URL/_TOKEN env vars (auto-set when
|
|
# `permissions: id-token: write` is on the job).
|
|
# See: https://vercel.com/docs/deployment-protection/methods-to-bypass-deployment-protection/trusted-sources
|
|
run: |
|
|
pnpm vitest run packages/core/e2e/benchmark.test.ts
|
|
|
|
- name: Upload benchmark results
|
|
# Partial results are still useful when a scenario failed
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: bench-results-${{ matrix.target.app }}-${{ matrix.target.world }}
|
|
path: bench-results-*.json
|
|
if-no-files-found: ignore
|
|
|
|
# Phase 3: Update the PR comment with the results (collapsing history)
|
|
comment:
|
|
name: Benchmark Comment
|
|
runs-on: ubuntu-latest
|
|
needs: [ci-scope, benchmark]
|
|
if: >-
|
|
always() && !cancelled() &&
|
|
github.event_name == 'pull_request' &&
|
|
!startsWith(github.head_ref, 'changeset-release/') &&
|
|
!contains(github.event.pull_request.labels.*.name, 'workflow-server-test') &&
|
|
needs.ci-scope.outputs.fast-path != 'true'
|
|
timeout-minutes: 10
|
|
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Download benchmark results
|
|
continue-on-error: true
|
|
uses: actions/download-artifact@v4
|
|
with:
|
|
pattern: bench-results-*
|
|
path: benchmark-results
|
|
merge-multiple: true
|
|
|
|
# Baseline for the avg-delta column: results from the most recent
|
|
# successful run of this workflow on main. Uses gh (first-party) rather
|
|
# than a third-party cross-run artifact action. Filtering to push /
|
|
# workflow_dispatch events matters for more than tidiness: it excludes
|
|
# artifacts uploaded by fork PR runs, whose head branch can also be
|
|
# named "main" (artifact poisoning).
|
|
- name: Download baseline results from main
|
|
continue-on-error: true
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
run: |
|
|
run_id=$(gh run list \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--workflow benchmarks.yml \
|
|
--branch main \
|
|
--status success \
|
|
--limit 20 \
|
|
--json databaseId,event \
|
|
--jq '[.[] | select(.event == "push" or .event == "workflow_dispatch")][0].databaseId // empty')
|
|
if [ -z "$run_id" ]; then
|
|
echo "No successful benchmark run found on main; skipping baseline"
|
|
exit 0
|
|
fi
|
|
echo "Using baseline artifacts from run $run_id"
|
|
gh run download "$run_id" \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--pattern 'bench-results-*' \
|
|
--dir baseline-results \
|
|
|| echo "Run $run_id has no benchmark artifacts; skipping baseline"
|
|
|
|
- name: Find existing benchmark comment
|
|
uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e # v3.1.0
|
|
id: find-comment
|
|
with:
|
|
issue-number: ${{ github.event.pull_request.number }}
|
|
comment-author: 'github-actions[bot]'
|
|
body-includes: '<!-- benchmark-results -->'
|
|
|
|
- name: Save existing comment body
|
|
if: steps.find-comment.outputs.comment-id != ''
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
const comment = await github.rest.issues.getComment({
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
comment_id: ${{ steps.find-comment.outputs.comment-id }}
|
|
});
|
|
fs.writeFileSync(`${process.env.RUNNER_TEMP}/previous-comment.md`, comment.data.body ?? '');
|
|
|
|
- name: Render results comment
|
|
env:
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
BENCH_STATUS: ${{ needs.benchmark.result == 'success' && 'completed' || 'failed' }}
|
|
run: |
|
|
node .github/scripts/render-benchmark-comment.mjs \
|
|
--status "$BENCH_STATUS" \
|
|
--results-dir benchmark-results \
|
|
--baseline-dir baseline-results \
|
|
--previous-body "$RUNNER_TEMP/previous-comment.md" \
|
|
--commit "$HEAD_SHA" \
|
|
--run-url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
|
|
--output "$RUNNER_TEMP/comment.md"
|
|
cat "$RUNNER_TEMP/comment.md" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
- name: Update PR comment
|
|
uses: marocchino/sticky-pull-request-comment@773744901bac0e8cbb5a0dc842800d45e9b2b405 # v2.9.4
|
|
with:
|
|
header: benchmark-results
|
|
path: ${{ runner.temp }}/comment.md
|