Skip to content

Commit 7fc5697

Browse files
committed
Improve benchmark website hydration read path
Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent c59e16f commit 7fc5697

27 files changed

Lines changed: 2381 additions & 626 deletions

Cargo.lock

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

benchmarks-website/server/Cargo.toml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,15 @@ path = "src/main.rs"
2626
anyhow = { workspace = true }
2727
axum = "0.8"
2828
base64 = "0.22"
29+
brotli = "8.0.2"
30+
bytes = "1.11"
31+
dashmap = { workspace = true }
2932
# track vortex-duckdb's bundled engine version (build.rs)
3033
duckdb = { version = "1.10502", features = ["bundled"] }
34+
flate2 = "1"
3135
maud = { version = "0.27", features = ["axum"] }
32-
serde = { workspace = true, features = ["derive"] }
36+
parking_lot = { workspace = true }
37+
serde = { workspace = true, features = ["derive", "rc"] }
3338
serde_json = { workspace = true }
3439
subtle = "2.6"
3540
thiserror = { workspace = true }
@@ -39,9 +44,9 @@ tower-http = { version = "0.6", features = ["compression-br", "compression-gzip"
3944
tracing = { workspace = true, features = ["std"] }
4045
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }
4146
twox-hash = "2.1"
47+
vortex-utils = { workspace = true }
4248

4349
[dev-dependencies]
44-
flate2 = "1"
4550
insta = { workspace = true }
4651
reqwest = { workspace = true, features = ["json"] }
4752
tempfile = { workspace = true }

benchmarks-website/server/src/api/charts.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//! returns a [`ChartResponse`].
1010
1111
use std::collections::BTreeMap;
12+
use std::sync::Arc;
1213

1314
use anyhow::Context as _;
1415
use anyhow::Result;
@@ -100,7 +101,7 @@ pub(crate) fn collect_group_charts(
100101
charts.push(NamedChartResponse {
101102
name: link.name,
102103
slug: link.slug,
103-
chart,
104+
chart: Arc::new(chart),
104105
});
105106
}
106107
if charts.is_empty() {

benchmarks-website/server/src/api/dto.rs

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! ingest side, see [`crate::records`]).
1111
1212
use std::collections::BTreeMap;
13+
use std::sync::Arc;
1314

1415
use serde::Serialize;
1516
use serde_json::Value as JsonValue;
@@ -54,17 +55,21 @@ pub fn group_sort_key(name: &str) -> (usize, &str) {
5455
}
5556

5657
/// Body of `GET /api/groups`: every group with its chart links and summary.
58+
///
59+
/// The inner [`Vec`] is held in an [`Arc`] so [`crate::query_cache::QueryCache`]
60+
/// can serve the same allocation to every concurrent reader without cloning.
61+
/// `Arc<T>` serialises through to `T`, so the wire shape is unchanged.
5762
#[derive(Debug, Serialize)]
5863
pub struct GroupsResponse {
5964
/// Every group surfaced by the discovery passes, in canonical order.
60-
pub groups: Vec<Group>,
65+
pub groups: Arc<Vec<Group>>,
6166
}
6267

6368
/// One group: a display name, a slug for the group permalink, and the chart
6469
/// links inside it. Optionally carries a v2-compatible rollup summary and a
6570
/// short editorial description (rendered as a hover tooltip on the
6671
/// disclosure title).
67-
#[derive(Debug, Serialize)]
72+
#[derive(Debug, Clone, Serialize)]
6873
pub struct Group {
6974
/// Human-readable group label rendered in the disclosure header.
7075
pub name: String,
@@ -83,7 +88,7 @@ pub struct Group {
8388
}
8489

8590
/// All charts in one group, returned by `GET /api/group/{slug}`.
86-
#[derive(Debug, Serialize)]
91+
#[derive(Debug, Clone, Serialize)]
8792
pub struct GroupChartsResponse {
8893
/// Group display name, matching the `name` field on [`Group`].
8994
pub name: String,
@@ -98,7 +103,7 @@ pub struct GroupChartsResponse {
98103
}
99104

100105
/// Server-computed group summary, matching the v2 metadata contract.
101-
#[derive(Debug, Serialize)]
106+
#[derive(Debug, Clone, Serialize)]
102107
#[serde(tag = "type")]
103108
pub enum Summary {
104109
/// Random-access format ranking for the latest populated random-access chart.
@@ -161,7 +166,7 @@ pub enum Summary {
161166
}
162167

163168
/// One random-access summary row.
164-
#[derive(Debug, Serialize)]
169+
#[derive(Debug, Clone, Serialize)]
165170
pub struct RandomAccessRanking {
166171
/// Series name, normally the physical format.
167172
pub name: String,
@@ -172,7 +177,7 @@ pub struct RandomAccessRanking {
172177
}
173178

174179
/// One query benchmark summary row.
175-
#[derive(Debug, Serialize)]
180+
#[derive(Debug, Clone, Serialize)]
176181
pub struct QueryRanking {
177182
/// Series name, normally `engine:format`.
178183
pub name: String,
@@ -186,20 +191,24 @@ pub struct QueryRanking {
186191
/// A single chart inside a [`GroupChartsResponse`]. `name` is the chart's
187192
/// short label inside the group (e.g. `Q1`); `slug` round-trips through
188193
/// `/api/chart/{slug}`.
189-
#[derive(Debug, Serialize)]
194+
///
195+
/// `chart` is held in an [`Arc`] so the cache and the landing-page builder
196+
/// share the same allocation; `Arc<T>` serialises as `T`, so the wire shape
197+
/// is identical to a plain `ChartResponse`.
198+
#[derive(Debug, Clone, Serialize)]
190199
pub struct NamedChartResponse {
191200
/// Chart label rendered in the chart-card title (e.g. `Q1`).
192201
pub name: String,
193202
/// Slug for `/chart/{slug}`. Round-trips through [`crate::slug::ChartKey`].
194203
pub slug: String,
195204
/// Inlined chart payload — same shape as `/api/chart/{slug}`.
196205
#[serde(flatten)]
197-
pub chart: ChartResponse,
206+
pub chart: Arc<ChartResponse>,
198207
}
199208

200209
/// One chart's short label inside a group (e.g. `Q1`) plus the slug that
201210
/// resolves to its `/api/chart/{slug}` payload.
202-
#[derive(Debug, Serialize)]
211+
#[derive(Debug, Clone, Serialize)]
203212
pub struct ChartLink {
204213
/// Chart label rendered in the chart-card title (e.g. `Q1`).
205214
pub name: String,

benchmarks-website/server/src/api/mod.rs

Lines changed: 143 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ use axum::Json;
3232
use axum::extract::Path;
3333
use axum::extract::Query;
3434
use axum::extract::State;
35+
use axum::http::HeaderMap;
3536
use axum::response::IntoResponse;
37+
use axum::response::Response;
3638
use duckdb::Connection;
3739

3840
pub(crate) use self::charts::chart_payload;
@@ -62,53 +64,180 @@ pub use self::window::CommitWindow;
6264
use crate::app::AppState;
6365
use crate::db;
6466
use crate::error::ApiError;
67+
use crate::read_model::ArtifactCachePolicy;
6568
use crate::slug::ChartKey;
6669
use crate::slug::GroupKey;
6770

71+
pub(crate) fn read_transaction<T>(
72+
conn: &mut Connection,
73+
f: impl FnOnce(&Connection) -> Result<T>,
74+
) -> Result<T> {
75+
conn.execute_batch("BEGIN TRANSACTION")?;
76+
let result = f(conn);
77+
match result {
78+
Ok(value) => {
79+
conn.execute_batch("COMMIT")?;
80+
Ok(value)
81+
}
82+
Err(err) => {
83+
let _ = conn.execute_batch("ROLLBACK");
84+
Err(err)
85+
}
86+
}
87+
}
88+
6889
/// Handler for `GET /api/groups`.
69-
pub async fn groups(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
70-
let groups = db::run_blocking(&state.db, |conn| collect_groups(conn)).await?;
71-
Ok(Json(GroupsResponse { groups }))
90+
pub async fn groups(
91+
State(state): State<AppState>,
92+
headers: HeaderMap,
93+
) -> Result<Response, ApiError> {
94+
let generation = state.read_store.active();
95+
Ok(generation
96+
.groups_artifact()
97+
.response(&headers, ArtifactCachePolicy::Revalidate))
7298
}
7399

74100
/// Handler for `GET /api/chart/{slug}`.
75101
pub async fn chart(
76102
State(state): State<AppState>,
77103
Path(slug): Path<String>,
78104
Query(q): Query<ChartQuery>,
79-
) -> Result<impl IntoResponse, ApiError> {
105+
headers: HeaderMap,
106+
) -> Result<Response, ApiError> {
80107
let key = ChartKey::from_slug(&slug)
81108
.map_err(|e| ApiError::BadRequest(format!("invalid slug: {e}")))?;
82109
let window = q.window();
83-
let response =
84-
db::run_blocking(&state.db, move |conn| chart_payload(conn, &key, &window)).await?;
110+
if is_materialized_window(&window) {
111+
let generation = state.read_store.active();
112+
let response = generation
113+
.chart_artifact(&slug)
114+
.ok_or_else(|| ApiError::NotFound(format!("no data for slug {slug:?}")))?;
115+
return Ok(response.response(&headers, ArtifactCachePolicy::Revalidate));
116+
}
117+
let response = cached_chart_payload(&state, &slug, &key, &window).await?;
85118
let response =
86119
response.ok_or_else(|| ApiError::NotFound(format!("no data for slug {slug:?}")))?;
87-
Ok(Json(response))
120+
Ok(Json(response).into_response())
88121
}
89122

90123
/// Handler for `GET /api/group/{slug}`.
91124
pub async fn group(
92125
State(state): State<AppState>,
93126
Path(slug): Path<String>,
94127
Query(q): Query<ChartQuery>,
95-
) -> Result<impl IntoResponse, ApiError> {
128+
headers: HeaderMap,
129+
) -> Result<Response, ApiError> {
96130
let key = GroupKey::from_slug(&slug)
97131
.map_err(|e| ApiError::BadRequest(format!("invalid group slug: {e}")))?;
98132
let window = q.window();
99-
let response = db::run_blocking(&state.db, move |conn| {
100-
collect_group_charts(conn, &key, &window)
101-
})
102-
.await?;
133+
if is_materialized_window(&window) {
134+
let generation = state.read_store.active();
135+
let response = generation
136+
.group_artifact(&slug)
137+
.ok_or_else(|| ApiError::NotFound(format!("no data for group slug {slug:?}")))?;
138+
return Ok(response.response(&headers, ArtifactCachePolicy::Revalidate));
139+
}
140+
let response = cached_group_charts(&state, &slug, &key, &window).await?;
103141
let response =
104142
response.ok_or_else(|| ApiError::NotFound(format!("no data for group slug {slug:?}")))?;
105-
Ok(Json(response))
143+
Ok(Json(response).into_response())
144+
}
145+
146+
/// Handler for versioned latest-100 group shard artifacts.
147+
pub async fn group_shard_artifact(
148+
State(state): State<AppState>,
149+
Path((generation_id, group_slug, index)): Path<(String, String, usize)>,
150+
headers: HeaderMap,
151+
) -> Result<Response, ApiError> {
152+
let generation = state
153+
.read_store
154+
.generation(&generation_id)
155+
.ok_or_else(|| ApiError::NotFound(format!("unknown generation {generation_id:?}")))?;
156+
let artifact = generation
157+
.group_shard_artifact(&group_slug, index)
158+
.ok_or_else(|| ApiError::NotFound(format!("unknown group shard {group_slug:?}#{index}")))?;
159+
Ok(artifact.response(&headers, ArtifactCachePolicy::Immutable))
160+
}
161+
162+
fn is_materialized_window(window: &CommitWindow) -> bool {
163+
matches!(window, CommitWindow::Last(n) if n.get() == DEFAULT_COMMIT_WINDOW)
164+
}
165+
166+
/// Cache-aware wrapper around `collect_groups`.
167+
pub async fn cached_groups(state: &AppState) -> Result<std::sync::Arc<Vec<Group>>> {
168+
let db = state.db.clone();
169+
state
170+
.cache
171+
.groups(move || async move {
172+
db::run_read_blocking(&db, |conn| read_transaction(conn, collect_groups)).await
173+
})
174+
.await
175+
}
176+
177+
/// Cache-aware wrapper around [`collect_filter_universe`].
178+
pub async fn cached_filter_universe(state: &AppState) -> Result<std::sync::Arc<FilterUniverse>> {
179+
let db = state.db.clone();
180+
state
181+
.cache
182+
.filter_universe(move || async move {
183+
db::run_read_blocking(&db, |conn| read_transaction(conn, collect_filter_universe)).await
184+
})
185+
.await
186+
}
187+
188+
/// Cache-aware wrapper around `chart_payload`.
189+
pub async fn cached_chart_payload(
190+
state: &AppState,
191+
slug: &str,
192+
key: &ChartKey,
193+
window: &CommitWindow,
194+
) -> Result<Option<std::sync::Arc<ChartResponse>>> {
195+
let db = state.db.clone();
196+
let key_for_compute = key.clone();
197+
let window_for_compute = *window;
198+
state
199+
.cache
200+
.chart_payload(slug, window, move || async move {
201+
db::run_read_blocking(&db, move |conn| {
202+
read_transaction(conn, |conn| {
203+
chart_payload(conn, &key_for_compute, &window_for_compute)
204+
})
205+
})
206+
.await
207+
})
208+
.await
209+
}
210+
211+
/// Cache-aware wrapper around `collect_group_charts`.
212+
pub async fn cached_group_charts(
213+
state: &AppState,
214+
slug: &str,
215+
key: &GroupKey,
216+
window: &CommitWindow,
217+
) -> Result<Option<std::sync::Arc<GroupChartsResponse>>> {
218+
let db = state.db.clone();
219+
let key_for_compute = key.clone();
220+
let window_for_compute = *window;
221+
state
222+
.cache
223+
.group_charts(slug, window, move || async move {
224+
db::run_read_blocking(&db, move |conn| {
225+
read_transaction(conn, |conn| {
226+
collect_group_charts(conn, &key_for_compute, &window_for_compute)
227+
})
228+
})
229+
.await
230+
})
231+
.await
106232
}
107233

108234
/// Handler for `GET /health`.
109235
pub async fn health(State(state): State<AppState>) -> Result<impl IntoResponse, ApiError> {
110236
let path = state.db_path.display().to_string();
111-
let response = db::run_blocking(&state.db, move |conn| collect_health(conn, path)).await?;
237+
let response = db::run_read_blocking(&state.db, move |conn| {
238+
read_transaction(conn, |conn| collect_health(conn, path))
239+
})
240+
.await?;
112241
Ok(Json(response))
113242
}
114243

benchmarks-website/server/src/api/window.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use super::dto::DEFAULT_COMMIT_WINDOW;
1717
///
1818
/// `Last(n)` keeps the most recent `n` commits by `commits.timestamp`; `All`
1919
/// returns every commit ever ingested.
20-
#[derive(Debug, Clone, Copy)]
20+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
2121
pub enum CommitWindow {
2222
/// Keep the most recent `n` commits.
2323
Last(NonZeroU32),

0 commit comments

Comments
 (0)