Skip to content

Add load tests, a browser swarm and a public benchmark site - #4055

Draft
T4rk1n wants to merge 9 commits into
devfrom
feat/benchmarks
Draft

T4rk1n wants to merge 9 commits into
devfrom
feat/benchmarks

Conversation

@T4rk1n

@T4rk1n T4rk1n commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Load tests for streaming and callbacks, a headless-browser swarm for large runs, and CI that commits the numbers to the repo, with a short table in the README.

Changes

  • benchmarks/streaming/: how many browsers each server setup can stream to (Flask/gunicorn, FastAPI and Quart on uvicorn, 1 or 4 workers). Simulated browsers follow the renderer's StreamClient and the count climbs until p95 frame latency or errors give out.
  • benchmarks/callbacks/: the same sweep for plain callbacks, HTTP vs websocket, by backend (Flask HTTP only; FastAPI and Quart both). --kind async and --work-ms add per-call work.
  • benchmarks/loadkit.py: the plumbing both share. It runs the server and the clients on separate CPUs, flags points where the clients ran out of CPU, and makes the server import the checkout under test, not the installed dash.
  • benchmarks/swarm/: real headless Chromium users (Playwright), many per machine and many machines, against a deployed app. Users click an HTTP callback, a websocket callback or a stream, timed in the page from click to DOM update, so machines only share a start time, not clocks.
    • Agents run locally or over SSH in a Docker image. They report mergeable histograms.
    • Steps stop when the server saturates or when the swarm stops delivering its load: failed agents or users, agents over 85% CPU, late starts.
    • Meant for a one-off cloud run; see benchmarks/README.md.
  • benchmarks/publish.py + .github/workflows/benchmarks-publish.yml: one place for the numbers, in this repo.
    • Weekly and on dispatch only, CI measures on dev. It opens a pull request that updates benchmarks/results/ and the README table only when the numbers moved significantly: a load test's capacity changes step, or a renderer median moves by over 25% and 5 ms. The pull request lists what moved.
    • The per-PR renderer regression comment (benchmarks.yml) is unchanged.
    • benchmarks/results/report.json has every chart as a Plotly figure plus the tables, for the docs to render with dcc.Graph. The raw numbers and their history sit next to it.
    • The repo needs "Allow GitHub Actions to create and approve pull requests" turned on. A pull request opened with GITHUB_TOKEN starts no checks, so close and reopen it before merging.
    • The README table says "No published run yet" until the first run.

Numbers

Callbacks, sync, 1 worker, 8-core laptop:

p95 at 1000 browsers p95 at 4000
Flask HTTP 3.7 ms 4.6 s (saturated from 2000)
FastAPI HTTP / websocket 1.2 / 0.9 ms 165 / 27 ms
Quart HTTP / websocket 1.9 / 1.0 ms 511 / 469 ms

With 4 workers no setup saturated at 4000 browsers; the swarm is how to find those limits. The multi-worker Redis streaming numbers need #4054.

Tests

  • Unit tests for the publisher (results, history, figures, README table) and for histogram merging.
  • A report from real runs rendered in a Dash app, headless: all 9 figures draw, no console errors, and the trend dropdown switches scenario.
  • Every load test and every setup ran locally.
  • The swarm ran locally and through a fake ssh/docker fan-out. One agent with 50 users measured about 0.05 core and up to 170 MB per user.
  • The swarm's Docker image builds, and one agent in it ran 10 real browsers against a local app with no errors.

T4rk1n added 4 commits October 7, 2026 12:10
benchmarks/streaming/ measures how many browsers each server setup can
stream to (Flask/gunicorn, FastAPI and Quart on uvicorn, 1 or 4 workers),
with simulated browsers that follow the renderer's StreamClient.
benchmarks/publish.py builds a static site from the renderer timings and
the streaming sweep (embeddable chart pages, raw data with history,
shields badges), and benchmarks-publish.yml pushes it to gh-pages from dev.
benchmarks/callbacks/ sweeps browser counts for plain callbacks over HTTP
and over the websocket on Flask (HTTP only), FastAPI and Quart, 1 and 4
workers, recording round trip, throughput, errors and CPU. The server and
client plumbing it shares with the streaming test moves to
benchmarks/loadkit.py.
benchmarks/swarm/ drives real Chromium users (Playwright) against a
deployed app: plain HTTP callbacks, websocket callbacks and streaming,
timed in the page from click to DOM update. Agents run locally or on remote
hosts over SSH in a Docker image, start together at a shared wall-clock
time, and report mergeable histograms; steps stop when the server
saturates or when the swarm stops delivering its load (failed agents or
users, agents over 85% CPU, late starts).
The benchmark site gains a callbacks section: browsers served per setup
coloured by transport, p95 round trip for 1 and 4 workers (websocket
dashed), server CPU per 1,000 calls a second, the full table, badges and
data with history. The publish workflow runs the callback sweep weekly and
on demand, like the streaming one.
@github-actions

github-actions Bot commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Dash performance benchmarks

⚠️ regressions to review

scenario metric p90 (ms) median growth baseline p90 note
⚠️ initial_render_small render_ms 87.6 53.5 1.34x 104.0 1.5x baseline (norm)
✅ callback_chain chain_ms 258.5 246.9 0.95x 499.1
✅ callback_chain graph_ms 1.2 1.2 1.0x 2.4
✅ callback_fanout fanout_ms 48.0 43.9 0.88x 92.5
✅ deep_nesting render_ms 35.5 34.0 1.03x 56.8
✅ full_children_replace replace_ms 2891.8 1069.9 18.48x 4697.1
✅ initial_render_large render_ms 371.6 337.9 0.99x 694.4
✅ patch_append_nested append_ms 88.9 59.6 2.64x 192.3
✅ patch_append_toplevel append_ms 77.2 55.6 2.46x 140.2
✅ patch_scalar_update_large update_ms 92.0 86.4 0.96x 202.9
✅ wildcard_all_resolve wildcard_ms 165.1 145.0 0.91x 313.6
✅ wildcard_all_resolve graph_ms 0.6 0.6 1.0x 1.3

growth = late-third / early-third per-op time; ~1 is flat, a large value means the per-op cost scales with accumulated state.

machine scale vs baseline: 0.56x - divided out of the baseline ratios so they compare like for like (the absolute warn/fail ceilings are left un-scaled); calibrated on initial_render_small.

T4rk1n added 2 commits October 7, 2026 13:51
Share the sweep loop between the streaming and callback runners
(loadkit.sweep), split the callback client's and swarm agent's long loops
into small steps, write the swarm report outside the event loop, dedupe
the site builder's sections and literals, run the agent image as its
unprivileged user, and pin the third-party setup-chrome action to a
commit. The site output is byte-identical for the same inputs.
@camdecoster
camdecoster marked this pull request as draft October 8, 2026 19:32
The playwright/python image ships the browsers but not the Python
package, so the agent failed to import it.
The numbers live in benchmarks/results/ for the docs to render
(report.json holds every chart as a Plotly figure), and a short table in
README.md shows how many browsers each server setup keeps interactive.
The workflow now runs weekly and opens a pull request with the update.
The weekly run commits a load test only when a setup's capacity changes
step, and the renderer only when a median moves by over 25% and 5 ms.
The pull request lists what moved.
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to 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.

1 participant