Skip to content

Commit e04ab81

Browse files
committed
server architecture
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 1c1dab6 commit e04ab81

3 files changed

Lines changed: 124 additions & 30 deletions

File tree

benchmarks-website/README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,13 @@ local DB file. Five fact tables (`query_measurements`, `compression_times`,
3636
`compression_sizes`, `random_access_times`, `vector_search_runs`) plus a
3737
`commits` dim table — see [`server/src/schema.rs`](server/src/schema.rs) for
3838
the column contracts. Three HTML routes (`/`, `/chart/{slug}`,
39-
`/group/{slug}`) and four JSON routes (`GET /api/groups`,
40-
`GET /api/chart/{slug}`, `GET /api/group/{slug}`, `GET /health`), plus a
41-
bearer-gated `POST /api/ingest`. Charts render inline on the landing page via
42-
SSR + lazy hydration; visual downsampling (LTTB at most
43-
`MAX_VISIBLE_POINTS = 500`) is client-side in
44-
[`server/static/chart-init.js`](server/static/chart-init.js).
39+
`/group/{slug}`) and four stable JSON routes (`GET /api/groups`,
40+
`GET /api/chart/{slug}`, `GET /api/group/{slug}`, `GET /health`), plus
41+
versioned group shard artifacts and bearer-gated `POST /api/ingest`. The hot
42+
website path serves precomputed, precompressed latest-100 artifacts from an
43+
in-memory read model; pages render chart shells and hydrate groups via shard
44+
artifacts, while full history warms in the background. See
45+
[`server/ARCHITECTURE.md`](server/ARCHITECTURE.md).
4546

4647
For the per-module crate map and the request-flow walkthrough, see the
4748
`//!` doc on [`server/src/lib.rs`](server/src/lib.rs). The producer side of
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<!--
2+
SPDX-License-Identifier: Apache-2.0
3+
SPDX-FileCopyrightText: Copyright the Vortex contributors
4+
-->
5+
6+
# Benchmark Server Architecture
7+
8+
The benchmark website is optimized around a materialized latest-100 read path.
9+
DuckDB is the source of truth, but normal landing-page and group-open
10+
hydration does not run SQL, serialize JSON, or compress responses per request.
11+
12+
## Hot Read Path
13+
14+
On startup the server builds a `ReadGeneration` from one DuckDB snapshot. That
15+
generation contains precomputed JSON artifacts for:
16+
17+
- `/api/groups`
18+
- default `/api/chart/{slug}` latest-100 payloads
19+
- default `/api/group/{slug}` latest-100 compatibility payloads
20+
- versioned group shard payloads under
21+
`/api/artifacts/{generation}/groups/{group_slug}/shards/{index}`
22+
23+
Each artifact is stored in memory as identity, gzip, and brotli bytes. Request
24+
handlers negotiate `Accept-Encoding` and serve those bytes directly with
25+
`ETag`, `Vary: Accept-Encoding`, `Content-Length`, and cache headers.
26+
27+
## Page Hydration
28+
29+
The landing page and `/group/{slug}` render group metadata plus chart shells,
30+
not inline chart payloads. Each group carries the active read generation, shard
31+
count, and shard URL prefix. `chart-init.js` fetches shard 0 on intent or group
32+
open so charts paint quickly, then queues the remaining latest-100 shards with
33+
bounded per-tab concurrency.
34+
35+
Latest-100 chart payloads include additive `history` metadata:
36+
37+
- `total_commits`: full x-axis length for the chart
38+
- `start_index`: where this payload starts in the full x-axis
39+
- `loaded_commits`: number of loaded commits
40+
- `complete`: whether the payload covers the full x-axis
41+
42+
The client normalizes incomplete latest-100 payloads onto the full virtual
43+
x-axis. Older unloaded commits are represented by blank labels and null series
44+
values, so the range strip, zoom limits, and slider bounds behave as if the
45+
whole history is present without fabricating data.
46+
47+
## Full-History Warmup
48+
49+
Opening a group queues `/api/chart/{slug}?n=all` for that group's charts in a
50+
separate low-concurrency priority queue. A later-opened group gets higher
51+
priority than queued work for older groups. If the user pans or zooms into an
52+
unloaded virtual range before warmup finishes, that chart's queued full-history
53+
request is promoted. In-flight requests are not cancelled.
54+
55+
When the full payload arrives, the client replaces the virtual latest-100
56+
payload in place and preserves the current x-range when possible.
57+
58+
## Fallback Paths
59+
60+
`?n=all` and non-default `?n=` windows still use the DB-backed fallback path.
61+
Those reads go through `QueryCache` single-flight entries and the DB read
62+
semaphore so cold or unusual requests do not stampede DuckDB. Ingest writes do
63+
not consume read permits.
64+
65+
## Ingest And Rebuild
66+
67+
Successful ingest invalidates `QueryCache` and schedules a read-model rebuild.
68+
The active generation remains live while rebuilding. Repeated rebuild requests
69+
coalesce, and a failed rebuild keeps serving the old generation. The server
70+
keeps the active generation plus the most recent previous generation so already
71+
loaded pages can continue resolving immutable shard URLs across a swap.
72+
73+
## Main Files
74+
75+
- `src/read_model.rs`: materialized generation and encoded artifact serving
76+
- `src/api/mod.rs`: API routing between materialized artifacts and fallbacks
77+
- `src/api/charts.rs`: chart DTO construction and `history` metadata
78+
- `src/html/mod.rs`, `src/html/landing.rs`: shell/shard HTML rendering
79+
- `static/chart-init.js`: virtual-axis normalization, shard hydration, and
80+
full-history priority warmup
81+
- `src/query_cache.rs`: single-flight fallback cache
82+
- `src/db.rs`: DuckDB connection cloning and read backpressure

