Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ jobs:
set -euo pipefail
pnpm install --frozen-lockfile
pnpm test
npm --prefix rendering/external/sveltekit ci
node --test rendering/external/sveltekit/stream-validator.test.mjs sveltekit/benchmark.test.mjs
node sveltekit/report.mjs --check
bash -n scripts/refresh-proa.sh rendering/run_all.sh

- name: Check Rust formatting
Expand Down Expand Up @@ -154,6 +157,7 @@ jobs:
npm --prefix rendering/external/marko ci
npm --prefix rendering/external/vue ci
npm --prefix rendering/external/svelte ci
npm --prefix rendering/external/sveltekit ci
pnpm exec playwright install --with-deps chromium

- name: Run Clippy correctness lints
Expand Down Expand Up @@ -191,7 +195,9 @@ jobs:

- name: Validate Suspense streaming fixtures
if: matrix.suite == 'rendering'
run: pnpm run bench:suspense-load:smoke
run: |
pnpm run bench:suspense-load:smoke
node sveltekit/run.mjs --smoke

- name: Validate canonical page servers
if: matrix.suite == 'rendering'
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ __pycache__/
target/
rendering/external/go/build/
rendering/external/marko/build/
rendering/external/sveltekit/build/
rendering/external/sveltekit/.svelte-kit/
rendering/external/svelte/*.compiled.mjs
.DS_Store
*.log
_tmp_*
Expand All @@ -17,6 +20,9 @@ page-load/results/*.partial
cold-start/results/*.json
cold-start/results/*.partial
!cold-start/results/latest.json
sveltekit/results/*.json
sveltekit/results/*.partial
!sveltekit/results/latest.json
travel-agent/dist/
travel-agent/results/*.json
travel-agent/results/*.in-progress
Expand Down
24 changes: 16 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

[Proa](https://github.com/Proa-Labs/proa) renders typed Rust components to HTML on the server, streams them, and hydrates fine-grained browser islands. This page shows what one core [serves over HTTP](#serving-three-pages-on-one-core), how fast [seventeen renderers](#rendering-the-same-pages-in-seventeen-renderers) render those pages, and [how to reproduce them](#reproducing-a-capture).

The [SvelteKit comparison](sveltekit.md) adds production HTTP, cold-start, product-scaling, and browser-complete streaming measurements. Native SvelteKit promise streaming sends data for the browser to render; its response end is a different boundary from Proa’s resolved-HTML stream.

If you see an issue with the benchmarks or would like to add any, please open an issue!

<!-- Keep this HTML table free of blank lines so GitHub renders it as one block. -->
Expand All @@ -24,7 +26,7 @@ If you see an issue with the benchmarks or would like to add any, please open an
<p>Three sample pages, one core each: throughput at saturation, latency at a fixed request rate.</p>
<!-- charts:headline:start -->
<a href="charts/svg/08-canonical-pages-http.svg">
<img width="100%" alt="Sustained responses per second, CPU per response, and peak RSS for Proa, React, Marko, and Go html/template serving three canonical pages over HTTP on one core" src="charts/svg/08-canonical-pages-http.svg">
<img width="100%" alt="Sustained responses per second, CPU per response, and peak RSS for Proa, React, Marko, Go html/template, and SvelteKit serving three canonical pages over HTTP on one core" src="charts/svg/08-canonical-pages-http.svg">
</a>
<p><em>Three parity-gated pages on loopback HTTP, one core per engine. <a href="http-capacity.md#canonical-pages-over-http-one-core-each">Details</a> · <a href="charts/svg/08-canonical-pages-http.svg">Full size</a> · <a href="charts/">how the figures are built</a></em></p>
<!-- charts:headline:end -->
Expand All @@ -46,9 +48,9 @@ If you see an issue with the benchmarks or would like to add any, please open an
<p>Single-core Suspense streaming with eight async boundaries per request.</p>
<!-- charts:suspense:start -->
<a href="charts/svg/07-suspense-load.svg">
<img width="100%" alt="Responses completed per second, latency distribution and peak memory for Proa, Marko and React streaming the product grid through Suspense over HTTP on one core" src="charts/svg/07-suspense-load.svg">
<img width="100%" alt="Overlaid HTTP streaming throughput and memory for Proa, Marko, React and SvelteKit, with four latency panels on a shared scale" src="charts/svg/07-suspense-load.svg">
</a>
<p><em>Throughput sweep with queueing markers (P50/P95/P99 + memory) at each rate. <a href="http-capacity.md#suspense-streaming-over-http-one-core-each">Details</a> · <a href="charts/svg/07-suspense-load.svg">Full size</a> · <a href="charts/">how the figures are built</a></em></p>
<p><em>Four engines on shared throughput, memory, and latency scales. <a href="http-capacity.md#suspense-streaming-over-http-one-core-each">Marko/React source</a> · <a href="sveltekit.md#http-streaming-capacity-and-latency">Proa/SvelteKit source</a>. SvelteKit response end excludes browser rendering; see <a href="sveltekit/browser.svg">complete-DOM timings</a>. <a href="charts/svg/07-suspense-load.svg">Full size</a> · <a href="charts/">how the figures are built</a></em></p>
<!-- charts:suspense:end -->
</td>
<td valign="top" width="50%">
Expand Down Expand Up @@ -78,7 +80,7 @@ If you see an issue with the benchmarks or would like to add any, please open an
<p>Startup latency from process creation to first byte-identical response.</p>
<!-- charts:coldstart:start -->
<a href="charts/svg/09-cold-start.svg">
<img width="100%" alt="Median, P95 and slowest time from process spawn to first complete product-grid response for Proa, React, Marko and Go html/template, with RSS after the first response" src="charts/svg/09-cold-start.svg">
<img width="100%" alt="Median, P95 and slowest time from process spawn to first complete product-grid response for Proa, React, Marko, Go html/template and SvelteKit, with RSS after the first response" src="charts/svg/09-cold-start.svg">
</a>
<p><em>Median, P95, and worst-start across 15 fresh processes. <a href="http-capacity.md#cold-start-to-first-response">Details</a> · <a href="charts/svg/09-cold-start.svg">Full size</a> · <a href="charts/">how the figures are built</a></em></p>
<!-- charts:coldstart:end -->
Expand All @@ -96,24 +98,30 @@ pnpm exec playwright install chromium
npm ci --prefix rendering/external/react
npm ci --prefix rendering/external/marko
npm ci --prefix rendering/external/handlebars
npm ci --prefix rendering/external/svelte
npm ci --prefix rendering/external/sveltekit
```

Then run a suite. Each command builds its servers, gates every page for parity, and rewrites its evidence page. It refuses to publish a phase whose load generator was the bottleneck.
Run timed suites sequentially. The runners build their servers, gate parity before timing, and reject a phase whose load generator was the bottleneck. The report commands regenerate the evidence pages and figures.

```sh filename="Terminal"
./rendering/run-all # scoreboard: build, parity-gate, time, regenerate scoreboard.md
pnpm run bench:page-load # canonical pages over HTTP, one core each
pnpm run bench:cold-start # cold start to first response
pnpm run bench:suspense-load # Suspense streaming over HTTP
node sveltekit/run.mjs # Proa vs SvelteKit HTTP streaming + browser completion
node sveltekit/report.mjs # regenerate SvelteKit comparison
pnpm run refresh # regenerate every evidence page and figure from the captures
```

> **Good to know**: the committed numbers are dated evidence from one machine, not universal claims. Rerun the relevant suite before publishing a current comparison.

## Next steps

- [Proa versus SvelteKit](sveltekit.md)
- Static SSR, scaling, HTTP, cold starts, and async streaming, with native data streaming and buffered HTML reported separately.
- [HTTP capacity, cold start and streaming](http-capacity.md)
- Sustained responses per second, CPU per response, latency at equal load, and cold start for four production servers.
- Sustained responses per second, CPU per response, latency at equal load, and cold start for five production servers.
- [Canonical scoreboard evidence](scoreboard.md)
- The seventeen-renderer table, parity coverage, every retained percentile, and the adapters side by side.
- [Engineering, application and agent captures](engineering.md)
Expand All @@ -122,10 +130,10 @@ pnpm run refresh # regenerate every evidence page and figure from
- How every SVG is drawn from the evidence pages' own tables and checked against them.
- [Canonical benchmark protocol](rendering/protocol.md)
- The fairness rules, workload shapes, and parity gate in full.
- Suite READMEs: [`rendering/`](rendering/), [`page-load/`](page-load/), [`suspense-load/`](suspense-load/), [`load/`](load/), [`showcase/`](showcase/), [`agent-chain/`](agent-chain/), and the separate [`travel-agent/`](travel-agent/) control.
- Suite READMEs: [`rendering/`](rendering/), [`page-load/`](page-load/), [`suspense-load/`](suspense-load/), [`sveltekit/`](sveltekit/), [`load/`](load/), [`showcase/`](showcase/), [`agent-chain/`](agent-chain/), and the separate [`travel-agent/`](travel-agent/) control.

## License and attribution

This repository is Apache-2.0; see [`LICENSE`](LICENSE). Benchmark adapters incorporate upstream work under its own terms, recorded in [`rendering/UPSTREAM_LICENSES.md`](rendering/UPSTREAM_LICENSES.md).

React, Marko, Vue, Svelte, Rails, jte, Rocker, Handlebars, Go, Jinja2, PHP, Askama, Sailfish, Leptos, Dioxus, and Topcoat belong to their respective owners. They appear here as the subjects of a benchmark; their inclusion implies no endorsement.
React, Marko, Vue, Svelte, SvelteKit, Rails, jte, Rocker, Handlebars, Go, Jinja2, PHP, Askama, Sailfish, Leptos, Dioxus, and Topcoat belong to their respective owners. They appear here as the subjects of a benchmark; their inclusion implies no endorsement.
14 changes: 8 additions & 6 deletions charts/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Figures

Nine figures covering the benchmark report: the [front page](../README.md) and its three evidence pages, [`scoreboard.md`](../scoreboard.md), [`http-capacity.md`](../http-capacity.md), and [`engineering.md`](../engineering.md).
Nine figures covering the benchmark report: the [front page](../README.md) and its evidence pages, [`scoreboard.md`](../scoreboard.md), [`http-capacity.md`](../http-capacity.md), [`engineering.md`](../engineering.md), and [`sveltekit.md`](../sveltekit.md).

Each one is **generated from the evidence pages' own tables** — no measurement is
retyped here. Re-run the capture, let it regenerate its page, then run the build and
Expand Down Expand Up @@ -43,13 +43,13 @@ node charts/verify.mjs # check them against the source, and against each othe
| | Figure | Source |
| ---: | --- | --- |
| 06 | [The linked showcase corpus](svg/06-showcase-corpus.svg) | [Linked proa_showcase corpus](../engineering.md#linked-proa_showcase-corpus) |
| 07 | [Suspense-heavy HTTP capacity, latency, CPU, and RSS](svg/07-suspense-load.svg) | [Suspense streaming over HTTP](../http-capacity.md#suspense-streaming-over-http-one-core-each) |
| 07 | [Four-engine Suspense HTTP throughput, latency, RSS, and failures](svg/07-suspense-load.svg) | [Retained Marko/React capture](../http-capacity.md#suspense-streaming-over-http-one-core-each) overlaid with [fresh Proa/SvelteKit measurements](../sveltekit.md#http-streaming-capacity-and-latency) |
| 08 | [Canonical pages over real HTTP on one core](svg/08-canonical-pages-http.svg) | [Canonical pages over HTTP](../http-capacity.md#canonical-pages-over-http-one-core-each) |
| 09 | [Cold start to first response](svg/09-cold-start.svg) | [Cold start to first response](../http-capacity.md#cold-start-to-first-response) |

## How they are built

`build.mjs` reads the front page and the three evidence pages, parses every Markdown
`build.mjs` reads the front page and the evidence pages, parses every Markdown
table in them, and hands the
parsed rows to one function per figure. A figure asks for its table by the exact
signature of its header row and fails the build if that signature matches anything
Expand Down Expand Up @@ -77,9 +77,9 @@ figure on its evidence page is the table view.
legends for units, scales, and marks. Keep at most one short footer for context
needed to interpret a comparison. Methodology and commentary belong on the
evidence page; the SVG's accessible description retains the measured values.
- **Proa is the accent; every other engine is neutral grey.** A figure with no
Proa row would accent nothing, because highlighting a row there would imply a
comparison the data cannot support.
- **Proa is the primary accent.** Multi-engine load curves use distinct colors
so overlapping series remain readable; other comparisons use neutral peers.
A figure with no Proa row accents nothing.
- **Logarithmic axes are labelled as such**, and are used wherever a panel spans
more than about one order of magnitude.
- **Two measures never share one axis.** Where a figure shows both time and
Expand All @@ -96,3 +96,5 @@ of [`lib/theme.mjs`](lib/theme.mjs).
Figures are written as XML, not HTML: GitHub serves `.svg` as `image/svg+xml` and
parses it strictly, so `build.mjs` validates each document before writing it. A
figure that a browser would tolerate but an XML parser would reject fails the build.

Chart 07 overlays all four engines on shared throughput and memory plots, with four latency panels on the same log scale. It uses the fresh Proa/SvelteKit capture and retained Marko/React capture, with dates labeled. SvelteKit response end excludes hydration and browser rendering. Failure totals appear beside each latency panel; failures by rate remain in the source tables. The additional [browser-completion figure](../sveltekit/browser.svg), generated and checked by `node sveltekit/report.mjs [--check]`, measures first-panel and complete-DOM timing from retained browser samples.
2 changes: 1 addition & 1 deletion charts/build.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Regenerates every figure in this directory from the evidence pages' own tables
// (README.md, scoreboard.md, http-capacity.md, engineering.md).
// (README.md, scoreboard.md, http-capacity.md, engineering.md, sveltekit.md).
// node charts/build.mjs
// Re-run it after any capture that rewrites one of those pages.

Expand Down
4 changes: 2 additions & 2 deletions charts/figures/pages.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export function pageLoad(tables) {
return doc({
w: WIDTH,
h: y,
title: 'Proa, React, Marko and Go html/template serving the canonical pages over HTTP on one core',
title: 'Proa, React, Marko, Go html/template and SvelteKit serving the canonical pages over HTTP on one core',
desc: 'One core per engine, uncompressed loopback HTTP/1.1. Medians of three fresh processes; CPU counts all server threads. ' + t.objs.map((o) => `${clean(o.Engine)} on the ${clean(o.Workload)}: ${o['Sustained responses/s']} responses per second, ${o['CPU per response']} CPU per response, ${o['Peak RSS']} peak RSS`).join('. ') + '.',
body,
});
Expand Down Expand Up @@ -136,7 +136,7 @@ export function coldStart(tables) {
return doc({
w: WIDTH,
h: top + rows.length * rowH + 30,
title: 'Cold start to first product-grid response for Proa, React, Marko and Go html/template',
title: 'Cold start to first product-grid response for Proa, React, Marko, Go html/template and SvelteKit',
desc: 'Wall time includes process creation, runtime and module loading, fixture construction and the first render. Fifteen fresh processes per engine on one host. ' + rows.map((r) => `${r.name}: ${sig(r.p50)} ms median, ${sig(r.p95)} ms P95, ${sig(r.max)} ms slowest, ${sig(r.rssMiB)} MiB after the first response`).join('. ') + '.',
body,
});
Expand Down
Loading
Loading