Skip to content

Commit da42f9c

Browse files
connortsui20claude
andcommitted
[claude] benchmarks-website v3: per-group descriptions + partial-coverage commits (#7784)
Two independent fixes for the v3 server, on the same branch. ## Task A — per-group hover descriptions Port v2's `BENCHMARK_DESCRIPTIONS` + `getBenchmarkDescription` strings into the v3 server. - New `description: Option<String>` field on `Group` and `GroupChartsResponse`, populated from a small hand-maintained table in `api/descriptions.rs`. Strings match v2 verbatim where v2 had one (Random Access, Compression, Compression Size, Clickbench, StatPopGen, PolarSignals). - TPC-H / TPC-DS descriptions are derived from the parsed group name so there's no need for one entry per `(storage, sf)` pair. TPC-H carries the `~XGB of data` annotation (`SF=1` → 1GB, `SF=10` → 10GB, etc.); TPC-DS does not, matching v2 verbatim. - Vector-search groups have no canonical description in v2, so the icon is omitted for those rather than fabricated. Render path: - Landing page: a small ⓘ icon (`.group-info-icon`, cursor-help) next to every group title, surfacing the description via a CSS-only `[data-tooltip]::after` pseudo-element. Visible on hover **and** on focus; the icon is `tabindex="0" role="note"` with `aria-label` set so it's reachable via keyboard and to screen readers. - `/group/{slug}` permalink page: same icon next to the chart-meta header. ## Task B — render commits with partial / missing series data **Symptom:** charts had invisible gaps where commits should be. **Diagnosis:** 1. `SeriesAccumulator::ensure_commit` only registered a commit *after* a series produced a row, so commits with zero rows in the chart's fact table were silently dropped from `commits[]`. 2. The client renderer set `spanGaps: true` on every dataset, so surviving nulls were drawn over as continuous lines. **Server fix.** Each `collect_*_chart` now seeds the accumulator with a canonical `commits`-dim pre-pass scoped to *every commit in the requested `CommitWindow` whose timestamp is at or after the earliest commit that has a row in this chart's fact table.* That bounded form keeps two things right: - Commits with zero fact-table rows still appear in `commits[]`; their per-series slot stays `null` and (with the client fix) renders as a visible gap. - Commits *older* than the chart's first fact-table row are excluded — we never render pre-history before the benchmark even existed. The accumulator now exposes `seed_commits` + `commit_idx(sha)`; the per-fact-table SQL is unchanged shape-wise but no longer carries `c.timestamp/message/url` (the seeded set already has those). **Client fix.** `spanGaps: false` on every dataset in `chart-init.js`, so missing measurements show up as a real break in the line. The external tooltip already cleanly skipped null rows via `.filter(Boolean)` — preserved. LTTB still operates on the union of non-null x-positions and skips downsampling when the union ≤ `MAX_VISIBLE_POINTS`, so a sparse series with a handful of non-null values across hundreds of commits still renders all of them as connected points. Bumped `STATIC_ASSET_VERSION` from `bench-v3-ui-17` → `bench-v3-ui-18` so cached browsers see the new bytes. ## Tests New `server/tests/web_ui.rs` covers both tasks: **Task A:** - `landing_page_renders_group_descriptions` — verbatim v2 strings + the SF-derived TPC blurb both appear on `/`. - `group_page_renders_description` — same icon on `/group/{slug}`. - `vector_search_group_has_no_description_icon` — no canonical description ⇒ no icon. - `groups_api_carries_description_field` — wire shape: `description` field present where v2 had one, absent for vector-search groups. **Task B:** - `chart_includes_commits_with_partial_series_coverage` — the headline regression test: commits A (X+Y), B (only Y), C (X+Y); B appears in `commits[]` with `null` for X. - `chart_includes_commits_with_zero_rows_in_fact_table` — a commit that emitted only `compression_size` still appears on the random-access chart's x-axis. - `chart_excludes_commits_before_first_fact_row` — commits older than the bench's first row stay off the chart. Plus unit tests in `api/descriptions.rs` for the static-vs-derived dispatch and the SF parser edge cases. ## Conventions - Branched from `ct/benchmarks-v3`, PR'd back to it. - Every commit has a `Signed-off-by:` trailer. - Don't auto-merge — leaving for review. - Local `cargo build` was hitting environment issues (target-dir lock contention); skipped in favour of GitHub Actions for the canonical Rust checks. The Rust changes are mechanical (new module, new field on existing structs, refactor of `collect_*_chart` to a seed-then-fill pattern) and well-covered by the new tests above. ## Test plan - [ ] CI green (`cargo test -p vortex-bench-server`, fmt, clippy on touched code) — tracked by GitHub Actions. - [ ] Open landing page, hover a group title's ⓘ → description appears below the icon. - [ ] Tab to a group title's ⓘ → description still appears (focus path). - [ ] Visit `/group/<slug>` for Random Access → description renders next to chart count. - [ ] Find a chart with known partial coverage; confirm previously-missing commits now appear with visible gaps in the affected series. --- _Generated by [Claude Code](https://claude.ai/code/session_01GCbWrKBT1ToBevooJRCJMx)_ --------- Signed-off-by: Claude <noreply@anthropic.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent b0227db commit da42f9c

15 files changed

Lines changed: 1096 additions & 110 deletions

File tree

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

Lines changed: 206 additions & 82 deletions
Large diffs are not rendered by default.
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Editorial group descriptions, ported from v2.
5+
//!
6+
//! These strings are the source of truth for the hover tooltip rendered on
7+
//! every `<details>` group title on the landing page and on the
8+
//! `/group/{slug}` permalink. They are deliberately editorial and
9+
//! hand-maintained — derived from the group *name*, not from the database —
10+
//! so that adding a new group's blurb is a one-line edit here rather than a
11+
//! schema or ingest change.
12+
//!
13+
//! Source of truth in v2 (kept verbatim where applicable):
14+
//! - `benchmarks-website/src/utils.js` — `getBenchmarkDescription`
15+
//! - `benchmarks-website/src/config.js` — `BENCHMARK_DESCRIPTIONS`,
16+
//! `QUERY_SUITES.description`
17+
//!
18+
//! TPC-H / TPC-DS fan out by storage and scale factor; the description is
19+
//! synthesised from the parsed name so we don't have to hand-maintain one
20+
//! entry per `(storage, sf)` pair.
21+
22+
/// Look up a short editorial description for a group display name. Returns
23+
/// `None` when the group has no canonical description (e.g. vector-search
24+
/// groups) — callers render the title without a tooltip in that case.
25+
pub fn group_description(name: &str) -> Option<String> {
26+
if let Some(d) = tpc_description(name) {
27+
return Some(d);
28+
}
29+
static_description(name).map(str::to_string)
30+
}
31+
32+
/// Hard-coded, name-keyed descriptions for the non-fan-out groups. These
33+
/// match v2 verbatim where v2 had a description; new strings here should
34+
/// match the wording style v2 set.
35+
fn static_description(name: &str) -> Option<&'static str> {
36+
match name {
37+
"Random Access" => {
38+
Some("Tests performance of selecting arbitrary row indices from a file on NVMe storage")
39+
}
40+
"Compression" => Some(
41+
"Measures encoding and decoding throughput (MB/s) for Vortex files and Parquet \
42+
files (with zstd page compression)",
43+
),
44+
"Compression Size" => Some(
45+
"Compares compressed file sizes and compression ratios across different encoding \
46+
strategies",
47+
),
48+
"Clickbench" => Some(
49+
"ClickHouse's analytical benchmark suite testing real-world query patterns on web \
50+
analytics data",
51+
),
52+
"Statistical and Population Genetics" => {
53+
Some("A suite of Statistical and Population genetics queries using the gnomAD dataset")
54+
}
55+
"PolarSignals Profiling" => Some(
56+
"Profiling data benchmark modeled on PolarSignals/Parca, exercising scan-layer \
57+
performance with projection and filter pushdown on deeply nested schemas",
58+
),
59+
_ => None,
60+
}
61+
}
62+
63+
/// Derive a description for `TPC-H (NVMe|S3) (SF=N)` and `TPC-DS (NVMe) (SF=N)`
64+
/// group names. The shape is fixed because [`crate::api::groups::group_name_query`]
65+
/// emits exactly this format for tpch/tpcds. Returns `None` for any name that
66+
/// does not start with `TPC-H ` or `TPC-DS `.
67+
fn tpc_description(name: &str) -> Option<String> {
68+
let parts = if let Some(rest) = name.strip_prefix("TPC-H ") {
69+
Some(("TPC-H", rest))
70+
} else {
71+
name.strip_prefix("TPC-DS ").map(|rest| ("TPC-DS", rest))
72+
};
73+
let (suite, rest) = parts?;
74+
let storage = if rest.starts_with("(NVMe)") {
75+
"nvme"
76+
} else if rest.starts_with("(S3)") {
77+
"s3"
78+
} else {
79+
return None;
80+
};
81+
let sf = parse_sf(rest)?;
82+
Some(format_tpc(suite, storage, &sf))
83+
}
84+
85+
/// Pull `SF=N` (digits only) out of strings like `(NVMe) (SF=10)`. Returns
86+
/// `None` if no `SF=` substring or the digits don't parse.
87+
fn parse_sf(s: &str) -> Option<String> {
88+
let after = s.split_once("SF=")?.1;
89+
let digits: String = after.chars().take_while(char::is_ascii_digit).collect();
90+
if digits.is_empty() {
91+
None
92+
} else {
93+
Some(digits)
94+
}
95+
}
96+
97+
/// Render the v2-compatible TPC blurb. Storage label comes from the parsed
98+
/// group name; scale-bytes annotation only renders for TPC-H (TPC-DS in v2
99+
/// did not annotate scale bytes).
100+
fn format_tpc(suite: &str, storage: &str, sf: &str) -> String {
101+
let storage_phrase = match storage {
102+
"nvme" => "on local NVMe storage",
103+
"s3" => "against S3 storage",
104+
_ => "on local NVMe storage",
105+
};
106+
let bytes = match sf {
107+
"1" => Some("1GB"),
108+
"10" => Some("10GB"),
109+
"100" => Some("100GB"),
110+
"1000" => Some("1TB"),
111+
_ => None,
112+
};
113+
match (suite, bytes) {
114+
("TPC-H", Some(b)) => {
115+
format!("TPC-H benchmark queries {storage_phrase} at SF={sf} (~{b} of data)",)
116+
}
117+
("TPC-H", None) => format!("TPC-H benchmark queries {storage_phrase} at SF={sf}"),
118+
("TPC-DS", _) => format!("TPC-DS benchmark queries {storage_phrase} at SF={sf}"),
119+
_ => format!("{suite} benchmark queries {storage_phrase} at SF={sf}"),
120+
}
121+
}
122+
123+
#[cfg(test)]
124+
mod tests {
125+
use super::*;
126+
127+
#[test]
128+
fn static_descriptions_match_v2() {
129+
assert_eq!(
130+
group_description("Random Access").as_deref(),
131+
Some(
132+
"Tests performance of selecting arbitrary row indices from a file on NVMe storage"
133+
),
134+
);
135+
assert_eq!(
136+
group_description("Compression").as_deref(),
137+
Some(
138+
"Measures encoding and decoding throughput (MB/s) for Vortex files and Parquet \
139+
files (with zstd page compression)",
140+
),
141+
);
142+
assert_eq!(
143+
group_description("Compression Size").as_deref(),
144+
Some(
145+
"Compares compressed file sizes and compression ratios across different encoding \
146+
strategies",
147+
),
148+
);
149+
assert_eq!(
150+
group_description("Clickbench").as_deref(),
151+
Some(
152+
"ClickHouse's analytical benchmark suite testing real-world query patterns on \
153+
web analytics data",
154+
),
155+
);
156+
assert_eq!(
157+
group_description("Statistical and Population Genetics").as_deref(),
158+
Some("A suite of Statistical and Population genetics queries using the gnomAD dataset",),
159+
);
160+
assert_eq!(
161+
group_description("PolarSignals Profiling").as_deref(),
162+
Some(
163+
"Profiling data benchmark modeled on PolarSignals/Parca, exercising scan-layer \
164+
performance with projection and filter pushdown on deeply nested schemas",
165+
),
166+
);
167+
}
168+
169+
#[test]
170+
fn tpch_descriptions_carry_scale_bytes() {
171+
assert_eq!(
172+
group_description("TPC-H (NVMe) (SF=1)").as_deref(),
173+
Some("TPC-H benchmark queries on local NVMe storage at SF=1 (~1GB of data)"),
174+
);
175+
assert_eq!(
176+
group_description("TPC-H (S3) (SF=10)").as_deref(),
177+
Some("TPC-H benchmark queries against S3 storage at SF=10 (~10GB of data)"),
178+
);
179+
assert_eq!(
180+
group_description("TPC-H (NVMe) (SF=100)").as_deref(),
181+
Some("TPC-H benchmark queries on local NVMe storage at SF=100 (~100GB of data)"),
182+
);
183+
assert_eq!(
184+
group_description("TPC-H (S3) (SF=1000)").as_deref(),
185+
Some("TPC-H benchmark queries against S3 storage at SF=1000 (~1TB of data)"),
186+
);
187+
}
188+
189+
#[test]
190+
fn tpcds_descriptions_omit_scale_bytes() {
191+
assert_eq!(
192+
group_description("TPC-DS (NVMe) (SF=1)").as_deref(),
193+
Some("TPC-DS benchmark queries on local NVMe storage at SF=1"),
194+
);
195+
assert_eq!(
196+
group_description("TPC-DS (NVMe) (SF=10)").as_deref(),
197+
Some("TPC-DS benchmark queries on local NVMe storage at SF=10"),
198+
);
199+
}
200+
201+
#[test]
202+
fn unknown_groups_have_no_description() {
203+
assert_eq!(group_description("cohere-large-10m / partitioned"), None);
204+
assert_eq!(group_description("Made-up benchmark"), None);
205+
}
206+
207+
#[test]
208+
fn malformed_tpc_names_fall_through() {
209+
// No `(NVMe)` / `(S3)` prefix → not matched.
210+
assert_eq!(group_description("TPC-H something else"), None);
211+
// SF= without digits → not matched.
212+
assert_eq!(group_description("TPC-H (NVMe) (SF=)"), None);
213+
}
214+
}

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

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ pub struct GroupsResponse {
6161
}
6262

