Skip to content

Commit 6444858

Browse files
connortsui20claude
andauthored
UI refactors of benchmarks v3 (#7723)
## Summary Fixes the UI of the benchmarks v3 website. - no longer max of 1000 commits - LTTB dynamic downsampling on the client side - a bunch of other stuff ## Testing More snapshot testing. --------- Signed-off-by: Claude <claude@anthropic.com> Signed-off-by: Connor Tsui <connor.tsui20@gmail.com> Co-authored-by: Claude <claude@anthropic.com>
1 parent 87a51c1 commit 6444858

8 files changed

Lines changed: 713 additions & 137 deletions

File tree

benchmarks-website/server/src/api.rs

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,10 @@ use crate::error::ApiError;
3131
use crate::slug::ChartKey;
3232
use crate::slug::GroupKey;
3333

34-
/// Default cap on the number of commits returned per chart.
34+
/// Default cap on the number of commits returned per chart when no `?n=` is
35+
/// supplied. The HTML routes override this with their own per-page defaults
36+
/// (see [`crate::html`]).
3537
pub const DEFAULT_COMMIT_WINDOW: u32 = 100;
36-
/// Hard server-side ceiling on `?n=NNN`.
37-
pub const MAX_COMMIT_WINDOW: u32 = 1000;
3838

3939
/// Canonical group ordering, ported from the v2 site's hard-coded list at
4040
/// `origin/ct/vfvb:benchmarks-website/index.html`. Group names not in this
@@ -90,7 +90,11 @@ impl Default for CommitWindow {
9090
impl CommitWindow {
9191
/// Parse the `?n=...` query string parameter. `None` and malformed values
9292
/// fall back to [`CommitWindow::default`]. `"all"` (any case) means
93-
/// unbounded. Numeric values are clamped to `[1, MAX_COMMIT_WINDOW]`.
93+
/// unbounded. Numeric values are floored to `1` so `?n=0` becomes
94+
/// `?n=1`; there is no upper bound — large histories are kept as-is.
95+
/// Any further reduction in rendered point count happens client-side
96+
/// (see `static/chart-init.js` for the LTTB pass on the visible
97+
/// commit range).
9498
pub fn parse(raw: Option<&str>) -> Self {
9599
let Some(s) = raw else {
96100
return Self::default();
@@ -102,7 +106,7 @@ impl CommitWindow {
102106
trimmed
103107
.parse::<u32>()
104108
.ok()
105-
.map(|v| v.clamp(1, MAX_COMMIT_WINDOW))
109+
.map(|v| v.max(1))
106110
.and_then(NonZeroU32::new)
107111
.map(Self::Last)
108112
.unwrap_or_default()
@@ -276,7 +280,7 @@ pub struct ChartLink {
276280
pub slug: String,
277281
}
278282

279-
#[derive(Debug, Serialize)]
283+
#[derive(Debug, Clone, Serialize)]
280284
pub struct ChartResponse {
281285
pub display_name: String,
282286
pub unit: &'static str,
@@ -296,7 +300,7 @@ pub struct ChartResponse {
296300
/// engine + format, while `compression_*` and `random_access_times` only
297301
/// carry format. Vector-search series have neither and are omitted from the
298302
/// map entirely.
299-
#[derive(Debug, Default, Serialize)]
303+
#[derive(Debug, Default, Clone, Serialize)]
300304
pub struct SeriesTag {
301305
#[serde(skip_serializing_if = "Option::is_none")]
302306
pub engine: Option<String>,
@@ -313,7 +317,7 @@ pub struct FilterUniverse {
313317
pub formats: Vec<String>,
314318
}
315319

316-
#[derive(Debug, Serialize)]
320+
#[derive(Debug, Clone, Serialize)]
317321
pub struct CommitPoint {
318322
pub sha: String,
319323
pub timestamp: String,
@@ -1644,13 +1648,18 @@ mod tests {
16441648
}
16451649

16461650
#[test]
1647-
fn commit_window_parse_clamps() {
1651+
fn commit_window_parse_floors_zero_but_keeps_large_values() {
1652+
// Large values are kept as-is — full history is no longer clamped
1653+
// server-side. Visual downsampling happens client-side in
1654+
// `static/chart-init.js`, on the currently visible commit range.
16481655
let CommitWindow::Last(n) = CommitWindow::parse(Some("99999")) else {
16491656
panic!()
16501657
};
1651-
assert_eq!(n.get(), MAX_COMMIT_WINDOW);
1658+
assert_eq!(n.get(), 99_999);
1659+
1660+
// 0 floors to 1 since the underlying type is `NonZeroU32`.
16521661
let CommitWindow::Last(n) = CommitWindow::parse(Some("0")) else {
1653-
panic!("clamp of 0 should round to 1")
1662+
panic!("floor of 0 should round to 1")
16541663
};
16551664
assert_eq!(n.get(), 1);
16561665
}

benchmarks-website/server/src/html.rs

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,33 @@
44
//! HTML routes for the bench.vortex.dev v3 web UI.
55
//!
66
//! Three pages, all backed by the same per-chart UX:
7-
//! - `GET /` — landing page. Every group is a collapsible `<details>`. The
8-
//! first group is open by default and its charts pre-inline their JSON
9-
//! payload for a fast first paint; closed groups carry only the chart-card
10-
//! shell and their payloads are fetched on first toggle (`details.open`).
7+
//! - `GET /` — landing page. Every group is a collapsible `<details>`,
8+
//! all collapsed by default; the user picks which to expand. The
9+
//! *first* group's chart payloads are still pre-inlined in the HTML
10+
//! so opening it skips the JS fetch round-trip; every other group
11+
//! ships only chart-card shells and is fetched on first toggle.
1112
//! - `GET /chart/{slug}` — single chart page; permalink for sharing.
1213
//! - `GET /group/{slug}` — every chart in one group on a single page.
1314
//!
1415
//! Each chart card owns its own compact toolbar (scope slider + Y-axis). There
1516
//! is no page-level toolbar — every chart is independent. Scope is
16-
//! **zoom-as-scope**: each chart fetches up to [`api::MAX_COMMIT_WINDOW`]
17-
//! commits once, then the toolbar manipulates `chart.options.scales.x.min`/
18-
//! `max` to set the visible window. No refetches on scope change.
17+
//! **zoom-as-scope**: each chart fetches a generous window once, then the
18+
//! toolbar manipulates `chart.options.scales.x.min`/`max` to set the visible
19+
//! window. No refetches on scope change.
1920
//!
20-
//! URL query params (`?n=`) are accepted as power-user overrides on the
21-
//! initial fetch but are not written back from the toolbar. Per-chart UI
21+
//! Every HTML route defaults to the unbounded commit window
22+
//! ([`CommitWindow::All`]) so users can pan/zoom all the way back to the
23+
//! very first commit. The chart payload is sent **raw** — any visual
24+
//! downsampling happens client-side in `chart-init.js`, applied only to
25+
//! the currently visible commit range. The common case (a chart zoomed in
26+
//! to the last ~100 commits) renders raw with no LTTB at all.
27+
//!
28+
//! URL query param `?n=` is accepted as a power-user override on the
29+
//! initial fetch but is not written back from the toolbar. Per-chart UI
2230
//! state is intentionally not persisted in the URL — the user feedback
2331
//! emphasised that this UX should feel local-and-immediate, not "share a
24-
//! perfect view via URL". Permalinks (`/chart/{slug}`, `/group/{slug}`) are
25-
//! the sharing mechanism, not query strings.
32+
//! perfect view via URL". Permalinks (`/chart/{slug}`, `/group/{slug}`)
33+
//! are the sharing mechanism, not query strings.
2634
//!
2735
//! Slugs are opaque strings the server received from `/api/groups`; the
2836
//! handler echoes them straight into [`crate::slug::ChartKey::from_slug`]
@@ -32,8 +40,6 @@
3240
//! script) are served from `/static/...` via [`include_bytes!`] so the
3341
//! binary is fully self-contained.
3442
35-
use std::num::NonZeroU32;
36-
3743
use anyhow::Result;
3844
use axum::Router;
3945
use axum::extract::Path;
@@ -62,18 +68,19 @@ use crate::db;
6268
use crate::slug::ChartKey;
6369
use crate::slug::GroupKey;
6470

65-
/// How many commits each chart pre-fetches. The toolbar's scope slider zooms
66-
/// into smaller windows of this slice; we never refetch on scope change.
67-
/// Capped at the API ceiling so a future bigger ceiling is picked up here too.
68-
const PER_CHART_FETCH_N: u32 = api::MAX_COMMIT_WINDOW;
71+
// All HTML routes default to the unbounded commit window. The wire payload
72+
// is the raw `(commits, series)` data; visual downsampling (LTTB on the
73+
// currently visible commit range) happens client-side in
74+
// `static/chart-init.js`. `?n=` remains a power-user override on the
75+
// commit window itself (not on the rendered point count).
6976

7077
const CHART_JS: &[u8] = include_bytes!("../static/chart.umd.js");
7178
const CHART_ZOOM_JS: &[u8] = include_bytes!("../static/chartjs-plugin-zoom.umd.min.js");
7279
const CHART_INIT_JS: &[u8] = include_bytes!("../static/chart-init.js");
7380
const STYLE_CSS: &[u8] = include_bytes!("../static/style.css");
7481
const VORTEX_BLACK_SVG: &[u8] = include_bytes!("../../public/vortex_black_nobg.svg");
7582
const VORTEX_WHITE_SVG: &[u8] = include_bytes!("../../public/vortex_white_nobg.svg");
76-
const STATIC_ASSET_VERSION: &str = "bench-v3-ui-10";
83+
const STATIC_ASSET_VERSION: &str = "bench-v3-ui-15";
7784

7885
/// HTML routes mounted under `/`.
7986
pub fn router() -> Router<AppState> {
@@ -92,15 +99,14 @@ pub fn router() -> Router<AppState> {
9299
.route("/vortex_white_nobg.svg", get(serve_vortex_white_svg))
93100
}
94101

95-
/// Query string for HTML routes. `?n=` overrides the per-chart fetch size;
102+
/// Query string for HTML routes. `?n=` overrides the commit window;
96103
/// `?engine=` and `?format=` carry the global filter bar's selection so a
97104
/// shared link or refresh preserves which engines/formats are visible. The
98105
/// per-chart toolbar (Y axis, scope slider) remains local-only — its state
99106
/// is intentionally not in the URL.
100107
#[derive(Debug, Default, Deserialize)]
101108
pub struct UiQuery {
102-
/// Override for the per-chart fetch size. Defaults to `PER_CHART_FETCH_N`.
103-
/// Accepts `25|50|100|250|all`.
109+
/// Override for the per-chart fetch size. Accepts `25|50|100|250|all`.
104110
pub n: Option<String>,
105111
/// Comma-separated list of engines to keep visible across every chart.
106112
/// Empty / unset means no engine filter is active. Unknown engines are
@@ -113,14 +119,15 @@ pub struct UiQuery {
113119
}
114120

115121
impl UiQuery {
116-
/// Resolve the [`CommitWindow`] for the initial fetch. When `?n=` is
117-
/// unset, falls back to [`PER_CHART_FETCH_N`].
122+
/// Resolve the [`CommitWindow`] for HTML routes. Defaults to
123+
/// [`CommitWindow::All`] so users can pan/zoom all the way back to
124+
/// the very first commit on every chart, including the first
125+
/// (open-by-default) group on the landing page. Visual downsampling
126+
/// happens client-side on the visible commit range only.
118127
fn fetch_window(&self) -> CommitWindow {
119128
match self.n.as_deref() {
120129
Some(_) => CommitWindow::parse(self.n.as_deref()),
121-
None => {
122-
CommitWindow::Last(NonZeroU32::new(PER_CHART_FETCH_N).expect("non-zero default"))
123-
}
130+
None => CommitWindow::All,
124131
}
125132
}
126133

@@ -190,8 +197,9 @@ async fn landing(State(state): State<AppState>, Query(ui): Query<UiQuery>) -> Re
190197

191198
/// One group's worth of data for the landing page.
192199
///
193-
/// The first group (in canonical order) ships with `charts` populated so the
194-
/// open-by-default `<details>` paints immediately. Subsequent groups ship
200+
/// The first group (in canonical order) ships with `charts` populated so
201+
/// the moment the user expands it the chart hydrates from the inline
202+
/// JSON without a network round-trip. Every other group ships
195203
/// with `charts` empty and only their chart-card shells — payloads are
196204
/// fetched client-side on first `details.toggle` to keep the cold landing
197205
/// HTML small.
@@ -220,8 +228,9 @@ fn collect_landing_groups(conn: &Connection, window: &CommitWindow) -> Result<Ve
220228
let mut out = Vec::with_capacity(groups.len());
221229
for (i, group) in groups.into_iter().enumerate() {
222230
let inlined = if i == 0 {
223-
// First (open-by-default) group: pre-fetch every chart so the
224-
// first paint is fast and there is no JS round-trip.
231+
// First group in canonical order: pre-fetch every chart so
232+
// the moment the user expands it the chart hydrates from
233+
// the inline JSON without a JS round-trip.
225234
let mut v = Vec::with_capacity(group.charts.len());
226235
for link in &group.charts {
227236
let key = ChartKey::from_slug(&link.slug)?;
@@ -512,9 +521,9 @@ fn landing_body(groups: &[LandingGroup]) -> Markup {
512521
// `<script id="chart-data-N">` agree across groups.
513522
let mut idx_iter = 0usize..total_charts;
514523
html! {
515-
@for (group_idx, group) in groups.iter().enumerate() {
524+
@for group in groups.iter() {
516525
section.group-details data-group-name=(group.name) {
517-
details.group-disclosure open[group_idx == 0] {
526+
details.group-disclosure {
518527
summary.group-summary {
519528
span.group-summary-row {
520529
span.group-name { (group.name) }
@@ -549,6 +558,7 @@ fn chart_card(link: &api::ChartLink, idx: usize, inlined: Option<&NamedChartResp
549558
section.chart-card data-chart-index=(idx) data-chart-slug=(link.slug) {
550559
h3.chart-card-title {
551560
a href=(permalink) { (link.name) }
561+
(downsample_badge_slot())
552562
}
553563
(per_chart_toolbar(idx))
554564
div.chart-tooltip-host {}
@@ -568,6 +578,18 @@ fn chart_card(link: &api::ChartLink, idx: usize, inlined: Option<&NamedChartResp
568578
}
569579
}
570580

581+
/// Empty hidden slot for the LTTB badge. `chart-init.js` flips it on when
582+
/// the *currently visible* commit range exceeds the LTTB threshold and the
583+
/// rendered point count is therefore less than the raw point count in that
584+
/// range.
585+
fn downsample_badge_slot() -> Markup {
586+
html! {
587+
span.chart-badge.chart-badge--downsampled
588+
data-role="downsample-badge"
589+
hidden {}
590+
}
591+
}
592+
571593
fn chart_body(chart: &ChartResponse, slug: &str, payload_json: &str) -> Markup {
572594
let series_count = chart.series.len();
573595
let commit_count = chart.commits.len();
@@ -577,6 +599,7 @@ fn chart_body(chart: &ChartResponse, slug: &str, payload_json: &str) -> Markup {
577599
" · "
578600
(series_count) " series · "
579601
(commit_count) " commit" @if commit_count != 1 { "s" }
602+
(downsample_badge_slot())
580603
}
581604
section.chart-card data-chart-index="0" data-chart-slug=(slug) {
582605
(per_chart_toolbar(0))
@@ -609,6 +632,7 @@ fn group_body(group: &GroupChartsResponse) -> Markup {
609632
section.chart-card data-chart-index=(i) data-chart-slug=(item.slug) {
610633
h3.chart-card-title {
611634
a href=(permalink) { (item.name) }
635+
(downsample_badge_slot())
612636
}
613637
(per_chart_toolbar(i))
614638
div.chart-tooltip-host {}
@@ -859,8 +883,11 @@ fn per_chart_toolbar(idx: usize) -> Markup {
859883
div.toolbar.toolbar--card aria-label="Chart controls" {
860884
div.toolbar-group role="group" aria-label="Visible commits" {
861885
span.toolbar-label { "Show" }
886+
// `max` and `step` are placeholders — `chart-init.js` resets
887+
// them after constructing the chart so the slider tracks the
888+
// actual loaded commit count, not the initial markup.
862889
input id=(slider_id).toolbar-slider type="range"
863-
min="5" max="1000" step="5" value="100"
890+
min="5" max="100" step="1" value="100"
864891
data-role="scope-slider"
865892
aria-label="Custom commit window";
866893
}
@@ -991,12 +1018,9 @@ mod tests {
9911018
}
9921019

9931020
#[test]
994-
fn fetch_window_default_is_max() {
1021+
fn fetch_window_default_is_all() {
9951022
let ui = UiQuery::default();
996-
match ui.fetch_window() {
997-
CommitWindow::Last(n) => assert_eq!(n.get(), PER_CHART_FETCH_N),
998-
CommitWindow::All => panic!("default should be Last(N)"),
999-
}
1023+
assert!(matches!(ui.fetch_window(), CommitWindow::All));
10001024
}
10011025

10021026
#[test]
@@ -1009,6 +1033,11 @@ mod tests {
10091033
CommitWindow::Last(n) => assert_eq!(n.get(), 25),
10101034
CommitWindow::All => panic!(),
10111035
}
1036+
let ui = UiQuery {
1037+
n: Some("all".into()),
1038+
..Default::default()
1039+
};
1040+
assert!(matches!(ui.fetch_window(), CommitWindow::All));
10121041
}
10131042

10141043
#[test]

0 commit comments

Comments
 (0)