Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
20171d6
chore(perf): add a perf host app for web performance testing
NikolaGoranNaumovski Oct 20, 2026
c16125c
feat: add a web target to token --print-config
NikolaGoranNaumovski Oct 21, 2026
d6d8ee4
chore(perf): add live-run scenarios, the settle detector and the perf…
NikolaGoranNaumovski Oct 22, 2026
2342e8d
chore(perf): extend the perf host for threads, jumps, switching and l…
NikolaGoranNaumovski Oct 22, 2026
0d7a569
chore(perf): add the remaining measured scenarios to the runner
NikolaGoranNaumovski Oct 22, 2026
5f53352
chore(perf): replay render fixtures offline and gate PRs on them
NikolaGoranNaumovski Oct 22, 2026
4451c1d
chore(perf): add scenario sets, the registry contract, compare and re…
NikolaGoranNaumovski Oct 22, 2026
2163120
ci(perf): add the nightly regression run for the dedicated perf machine
NikolaGoranNaumovski Oct 22, 2026
ad5b2c1
docs(perf): document the runner's commands, live batches and validati…
NikolaGoranNaumovski Oct 22, 2026
f05c3ff
fix(perf): match the offline WebSocket route by hostname
NikolaGoranNaumovski Oct 26, 2026
8f70455
chore(perf): record the render baselines from channels opened by navi…
NikolaGoranNaumovski Oct 26, 2026
aa8c2d8
fix(perf): keep validation runs out of the report and the comparison
NikolaGoranNaumovski Oct 26, 2026
8f2c294
fix(perf): keep WB-01's Chrome window rendering when another app cove…
NikolaGoranNaumovski Oct 26, 2026
808f27e
chore(perf): record the evidence behind the ML-13, TH-03 and CL-02 nu…
NikolaGoranNaumovski Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/perf-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: Perf nightly

# The nightly regression run (WEB-SCENARIOS.md §7): the nightly set against the live test app, on
# the dedicated perf machine only. That machine holds chatperf (CHATPERF_DIR), Ruby, and the test
# app's credentials in its own environment (~/.config/chatperf/env); none of them are GitHub
# secrets. Results accumulate in PERF_RESULTS_DIR outside the checkout, and `perf compare` fails
# the job when a row is more than 15 % worse than its 7-day median.

on:
schedule:
- cron: '0 2 * * *'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: perf-nightly
cancel-in-progress: false

jobs:
nightly:
runs-on: [self-hosted, perf-nightly]
timeout-minutes: 240
env:
PERF_RESULTS_DIR: ${{ vars.PERF_RESULTS_DIR }}
CHATPERF_DIR: ${{ vars.CHATPERF_DIR }}
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/setup-node

- run: yarn build

- name: Nightly set
shell: bash
run: |
set -a; source ~/.config/chatperf/env; set +a
yarn workspace @stream-io/react-perf-runner perf run --set nightly -n 5

- name: Compare with the last 7 days
run: yarn workspace @stream-io/react-perf-runner perf compare
55 changes: 55 additions & 0 deletions .github/workflows/perf.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Perf

# The web performance harness's PR checks (perf/README.md). Neither job talks to a Stream app or
# needs a secret, so both also run for pull requests from forks:
# - contract: the runner's scenario files against the pinned chatperf registry
# - renders: M-RENDERS replayed from recorded fixtures against their recorded backend; fails if
# untargeted rows per commit rise above the baseline committed from master

on:
pull_request:
types:
- opened
- reopened
- synchronize
paths:
- 'src/**'
- 'perf/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/perf.yml'

permissions:
contents: read

jobs:
contract:
runs-on: ubuntu-latest
name: Scenario contract
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/setup-node

- run: yarn workspace @stream-io/react-perf-runner test

renders:
runs-on: ubuntu-latest
name: Render gate (ML-09, ML-11, CL-07, ML-08)
steps:
- uses: actions/checkout@v6

- uses: ./.github/actions/setup-node

- name: Build the SDK (the perf host consumes dist/)
run: yarn build

- name: Install Chromium
run: yarn workspace @stream-io/react-perf-runner exec playwright install --with-deps chromium