benchmarks-website/server/src/lib.rs

Lines changed: 35 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,31 @@
99
//! plus every HTML page. All static assets (Chart.js + the zoom plugin +
1010
//! `chart-init.js` + `style.css` + the two logos) are baked into the
1111
//! binary via `include_bytes!` so a deploy is one binary plus a database
12-
//! file. A `tower-http::CompressionLayer` wraps every response — the
13-
//! landing page HTML alone is several hundred KB uncompressed, so this is
14-
//! the single biggest cold-load win.
12+
//! file.
13+
//!
14+
//! The hot website path is a materialized latest-100 read model. DuckDB
15+
//! remains the source of truth, but default group/chart hydration is served
16+
//! from precomputed, precompressed in-memory artifacts. See
17+
//! `benchmarks-website/server/ARCHITECTURE.md` for the higher-level model.
1518
//!
1619
//! ## Routes
1720
//!
18-
//! - `GET /` — landing page, every group rendered as a collapsed
19-
//! `<details>`. The first group's chart payloads are inlined into the
20-
//! HTML; the rest are shells fetched on first toggle.
21-
//! - `GET /chart/{slug}` — single-chart permalink.
22-
//! - `GET /group/{slug}` — every chart in one group on a single page.
21+
//! - `GET /` — landing page, every group rendered as chart shells plus
22+
//! versioned shard metadata. Groups hydrate on intent/open.
23+
//! - `GET /chart/{slug}` — single-chart permalink. Defaults to the
24+
//! materialized latest-100 window; `?n=all` uses the DB fallback.
25+
//! - `GET /group/{slug}` — every chart shell in one group on a single page,
26+
//! opened by default and hydrated through the shard path.
2327
//! - `GET /static/...` — the bundled JS / CSS / logos.
2428
//! - `GET /api/groups` — flat list of every group with chart-link metadata.
25-
//! - `GET /api/chart/{slug}` — one chart's payload (`commits`, `series`,
26-
//! `unit_kind`, ...).
27-
//! - `GET /api/group/{slug}` — every chart in one group, payload-inlined.
29+
//! Defaults to a materialized artifact.
30+
//! - `GET /api/chart/{slug}` — one chart's payload (`history`, `commits`,
31+
//! `series`, `unit_kind`, ...). Defaults to a materialized latest-100
32+
//! artifact; `?n=all` and non-default windows use the DB fallback.
33+
//! - `GET /api/group/{slug}` — every chart in one group. Defaults to a
34+
//! materialized latest-100 compatibility artifact.
35+
//! - `GET /api/artifacts/{generation}/groups/{group_slug}/shards/{index}` —
36+
//! immutable versioned latest-100 group shard artifact for page hydration.
2837
//! - `GET /health` — liveness probe + per-table row counts.
2938
//! - `POST /api/ingest` — bearer-gated ingest. See [`ingest`] for the HTTP
3039
//! matrix and [`auth`] for the bearer middleware.
@@ -33,16 +42,18 @@
3342
//!
3443
//! | Module | Role |
3544
//! |---------------|---------------------------------------------------------------------------------------------|
36-
//! | [`app`] | [`app::AppState`] (DB handle + bearer + path) and the Axum router composition. |
45+
//! | [`app`] | [`app::AppState`] (DB handle + bearer + read store + paths) and the Axum router composition. |
3746
//! | [`auth`] | Bearer-token middleware for `/api/ingest`. |
38-
//! | [`db`] | [`db::DbHandle`] task-local connection cloning + the per-fact-table `measurement_id_*` hash functions. |
47+
//! | [`db`] | [`db::DbHandle`] task-local connection cloning + read backpressure + hash helpers. |
3948
//! | [`schema`] | DuckDB DDL ([`schema::SCHEMA_DDL`]) and the wire schema version. |
4049
//! | [`records`] | Wire shapes for `POST /api/ingest`. |
41-
//! | [`ingest`] | `POST /api/ingest` handler — envelope validation, transaction, upsert dispatch. |
50+
//! | [`ingest`] | `POST /api/ingest` handler, cache invalidation, and read-model rebuild scheduling. |
4251
//! | [`error`] | [`error::IngestError`] and [`error::ApiError`] with their HTTP-status mapping. |
4352
//! | [`slug`] | [`slug::ChartKey`] / [`slug::GroupKey`] enums + base64url round-trip. |
4453
//! | [`api`] | Read API. `mod.rs` mounts the handlers; submodules are listed on its module doc. |
4554
//! | [`html`] | HTML pages — `mod.rs` mounts the routes; submodules render the body. |
55+
//! | [`read_model`]| Materialized latest-100 generations and encoded artifact serving. |
56+
//! | [`query_cache`]| Single-flight cache for non-materialized fallback reads. |
4657
//!
4758
//! ## Request flow
4859
//!
@@ -51,16 +62,16 @@
5162
//! routes skip auth.
5263
//! 3. The handler parses body / path / query into typed inputs (e.g.
5364
//! [`slug::ChartKey::from_slug`]).
54-
//! 4. The handler hands a closure to [`db::run_blocking`], which clones a
55-
//! task-local DuckDB connection and runs the synchronous call on
56-
//! `tokio::task::spawn_blocking` so the runtime stays free.
57-
//! 5. The closure returns `Result<T, anyhow::Error>`. Errors are mapped
58-
//! into [`error::IngestError`] / [`error::ApiError`] with the right
59-
//! HTTP status.
60-
//! 6. The response is rendered (JSON via [`axum::Json`], HTML via the
61-
//! `maud` router in [`html`]).
62-
//! 7. Every response passes through [`tower_http::compression::CompressionLayer`]
63-
//! on the way out.
65+
//! 4. For default latest-100 API reads, the handler serves an encoded
66+
//! [`read_model`] artifact directly from memory.
67+
//! 5. For ingest, `?n=all`, and non-default windows, the handler hands a
68+
//! closure to [`db::run_blocking`], which clones a task-local DuckDB
69+
//! connection and runs synchronous work on `tokio::task::spawn_blocking`.
70+
//! 6. Fallback chart/group reads go through [`query_cache`] so concurrent
71+
//! identical misses share one compute.
72+
//! 7. Errors are mapped into [`error::IngestError`] / [`error::ApiError`]
73+
//! with the right HTTP status. HTML responses are rendered via `maud`;
74+
//! hot JSON artifacts are returned as already encoded bytes.
6475
6576
pub mod api;
6677
pub mod app;

0 commit comments

Comments
 (0)