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
31 changes: 20 additions & 11 deletions benchmarks-website/server/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,10 @@ use crate::error::ApiError;
use crate::slug::ChartKey;
use crate::slug::GroupKey;

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

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

#[derive(Debug, Serialize)]
#[derive(Debug, Clone, Serialize)]
pub struct ChartResponse {
pub display_name: String,
pub unit: &'static str,
Expand All @@ -296,7 +300,7 @@ pub struct ChartResponse {
/// engine + format, while `compression_*` and `random_access_times` only
/// carry format. Vector-search series have neither and are omitted from the
/// map entirely.
#[derive(Debug, Default, Serialize)]
#[derive(Debug, Default, Clone, Serialize)]
pub struct SeriesTag {
#[serde(skip_serializing_if = "Option::is_none")]
pub engine: Option<String>,
Expand All @@ -313,7 +317,7 @@ pub struct FilterUniverse {
pub formats: Vec<String>,
}

#[derive(Debug, Serialize)]
#[derive(Debug, Clone, Serialize)]
pub struct CommitPoint {
pub sha: String,
pub timestamp: String,
Expand Down Expand Up @@ -1644,13 +1648,18 @@ mod tests {
}

#[test]
fn commit_window_parse_clamps() {
fn commit_window_parse_floors_zero_but_keeps_large_values() {
// Large values are kept as-is — full history is no longer clamped
// server-side. Visual downsampling happens client-side in
// `static/chart-init.js`, on the currently visible commit range.
let CommitWindow::Last(n) = CommitWindow::parse(Some("99999")) else {
panic!()
};
assert_eq!(n.get(), MAX_COMMIT_WINDOW);
assert_eq!(n.get(), 99_999);

// 0 floors to 1 since the underlying type is `NonZeroU32`.
let CommitWindow::Last(n) = CommitWindow::parse(Some("0")) else {
panic!("clamp of 0 should round to 1")
panic!("floor of 0 should round to 1")
};
assert_eq!(n.get(), 1);
}
Expand Down
105 changes: 67 additions & 38 deletions benchmarks-website/server/src/html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,33 @@
//! HTML routes for the bench.vortex.dev v3 web UI.
//!
//! Three pages, all backed by the same per-chart UX:
//! - `GET /` — landing page. Every group is a collapsible `<details>`. The
//! first group is open by default and its charts pre-inline their JSON
//! payload for a fast first paint; closed groups carry only the chart-card
//! shell and their payloads are fetched on first toggle (`details.open`).
//! - `GET /` — landing page. Every group is a collapsible `<details>`,
//! all collapsed by default; the user picks which to expand. The
//! *first* group's chart payloads are still pre-inlined in the HTML
//! so opening it skips the JS fetch round-trip; every other group
//! ships only chart-card shells and is fetched on first toggle.
//! - `GET /chart/{slug}` — single chart page; permalink for sharing.
//! - `GET /group/{slug}` — every chart in one group on a single page.
//!
//! Each chart card owns its own compact toolbar (scope slider + Y-axis). There
//! is no page-level toolbar — every chart is independent. Scope is
//! **zoom-as-scope**: each chart fetches up to [`api::MAX_COMMIT_WINDOW`]
//! commits once, then the toolbar manipulates `chart.options.scales.x.min`/
//! `max` to set the visible window. No refetches on scope change.
//! **zoom-as-scope**: each chart fetches a generous window once, then the
//! toolbar manipulates `chart.options.scales.x.min`/`max` to set the visible
//! window. No refetches on scope change.
//!
//! URL query params (`?n=`) are accepted as power-user overrides on the
//! initial fetch but are not written back from the toolbar. Per-chart UI
//! Every HTML route defaults to the unbounded commit window
//! ([`CommitWindow::All`]) so users can pan/zoom all the way back to the
//! very first commit. The chart payload is sent **raw** — any visual
//! downsampling happens client-side in `chart-init.js`, applied only to
//! the currently visible commit range. The common case (a chart zoomed in
//! to the last ~100 commits) renders raw with no LTTB at all.
//!
//! URL query param `?n=` is accepted as a power-user override on the
//! initial fetch but is not written back from the toolbar. Per-chart UI
//! state is intentionally not persisted in the URL — the user feedback
//! emphasised that this UX should feel local-and-immediate, not "share a
//! perfect view via URL". Permalinks (`/chart/{slug}`, `/group/{slug}`) are
//! the sharing mechanism, not query strings.
//! perfect view via URL". Permalinks (`/chart/{slug}`, `/group/{slug}`)
//! are the sharing mechanism, not query strings.
//!
//! Slugs are opaque strings the server received from `/api/groups`; the
//! handler echoes them straight into [`crate::slug::ChartKey::from_slug`]
Expand All @@ -32,8 +40,6 @@
//! script) are served from `/static/...` via [`include_bytes!`] so the
//! binary is fully self-contained.

use std::num::NonZeroU32;

use anyhow::Result;
use axum::Router;
use axum::extract::Path;
Expand Down Expand Up @@ -62,18 +68,19 @@ use crate::db;
use crate::slug::ChartKey;
use crate::slug::GroupKey;

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

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

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

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

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

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

/// One group's worth of data for the landing page.
///
/// The first group (in canonical order) ships with `charts` populated so the
/// open-by-default `<details>` paints immediately. Subsequent groups ship
/// The first group (in canonical order) ships with `charts` populated so
/// the moment the user expands it the chart hydrates from the inline
/// JSON without a network round-trip. Every other group ships
/// with `charts` empty and only their chart-card shells — payloads are
/// fetched client-side on first `details.toggle` to keep the cold landing
/// HTML small.
Expand Down Expand Up @@ -220,8 +228,9 @@ fn collect_landing_groups(conn: &Connection, window: &CommitWindow) -> Result<Ve
let mut out = Vec::with_capacity(groups.len());
for (i, group) in groups.into_iter().enumerate() {
let inlined = if i == 0 {
// First (open-by-default) group: pre-fetch every chart so the
// first paint is fast and there is no JS round-trip.
// First group in canonical order: pre-fetch every chart so
// the moment the user expands it the chart hydrates from
// the inline JSON without a JS round-trip.
let mut v = Vec::with_capacity(group.charts.len());
for link in &group.charts {
let key = ChartKey::from_slug(&link.slug)?;
Expand Down Expand Up @@ -512,9 +521,9 @@ fn landing_body(groups: &[LandingGroup]) -> Markup {
// `<script id="chart-data-N">` agree across groups.
let mut idx_iter = 0usize..total_charts;
html! {
@for (group_idx, group) in groups.iter().enumerate() {
@for group in groups.iter() {
section.group-details data-group-name=(group.name) {
details.group-disclosure open[group_idx == 0] {
details.group-disclosure {
summary.group-summary {
span.group-summary-row {
span.group-name { (group.name) }
Expand Down Expand Up @@ -549,6 +558,7 @@ fn chart_card(link: &api::ChartLink, idx: usize, inlined: Option<&NamedChartResp
section.chart-card data-chart-index=(idx) data-chart-slug=(link.slug) {
h3.chart-card-title {
a href=(permalink) { (link.name) }
(downsample_badge_slot())
}
(per_chart_toolbar(idx))
div.chart-tooltip-host {}
Expand All @@ -568,6 +578,18 @@ fn chart_card(link: &api::ChartLink, idx: usize, inlined: Option<&NamedChartResp
}
}

/// Empty hidden slot for the LTTB badge. `chart-init.js` flips it on when
/// the *currently visible* commit range exceeds the LTTB threshold and the
/// rendered point count is therefore less than the raw point count in that
/// range.
fn downsample_badge_slot() -> Markup {
html! {
span.chart-badge.chart-badge--downsampled
data-role="downsample-badge"
hidden {}
}
}

fn chart_body(chart: &ChartResponse, slug: &str, payload_json: &str) -> Markup {
let series_count = chart.series.len();
let commit_count = chart.commits.len();
Expand All @@ -577,6 +599,7 @@ fn chart_body(chart: &ChartResponse, slug: &str, payload_json: &str) -> Markup {
" · "
(series_count) " series · "
(commit_count) " commit" @if commit_count != 1 { "s" }
(downsample_badge_slot())
}
section.chart-card data-chart-index="0" data-chart-slug=(slug) {
(per_chart_toolbar(0))
Expand Down Expand Up @@ -609,6 +632,7 @@ fn group_body(group: &GroupChartsResponse) -> Markup {
section.chart-card data-chart-index=(i) data-chart-slug=(item.slug) {
h3.chart-card-title {
a href=(permalink) { (item.name) }
(downsample_badge_slot())
}
(per_chart_toolbar(i))
div.chart-tooltip-host {}
Expand Down Expand Up @@ -859,8 +883,11 @@ fn per_chart_toolbar(idx: usize) -> Markup {
div.toolbar.toolbar--card aria-label="Chart controls" {
div.toolbar-group role="group" aria-label="Visible commits" {
span.toolbar-label { "Show" }
// `max` and `step` are placeholders — `chart-init.js` resets
// them after constructing the chart so the slider tracks the
// actual loaded commit count, not the initial markup.
input id=(slider_id).toolbar-slider type="range"
min="5" max="1000" step="5" value="100"
min="5" max="100" step="1" value="100"
data-role="scope-slider"
aria-label="Custom commit window";
}
Expand Down Expand Up @@ -991,12 +1018,9 @@ mod tests {
}

#[test]
fn fetch_window_default_is_max() {
fn fetch_window_default_is_all() {
let ui = UiQuery::default();
match ui.fetch_window() {
CommitWindow::Last(n) => assert_eq!(n.get(), PER_CHART_FETCH_N),
CommitWindow::All => panic!("default should be Last(N)"),
}
assert!(matches!(ui.fetch_window(), CommitWindow::All));
}

#[test]
Expand All @@ -1009,6 +1033,11 @@ mod tests {
CommitWindow::Last(n) => assert_eq!(n.get(), 25),
CommitWindow::All => panic!(),
}
let ui = UiQuery {
n: Some("all".into()),
..Default::default()
};
assert!(matches!(ui.fetch_window(), CommitWindow::All));
}

#[test]
Expand Down
Loading
Loading