- name: Replay the fixtures
run: |
for id in ml-09 ml-11 cl-07 ml-08; do
yarn workspace @stream-io/react-perf-runner perf renders "$id" \
--replay --offline --build profiling --baseline check
done
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ examples/*/next-env.d.ts
.agents/
.claude/skills/
skills-lock.json

# perf harness data: recorded event fixtures and run results (generated, not hand-edited)
perf/runner/fixtures/
perf/runner/contract/
perf/runner/results/
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@
},
"packageManager": "yarn@4.15.0",
"workspaces": [
"examples/*"
"examples/*",
"perf/*"
]
}
5 changes: 5 additions & 0 deletions perf/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Build output of the host's profiling mode (dist/ is ignored at the repo root).
dist-profiling/

# Runner output; fixtures/ is kept.
runner/results/
78 changes: 78 additions & 0 deletions perf/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# perf

Web performance harness for `stream-chat-react`. Private workspaces, never published (the SDK
publishes `dist/` only).

The plan and protocol live in `stream-chat-perf-cli` (`WEB-PLAN.md`, `WEB-SCENARIOS.md`). The
backend state comes from that repo's `chatperf` CLI.

| Package | Path | What it is |
| ------------------------------ | --------- | ------------------------------------------------------------------------------- |
| `@stream-io/react-perf-host` | `host/` | A minimal chat page using the SDK under test, configured entirely by URL params |
| `@stream-io/react-perf-runner` | `runner/` | Seeds through `chatperf`, drives the host in Chromium, and writes result rows |

The names deliberately don't match `@stream-io/stream-chat-react-*`, which `yarn examples:build`
(and CI) builds.

## Host

```bash
yarn build # the SDK, once (the host consumes dist/)
yarn workspace @stream-io/react-perf-host dev # dev: http://localhost:5173
yarn workspace @stream-io/react-perf-host build && yarn workspace @stream-io/react-perf-host preview
# prod: http://localhost:4173
yarn workspace @stream-io/react-perf-host build:profiling && yarn workspace @stream-io/react-perf-host preview:profiling
# profiling (render counts only): http://localhost:4174
```

Open it with `?api_key=…&token=…&user_id=qatest1&list=standard|virtualized&type=messaging|livestream`
(`chatperf token qatest1` prints the token). Optional: `cap=<n>|none` (exploration only; the
protocol's cap follows `type`) and `page_size=<n>` (default 30).

## Runner

Needs `chatperf` (its checkout next to this repo, or `CHATPERF_DIR`), with `STREAM_KEY` /
`STREAM_SECRET` in the environment. Node runs the TypeScript directly; there is no build step.

```bash
perf() { yarn workspace @stream-io/react-perf-runner perf "$@"; }
perf run cl-10 [--build dev|prod] [--column chrome-desktop|chrome-4x] [-n 5]
perf run ml-08 --type messaging|livestream [--list standard|virtualized] [--no-seed] [--no-verify]
perf run --set nightly|signoff|channel-list|message-list|leaks|desktop
perf renders ml-09 --record|--replay [--build profiling|dev] [--offline] [--baseline write|check]
perf check ml-08-oldest [--seed]
perf contract [--update] # scenario files vs the pinned chatperf registry
perf compare # last 12 h of rows vs their 7-day median (the nightly check)
perf report # results/report-<date>.md from rows.csv
yarn workspace @stream-io/react-perf-runner test # contract + compare unit tests
```

- `run` seeds the scenario's dataset (unless `--no-seed`), verifies it, starts the host, runs N
iterations (the first is dropped), and appends one row per grid cell to `results/rows.csv`, with
every iteration in `results/run-*.json`. Iterations that fail a gate are discarded with their
reasons; if none is left, the evidence is kept in `results/run-*-discarded.json`. Memory
scenarios also write `results/mem-*-chart.html`.
- Live scenarios (ML-08 … ML-12, CL-06 … CL-09, TH-02, CP-01, SK-01, WB-01) mutate their dataset. A
batch is one seed and one verify: run later scenarios on the same seed with
`--no-seed --no-verify`, and ML-10 last (its deletes leave the newest page as placeholders).
Their `chatperf live` commands are data, in `src/scenarios/live-specs.ts`.
- `renders --offline` replays a fixture against the backend recorded with it (`src/world.ts`): no
Stream app, no credentials, no Ruby. It is what the PR job (`.github/workflows/perf.yml`) runs;
`--baseline check` fails when untargeted rows per commit exceed `fixtures/baseline.json`.
- Runs are headed (Chromium windows open) unless `--headless`; frame numbers depend on the
display's refresh rate. WB-01 launches its own Chrome over raw CDP (Playwright keeps attached
pages visible), so a second window opens for it.
- `PERF_RESULTS_DIR` moves results out of the checkout (the nightly machine keeps its history
there).

Validation switches (deliberate failures, to prove a gate or metric works; every row says so):

| Variable | Effect |
| ------------------------------------------------------ | ------------------------------------------------------------------ |
| `PERF_HOST_PARAMS='leak=messages'` / `'leak=detached'` | a deliberate leak in the host (M-MEM must show it) |
| `PERF_HOST_PARAMS='drop_events=<types>'` | the client never sees those events (e.g. `rows-changed` must fail) |
| `PERF_GATE_CHECK_OFFLINE=1` | ML-08 goes offline mid-window (`live-delivered` must fail) |
| `PERF_GATE_CHECK_ONLINE=1` | OF-02 skips going offline (`went-offline` must fail) |
| `PERF_GATE_CHECK_STALE=1` | no per-iteration re-seed (ML-12's `receipts-changed` must fail) |
| `PERF_GATE_CHECK_KIND=<kind>` | ML-07 looks for that kind with a selector that can't match |
| `PERF_AUTOMATION_CHECK=1` | CL-11 polls the page during the window (M-FRAMES must get worse) |
12 changes: 12 additions & 0 deletions perf/host/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Stream Chat React — perf host</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions perf/host/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"name": "@stream-io/react-perf-host",
"private": true,
"version": "0.0.0",
"type": "module",
"installConfig": {
"hoistingLimits": "workspaces"
},
"scripts": {
"dev": "vite --port 5173 --strictPort",
"build": "tsc -b && vite build",
"build:profiling": "tsc -b && vite build --mode profiling --outDir dist-profiling",
"preview": "vite preview --port 4173 --strictPort",
"preview:profiling": "vite preview --port 4174 --strictPort --outDir dist-profiling"
},
"dependencies": {
"react": "^19.2.6",
"react-dom": "^19.2.6",
"stream-chat": "^10.0.0-rc.22",
"stream-chat-react": "workspace:^"
},
"devDependencies": {
"@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.3",
"typescript": "^6.0.3",
"vite": "^8.1.3"
}
}
Loading
Loading