6363
/// One group: a display name, a slug for the group permalink, and the chart
64-
/// links inside it. Optionally carries a v2-compatible rollup summary.
64+
/// links inside it. Optionally carries a v2-compatible rollup summary and a
65+
/// short editorial description (rendered as a hover tooltip on the
66+
/// disclosure title).
6567
#[derive(Debug, Serialize)]
6668
pub struct Group {
6769
/// Human-readable group label rendered in the disclosure header.
@@ -73,6 +75,11 @@ pub struct Group {
7375
/// Optional v2-compatible rollup computed from the fact tables.
7476
#[serde(skip_serializing_if = "Option::is_none")]
7577
pub summary: Option<Summary>,
78+
/// Short editorial description ported from v2's `BENCHMARK_DESCRIPTIONS` +
79+
/// `getBenchmarkDescription`. Rendered as a hover tooltip on the disclosure title; absent when
80+
/// no description exists for this group name (e.g. vector-search groups).
81+
#[serde(skip_serializing_if = "Option::is_none")]
82+
pub description: Option<String>,
7683
}
7784

7885
/// All charts in one group, returned by `GET /api/group/{slug}`.
@@ -83,6 +90,9 @@ pub struct GroupChartsResponse {
8390
/// Optional v2-compatible rollup computed from the fact tables.
8491
#[serde(skip_serializing_if = "Option::is_none")]
8592
pub summary: Option<Summary>,
93+
/// Optional editorial description, mirroring [`Group::description`].
94+
#[serde(skip_serializing_if = "Option::is_none")]
95+
pub description: Option<String>,
8696
/// Every chart inside the group, with full payload inlined.
8797
pub charts: Vec<NamedChartResponse>,
8898
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use anyhow::Context as _;
1111
use anyhow::Result;
1212
use duckdb::Connection;
1313

14+
use super::descriptions::group_description;
1415
use super::dto::ChartLink;
1516
use super::dto::Group;
1617
use super::dto::group_sort_key;
@@ -42,6 +43,7 @@ pub(crate) fn collect_groups(conn: &Connection) -> Result<Vec<Group>> {
4243
let key = GroupKey::from_slug(&group.slug)
4344
.with_context(|| format!("invalid generated group slug: {}", group.slug))?;
4445
group.summary = collect_group_summary(conn, &key, &group.charts)?;
46+
group.description = group_description(&group.name);
4547
}
4648

4749
// Apply canonical ordering. `sort_by_key` is stable, so groups whose
@@ -96,6 +98,7 @@ fn collect_query_groups(conn: &Connection) -> Result<Vec<Group>> {
9698
slug: group_slug,
9799
charts: Vec::new(),
98100
summary: None,
101+
description: None,
99102
});
100103
current = Some(key);
101104
}
@@ -213,6 +216,7 @@ fn collect_compression_time_group(conn: &Connection) -> Result<Option<Group>> {
213216
slug: GroupKey::CompressionTimeGroup.to_slug(),
214217
charts,
215218
summary: None,
219+
description: None,
216220
}))
217221
}
218222
}
@@ -258,6 +262,7 @@ fn collect_compression_size_group(conn: &Connection) -> Result<Option<Group>> {
258262
slug: GroupKey::CompressionSizeGroup.to_slug(),
259263
charts,
260264
summary: None,
265+
description: None,
261266
}))
262267
}
263268
}
@@ -287,6 +292,7 @@ fn collect_random_access_group(conn: &Connection) -> Result<Option<Group>> {
287292
slug: GroupKey::RandomAccessGroup.to_slug(),
288293
charts,
289294
summary: None,
295+
description: None,
290296
}))
291297
}
292298
}
@@ -324,6 +330,7 @@ fn collect_vector_search_groups(conn: &Connection) -> Result<Vec<Group>> {
324330
slug: group_slug,
325331
charts: Vec::new(),
326332
summary: None,
333+
description: None,
327334
});
328335
current = Some(key);
329336
}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,17 @@
99
//! [`crate::slug::ChartKey`] / [`crate::slug::GroupKey`].
1010
//!
1111
//! Submodules:
12-
//! - [`mod@dto`] — every wire-shape struct (`Group`, `ChartResponse`, …).
13-
//! - [`mod@window`] — [`CommitWindow`] + [`ChartQuery`].
14-
//! - [`mod@groups`] — discovery passes that build the group / chart-link tree.
15-
//! - [`mod@summary`] — v2-compatible per-group rollups.
16-
//! - [`mod@charts`] — `chart_payload` + the per-fact-table `collect_*_chart`
12+
//! - [`mod@dto`] — every wire-shape struct (`Group`, `ChartResponse`, …).
13+
//! - [`mod@window`] — [`CommitWindow`] + [`ChartQuery`].
14+
//! - [`mod@groups`] — discovery passes that build the group / chart-link tree.
15+
//! - [`mod@summary`] — v2-compatible per-group rollups.
16+
//! - [`mod@charts`] — `chart_payload` + the per-fact-table `collect_*_chart`
1717
//! functions and their shared `SeriesAccumulator`.
18-
//! - [`mod@filter`] — chip-universe collection for the global filter bar.
18+
//! - [`mod@filter`] — chip-universe collection for the global filter bar.
19+
//! - [`mod@descriptions`] — editorial blurbs surfaced as hover tooltips.
1920
2021
pub mod charts;
22+
pub mod descriptions;
2123
pub mod dto;
2224
pub mod filter;
2325
pub mod groups;

benchmarks-website/server/src/html/chart.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use maud::PreEscaped;
1212
use maud::html;
1313

1414
use super::landing::downsample_badge_slot;
15+
use super::landing::group_description_icon;
1516
use super::render::escape_json_for_script;
1617
use super::summary::summary_markup;
1718
use super::toolbar::per_chart_toolbar;
@@ -55,6 +56,7 @@ pub(super) fn group_body(group: &GroupChartsResponse) -> Markup {
5556
html! {
5657
p.chart-meta {
5758
(chart_count) " chart" @if chart_count != 1 { "s" }
59+
(group_description_icon(group.description.as_deref()))
5860
}
5961
(summary_markup(group.summary.as_ref()))
6062
div.chart-grid {

benchmarks-website/server/src/html/landing.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ use crate::api::Summary;
3131
pub(super) struct LandingGroup {
3232
/// Display name rendered in the disclosure header.
3333
pub(super) name: String,
34+
/// Optional editorial blurb rendered as a hover tooltip on the
35+
/// disclosure title's info-icon.
36+
pub(super) description: Option<String>,
3437
/// Optional v2-compatible summary card rendered above the chart grid.
3538
pub(super) summary: Option<Summary>,
3639
/// Chart links for every chart in the group. Always present — we need
@@ -62,6 +65,7 @@ pub(super) fn landing_body(groups: &[LandingGroup]) -> Markup {
6265
summary.group-summary {
6366
span.group-summary-row {
6467
span.group-name { (group.name) }
68+
(group_description_icon(group.description.as_deref()))
6569
span.group-count {
6670
(group.chart_links.len()) " chart" @if group.chart_links.len() != 1 { "s" }
6771
}
@@ -84,6 +88,30 @@ pub(super) fn landing_body(groups: &[LandingGroup]) -> Markup {
8488
}
8589
}
8690

91+
/// Render the small ⓘ info icon that surfaces the group's editorial
92+
/// description on hover and on focus. The CSS-only tooltip uses a
93+
/// `data-tooltip` attribute so it shows below the icon (see `style.css`'s
94+
/// `.group-info-icon` rule). The icon itself is keyboard-focusable and
95+
/// `aria-label`-ed so the description is reachable via the keyboard and to
96+
/// screen readers.
97+
///
98+
/// Returns an empty markup fragment when `description` is `None` so groups
99+
/// without a canonical blurb (e.g. vector-search groups) render unchanged.
100+
pub(super) fn group_description_icon(description: Option<&str>) -> Markup {
101+
let Some(text) = description else {
102+
return html! {};
103+
};
104+
html! {
105+
span.group-info-icon
106+
tabindex="0"
107+
role="note"
108+
aria-label=(text)
109+
data-tooltip=(text) {
110+
"ⓘ"
111+
}
112+
}
113+
}
114+
87115
/// Render one chart-card. `inlined` carries the JSON payload when the
88116
/// server pre-fetched it; absent on closed-by-default landing groups, where
89117
/// the JS fetches on first `details.toggle`.

0 commit comments

Comments
 (0)