This is an automated email from the ASF dual-hosted git repository.
spetz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 832c34164 refactor(bench): unify benchmarks list and fix best-pick
selection (#3146)
832c34164 is described below
commit 832c34164f995dcb5d622213869c5a5e2527e296
Author: Piotr Gankiewicz <[email protected]>
AuthorDate: Tue Apr 21 11:17:05 2026 +0200
refactor(bench): unify benchmarks list and fix best-pick selection (#3146)
The dual-mode sidebar (Version vs Recent) bred a cascade of
routing bugs, and the hero picked a year-old run because
`max_by(throughput)` had no time window.
Collapse the two scopes into one `/api/recent`-backed list where
hardware and gitref are plain filter dropdowns, AND-combined with
kind chips, sort, search, and param ranges. No scope state means
no race conditions: the sidebar fetches once, URL is authoritative.
Hero showcase now uses `latest_sweep`, which groups by
(hardware, gitref) of the newest-timestamp run and ranks by lowest
P99 → P99.9 → P99.99 → highest throughput (NaN-safe). While
loading, a branded pulsing Iggy mark replaces placeholder copy.
Gitref pill links to apache/iggy at that commit, both on the
landing and the detail title. A top-bar share button copies the
current URL so users don't grab the address bar.
---
assets/benchmarking_platform.png | Bin 1756918 -> 1165558
bytes
core/bench/dashboard/frontend/assets/style.css | 250 +++++++++++--
.../frontend/src/components/app_content.rs | 46 ++-
.../dashboard/frontend/src/components/footer.rs | 4 +-
.../frontend/src/components/layout/hero.rs | 204 ++++++----
.../frontend/src/components/layout/main_content.rs | 55 ++-
.../frontend/src/components/layout/sidebar.rs | 233 +++++++++---
.../frontend/src/components/layout/top_app_bar.rs | 88 +++--
.../{benchmark_selector.rs => benchmarks_list.rs} | 118 +++---
.../src/components/selectors/gitref_selector.rs | 63 ----
.../src/components/selectors/hardware_selector.rs | 61 ---
.../frontend/src/components/selectors/mod.rs | 5 +-
.../selectors/recent_benchmarks_selector.rs | 302 ---------------
.../components/tooltips/benchmark_info_tooltip.rs | 23 +-
.../components/tooltips/server_stats_tooltip.rs | 2 -
core/bench/dashboard/frontend/src/main.rs | 1 +
.../dashboard/frontend/src/state/benchmark.rs | 412 ++++++++++++++++++---
core/bench/dashboard/frontend/src/state/ui.rs | 25 +-
core/bench/dashboard/frontend/src/version.rs | 180 +++++++++
core/bench/report/src/plotting/chart.rs | 23 +-
20 files changed, 1295 insertions(+), 800 deletions(-)
diff --git a/assets/benchmarking_platform.png b/assets/benchmarking_platform.png
index dfc9e8c71..de8d1a74a 100644
Binary files a/assets/benchmarking_platform.png and
b/assets/benchmarking_platform.png differ
diff --git a/core/bench/dashboard/frontend/assets/style.css
b/core/bench/dashboard/frontend/assets/style.css
index 2e87bfc80..077bbd14f 100644
--- a/core/bench/dashboard/frontend/assets/style.css
+++ b/core/bench/dashboard/frontend/assets/style.css
@@ -335,14 +335,15 @@ body {
}
.chart-title-identifier {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: center;
font-size: var(--font-size-md, 0.75rem);
color: var(--color-text-muted);
- font-style: italic;
font-weight: 500;
margin-top: var(--spacing-sm);
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
max-width: 100%;
padding: 0 var(--spacing-sm);
box-sizing: border-box;
@@ -352,6 +353,35 @@ body.dark .chart-title-identifier {
color: var(--color-dark-text-secondary);
}
+.chart-title-sep {
+ opacity: 0.5;
+}
+
+.chart-title-gitref {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ background: rgba(255, 145, 3, 0.1);
+ color: #ff9103;
+ font-weight: 600;
+ font-style: normal;
+ text-decoration: none;
+ border: 1px solid rgba(255, 145, 3, 0.25);
+ transition: background 140ms ease, border-color 140ms ease, transform
140ms ease;
+}
+
+.chart-title-gitref:hover {
+ background: rgba(255, 145, 3, 0.18);
+ border-color: rgba(255, 145, 3, 0.5);
+ transform: translateY(-1px);
+}
+
+.chart-title-gitref svg {
+ opacity: 0.75;
+}
+
.single-view {
flex: 1;
min-height: 0;
@@ -562,6 +592,33 @@ body.dark .app-bar {
display: inline-flex;
}
+.app-bar-toast {
+ position: absolute;
+ top: calc(100% + 8px);
+ right: 0;
+ padding: 5px 10px;
+ background: #111827;
+ color: #fff;
+ font-size: 11px;
+ font-weight: 600;
+ border-radius: 6px;
+ white-space: nowrap;
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+ pointer-events: none;
+ animation: app-bar-toast-in 160ms ease;
+ z-index: 40;
+}
+
+body.dark .app-bar-toast {
+ background: #f5f5f5;
+ color: #0b1220;
+}
+
+@keyframes app-bar-toast-in {
+ from { opacity: 0; transform: translateY(-4px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
.app-bar-brand {
display: inline-flex;
align-items: center;
@@ -1098,6 +1155,17 @@ body.dark .segment.active {
font-size: 11px;
}
+.footer-meta a {
+ color: var(--color-text-secondary);
+ text-decoration: none;
+ font-weight: 500;
+ transition: color 150ms;
+}
+
+.footer-meta a:hover {
+ color: #ff9103;
+}
+
.footer-version {
padding: 2px 6px;
background: var(--color-border);
@@ -1652,44 +1720,50 @@ body.dark .sidebar-search-hint {
background: rgba(255, 255, 255, 0.08);
}
-.sidebar-scope {
+.sidebar-facet-row {
display: grid;
grid-template-columns: 1fr 1fr;
- gap: 2px;
- padding: 3px;
- background: var(--color-border);
- border-radius: 8px;
+ gap: 8px;
}
-body.dark .sidebar-scope {
- background: rgba(255, 255, 255, 0.04);
+.sidebar-facet {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ min-width: 0;
}
-.sidebar-scope-btn {
- padding: 7px 10px;
- background: transparent;
- border: none;
- border-radius: 6px;
+.sidebar-facet-label {
+ font-size: 10px;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
color: var(--color-text-secondary);
- font-family: inherit;
- font-size: 12.5px;
- font-weight: 500;
- cursor: pointer;
- transition: all 150ms;
}
-.sidebar-scope-btn:hover {
+.sidebar-facet-select {
+ width: 100%;
+ padding: 6px 8px;
+ font-size: 12px;
+ font-family: inherit;
+ background: var(--color-background);
color: var(--color-text);
+ border: 1px solid var(--color-border);
+ border-radius: 6px;
+ cursor: pointer;
+ transition: border-color 150ms;
}
-.sidebar-scope-btn.active {
- background: var(--color-background);
- color: var(--color-text);
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);
+.sidebar-facet-select:hover,
+.sidebar-facet-select:focus {
+ border-color: #ff9103;
+ outline: none;
}
-body.dark .sidebar-scope-btn.active {
- background: var(--color-dark-background);
+body.dark .sidebar-facet-select {
+ background: var(--color-dark-input);
+ color: var(--color-dark-text);
+ border-color: var(--color-dark-border);
}
.sidebar-kind-chips {
@@ -2220,6 +2294,82 @@ body.dark .hero-v2 {
line-height: 1.5;
}
+.hero-v2-loading {
+ justify-content: center;
+ align-items: center;
+ min-height: 520px;
+}
+
+.hero-v2-loading-inner {
+ position: relative;
+ z-index: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 14px;
+ text-align: center;
+ max-width: 520px;
+ padding: 0 24px;
+}
+
+.hero-v2-loading-mark {
+ width: 112px;
+ height: 112px;
+ object-fit: contain;
+ filter: drop-shadow(0 0 24px rgba(255, 145, 3, 0.35));
+ animation: hero-loading-pulse 1800ms ease-in-out infinite;
+}
+
+.hero-v2-loading-brand {
+ font-family: var(--hero-mono);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.22em;
+ text-transform: uppercase;
+ color: #ff9103;
+ margin-top: 4px;
+}
+
+.hero-v2-loading-sub {
+ font-size: clamp(28px, 4vw, 44px);
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ color: var(--hero-text);
+ line-height: 1;
+}
+
+.hero-v2-loading-slow {
+ margin: 8px 0 0;
+ color: var(--hero-muted);
+ font-size: 13px;
+ line-height: 1.5;
+ animation: hero-fade-in 400ms ease both;
+}
+
+@keyframes hero-loading-pulse {
+ 0%, 100% {
+ opacity: 0.4;
+ transform: scale(0.96);
+ }
+ 50% {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.visually-hidden {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ white-space: nowrap;
+ border: 0;
+}
+
.hero-v2-headline {
display: flex;
flex-direction: column;
@@ -2271,6 +2421,39 @@ body.dark .hero-v2 {
font-family: var(--hero-mono);
font-size: 13px;
letter-spacing: 0.02em;
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0 6px;
+}
+
+.hero-v2-sub-sep {
+ color: var(--hero-muted);
+ opacity: 0.6;
+}
+
+.hero-v2-sub-gitref {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ background: rgba(255, 145, 3, 0.1);
+ color: #ff9103;
+ font-weight: 600;
+ text-decoration: none;
+ border: 1px solid rgba(255, 145, 3, 0.25);
+ transition: background 140ms ease, border-color 140ms ease, transform
140ms ease;
+}
+
+.hero-v2-sub-gitref:hover {
+ background: rgba(255, 145, 3, 0.18);
+ border-color: rgba(255, 145, 3, 0.5);
+ transform: translateY(-1px);
+}
+
+.hero-v2-sub-gitref svg {
+ opacity: 0.75;
}
.hero-v2-tagline {
@@ -2959,6 +3142,17 @@ body.dark .benchmark-meta-chip {
.benchmark-meta-chip.flavor-latency .benchmark-meta-chip-label { color:
#ff9103; opacity: 0.85; }
.benchmark-meta-chip.flavor-throughput .benchmark-meta-chip-label { color:
#38bdf8; opacity: 0.85; }
+.compare-pane .benchmark-meta-row {
+ grid-template-columns: 1fr;
+ gap: 4px;
+ align-items: start;
+}
+
+.compare-pane .benchmark-meta-label {
+ border-right: none;
+ padding-right: 0;
+}
+
@media (max-width: 720px) {
.benchmark-meta-row {
grid-template-columns: 1fr;
diff --git a/core/bench/dashboard/frontend/src/components/app_content.rs
b/core/bench/dashboard/frontend/src/components/app_content.rs
index 5ecb741aa..e3dc1a3c6 100644
--- a/core/bench/dashboard/frontend/src/components/app_content.rs
+++ b/core/bench/dashboard/frontend/src/components/app_content.rs
@@ -218,23 +218,35 @@ pub fn app_content() -> Html {
gitref_ctx.state.selected_gitref.clone(),
);
- let on_gitref_select = {
- let gitref_dispatch = gitref_ctx.dispatch.clone();
- Callback::from(move |gitref: String| {
-
gitref_dispatch.emit(GitrefAction::SetSelectedGitref(Some(gitref)));
- })
- };
-
- let on_hardware_select = {
- let hardware_dispatch = hardware_ctx.dispatch.clone();
+ {
+ let selected_uuid = benchmark_ctx
+ .state
+ .selected_benchmark
+ .as_ref()
+ .map(|benchmark| benchmark.uuid.to_string());
+ let route_uuid = match route.as_ref() {
+ Some(AppRoute::Benchmark { uuid }) => Some(uuid.clone()),
+ _ => None,
+ };
let navigator = navigator.clone();
- Callback::from(move |hardware_id: String| {
-
hardware_dispatch.emit(HardwareAction::SelectHardware(Some(hardware_id)));
- if let Some(nav) = navigator.as_ref() {
- nav.push(&AppRoute::Home);
- }
- })
- };
+ let is_loading_handle = is_loading_from_url.clone();
+ use_effect_with(
+ (selected_uuid, route_uuid),
+ move |(selected_uuid, route_uuid)| {
+ if *is_loading_handle {
+ return;
+ }
+ if let (Some(selected), Some(current)) = (selected_uuid,
route_uuid)
+ && selected != current
+ && let Some(nav) = navigator.as_ref()
+ {
+ nav.push(&AppRoute::Benchmark {
+ uuid: selected.clone(),
+ });
+ }
+ },
+ );
+ }
let show_detail = matches!(
route,
@@ -250,7 +262,7 @@ pub fn app_content() -> Html {
)}>
<TopAppBar show_sidebar_toggle={show_detail}
show_detail_actions={show_detail} />
if show_detail {
- <Sidebar on_gitref_select={on_gitref_select}
on_hardware_select={on_hardware_select} />
+ <Sidebar />
<MainContent
selected_gitref={gitref_ctx.state.selected_gitref.clone().unwrap_or_default()}
/>
} else {
<Hero
selected_gitref={gitref_ctx.state.selected_gitref.clone().unwrap_or_default()}
/>
diff --git a/core/bench/dashboard/frontend/src/components/footer.rs
b/core/bench/dashboard/frontend/src/components/footer.rs
index 8bd5d2d95..58cfd6f0f 100644
--- a/core/bench/dashboard/frontend/src/components/footer.rs
+++ b/core/bench/dashboard/frontend/src/components/footer.rs
@@ -41,7 +41,9 @@ pub fn footer() -> Html {
<div class="footer-meta">
<span
class="footer-version">{"v"}{env!("CARGO_PKG_VERSION")}</span>
<span class="footer-sep">{"•"}</span>
- <span>{"Apache Software Foundation"}</span>
+ <a href="https://www.apache.org/" target="_blank"
rel="noopener noreferrer">
+ {"Apache Software Foundation"}
+ </a>
</div>
</div>
</footer>
diff --git a/core/bench/dashboard/frontend/src/components/layout/hero.rs
b/core/bench/dashboard/frontend/src/components/layout/hero.rs
index a558f73f3..a5905d87a 100644
--- a/core/bench/dashboard/frontend/src/components/layout/hero.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/hero.rs
@@ -17,15 +17,14 @@
use crate::api;
use crate::components::chart::tail_chart::TailChart;
-use crate::format::{format_ms, nan_safe_cmp};
+use crate::format::format_ms;
use crate::router::AppRoute;
-use crate::state::benchmark::{pick_best_from_recent_batch, use_benchmark};
+use crate::state::benchmark::{latest_sweep, pick_best_from_recent_batch,
use_benchmark};
use bench_dashboard_shared::BenchmarkReportLight;
-use bench_report::benchmark_kind::BenchmarkKind;
use chrono::DateTime;
use gloo::console::log;
+use gloo::timers::callback::Timeout;
use std::cell::Cell;
-use std::collections::BTreeMap;
use std::rc::Rc;
use yew::platform::spawn_local;
use yew::prelude::*;
@@ -40,10 +39,14 @@ pub struct HeroProps {
pub fn hero(props: &HeroProps) -> Html {
let benchmark_ctx = use_benchmark();
let navigator = use_navigator();
+ let (is_dark, _) = use_context::<(bool, Callback<()>)>().expect("Theme
context not found");
let recent = use_state(Vec::<BenchmarkReportLight>::new);
+ let is_loading = use_state(|| true);
+ let is_slow = use_state(|| false);
{
let recent = recent.clone();
+ let is_loading = is_loading.clone();
let cancelled = Rc::new(Cell::new(false));
let cancelled_async = cancelled.clone();
use_effect_with((), move |_| {
@@ -52,49 +55,46 @@ pub fn hero(props: &HeroProps) -> Html {
Ok(data) => {
if !cancelled_async.get() {
recent.set(data);
+ is_loading.set(false);
+ }
+ }
+ Err(error) => {
+ log!(format!("Hero: fetch_recent_benchmarks failed:
{}", error));
+ if !cancelled_async.get() {
+ is_loading.set(false);
}
}
- Err(error) => log!(format!("Hero: fetch_recent_benchmarks
failed: {}", error)),
}
});
move || cancelled.set(true)
});
}
- let recent_vec = (*recent).clone();
- let source: Vec<&BenchmarkReportLight> = if recent_vec.is_empty() {
- benchmark_ctx.state.entries.values().flatten().collect()
- } else {
- recent_vec.iter().collect()
- };
- let unrestricted: Vec<&BenchmarkReportLight> = source
- .iter()
- .copied()
- .filter(|benchmark| benchmark.params.rate_limit.is_none())
- .collect();
- let mut stats = compute_stats(unrestricted.iter().copied());
- stats.showcase = unrestricted
- .iter()
- .copied()
- .max_by(|left, right| nan_safe_cmp(throughput_mb(left),
throughput_mb(right)))
- .cloned();
- if stats.showcase.is_none() && !source.is_empty() {
- stats.showcase = pick_best_from_recent_batch(&source);
+ {
+ let is_slow = is_slow.clone();
+ let is_loading_value = *is_loading;
+ use_effect_with(is_loading_value, move |loading| {
+ if !*loading {
+ is_slow.set(false);
+ return Box::new(|| ()) as Box<dyn FnOnce()>;
+ }
+ let timeout = Timeout::new(2_000, move || is_slow.set(true));
+ Box::new(move || drop(timeout)) as Box<dyn FnOnce()>
+ });
+ }
+
+ if *is_loading {
+ return render_hero_loading(is_dark, *is_slow);
}
+ let recent_vec = (*recent).clone();
+ let source: Vec<&BenchmarkReportLight> = recent_vec.iter().collect();
+ let sweep = latest_sweep(&source);
+ let mut stats = compute_stats(sweep.iter().copied());
+ stats.showcase = pick_best_from_recent_batch(&source);
+
if stats.total == 0 {
- return html! {
- <div class="hero-v2 hero-v2-empty">
- { render_background_grid() }
- <div class="hero-v2-empty-inner">
- <div class="hero-v2-eyebrow">{"Apache Iggy"}</div>
- <h1 class="hero-v2-empty-title">{"Benchmarks"}</h1>
- <p class="hero-v2-empty-sub">
- {"Pick a hardware and gitref in the sidebar to view
performance data."}
- </p>
- </div>
- </div>
- };
+ return render_hero_loading(is_dark, true);
}
let hardware = benchmark_ctx
@@ -102,11 +102,12 @@ pub fn hero(props: &HeroProps) -> Html {
.current_hardware
.clone()
.unwrap_or_default();
- let gitref_suffix = if props.selected_gitref.is_empty() {
- String::new()
- } else {
- format!(" @ {}", props.selected_gitref)
- };
+ let sweep_gitref = stats
+ .showcase
+ .as_ref()
+ .and_then(|showcase| showcase.params.gitref.clone())
+ .filter(|gitref| !gitref.is_empty())
+ .or_else(|| Some(props.selected_gitref.clone()).filter(|gitref|
!gitref.is_empty()));
let on_view_details = stats.showcase.as_ref().map(|showcase| {
let uuid = showcase.uuid.to_string();
@@ -119,19 +120,14 @@ pub fn hero(props: &HeroProps) -> Html {
});
let on_browse_click = {
- let latest_uuid =
latest_uuid_from_entries(&benchmark_ctx.state.entries);
let navigator = navigator.clone();
Callback::from(move |_: MouseEvent| {
- if let Some(uuid) = latest_uuid.clone() {
- navigate_to_benchmark(&navigator, uuid);
- } else {
- let navigator = navigator.clone();
- spawn_local(async move {
- if let Some(uuid) = fetch_latest_uuid().await {
- navigate_to_benchmark(&navigator, uuid);
- }
- });
- }
+ let navigator = navigator.clone();
+ spawn_local(async move {
+ if let Some(uuid) = fetch_latest_uuid().await {
+ navigate_to_benchmark(&navigator, uuid);
+ }
+ });
})
};
@@ -139,7 +135,7 @@ pub fn hero(props: &HeroProps) -> Html {
<div class="hero-v2">
{ render_background_grid() }
<div class="hero-v2-inner">
- { render_headline(&stats, &hardware, &gitref_suffix,
&on_browse_click) }
+ { render_headline(&stats, &hardware, sweep_gitref.as_deref(),
&on_browse_click) }
{ render_stat_cards(&stats) }
{
match (stats.showcase.as_ref(), on_view_details) {
@@ -157,16 +153,6 @@ pub fn hero(props: &HeroProps) -> Html {
}
}
-fn latest_uuid_from_entries(
- entries: &BTreeMap<BenchmarkKind, Vec<BenchmarkReportLight>>,
-) -> Option<String> {
- entries
- .values()
- .flatten()
- .max_by(|left, right| left.timestamp.cmp(&right.timestamp))
- .map(|benchmark| benchmark.uuid.to_string())
-}
-
async fn fetch_latest_uuid() -> Option<String> {
match api::fetch_recent_benchmarks(Some(1)).await {
Ok(recent) => recent.into_iter().next().map(|b| b.uuid.to_string()),
@@ -273,7 +259,7 @@ fn render_background_grid() -> Html {
fn render_headline(
stats: &HeroStats,
hardware: &str,
- gitref_suffix: &str,
+ gitref: Option<&str>,
on_browse_click: &Callback<MouseEvent>,
) -> Html {
let (value, unit, subject) = match &stats.peak_mb_s {
@@ -283,11 +269,6 @@ fn render_headline(
}
None => ("-".to_string(), "MB/s", String::new()),
};
- let sub = if subject.is_empty() {
- format!("{hardware}{gitref_suffix}")
- } else {
- format!("{subject} · {hardware}{gitref_suffix}")
- };
html! {
<div class="hero-v2-headline">
@@ -296,7 +277,9 @@ fn render_headline(
<span class="hero-v2-big">{value}</span>
<span class="hero-v2-unit">{unit}</span>
</h1>
- <p class="hero-v2-sub">{sub}</p>
+ <p class="hero-v2-sub">
+ { render_hero_sub(&subject, hardware, gitref) }
+ </p>
<p class="hero-v2-tagline">
{"Modern hardware is incredibly capable. "}
<span class="hero-v2-tagline-accent">{"Apache Iggy was built
for it."}</span>
@@ -319,6 +302,50 @@ fn render_headline(
}
}
+fn render_hero_sub(subject: &str, hardware: &str, gitref: Option<&str>) ->
Html {
+ let prefix = match (subject.is_empty(), hardware.is_empty()) {
+ (true, true) => String::new(),
+ (true, false) => hardware.to_string(),
+ (false, true) => subject.to_string(),
+ (false, false) => format!("{subject} · {hardware}"),
+ };
+ let has_prefix = !prefix.is_empty();
+ let gitref = gitref.map(str::to_string);
+ let gitref_owned = gitref.clone();
+
+ html! {
+ <>
+ if has_prefix {
+ <span>{prefix}</span>
+ }
+ if let Some(gitref) = gitref_owned {
+ if has_prefix {
+ <span class="hero-v2-sub-sep">{" @ "}</span>
+ }
+ <a
+ class="hero-v2-sub-gitref"
+ href={iggy_gitref_url(&gitref)}
+ target="_blank"
+ rel="noopener noreferrer"
+ title={format!("Browse apache/iggy at {gitref}")}
+ >
+ {gitref}
+ <svg xmlns="http://www.w3.org/2000/svg" width="11"
height="11" viewBox="0 0 24 24"
+ fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2
0 0 1 2-2h6" />
+ <polyline points="15 3 21 3 21 9" />
+ <line x1="10" y1="14" x2="21" y2="3" />
+ </svg>
+ </a>
+ }
+ </>
+ }
+}
+
+fn iggy_gitref_url(gitref: &str) -> String {
+ format!("https://github.com/apache/iggy/tree/{gitref}")
+}
+
fn render_stat_cards(stats: &HeroStats) -> Html {
html! {
<div class="hero-v2-cards">
@@ -404,14 +431,6 @@ fn render_showcase_card(stagger: usize, showcase:
Option<&BenchmarkReportLight>)
}
}
-fn throughput_mb(benchmark: &BenchmarkReportLight) -> f64 {
- benchmark
- .group_metrics
- .first()
- .map(|metrics| metrics.summary.total_throughput_megabytes_per_second)
- .unwrap_or(0.0)
-}
-
fn render_summary_card(stagger: usize, total: usize, latest_ts: Option<&str>)
-> Html {
let sub = match latest_ts {
Some(ts) => format!("Latest: {}", format_date(ts)),
@@ -467,3 +486,32 @@ fn format_date(timestamp_str: &str) -> String {
Err(_) => "unknown".to_string(),
}
}
+
+fn render_hero_loading(is_dark: bool, is_slow: bool) -> Html {
+ let logo_src = if is_dark {
+ "/assets/iggy-light.png"
+ } else {
+ "/assets/iggy-dark.png"
+ };
+ html! {
+ <div class="hero-v2 hero-v2-loading" aria-busy="true"
aria-live="polite">
+ { render_background_grid() }
+ <div class="hero-v2-loading-inner">
+ <img
+ class="hero-v2-loading-mark"
+ src={logo_src}
+ alt=""
+ aria-hidden="true"
+ />
+ <div class="hero-v2-loading-brand">{"Apache Iggy"}</div>
+ <div class="hero-v2-loading-sub">{"Benchmarks"}</div>
+ if is_slow {
+ <p class="hero-v2-loading-slow">
+ {"Fetching the latest benchmark run. This can take a
moment on a cold cache."}
+ </p>
+ }
+ <span class="visually-hidden">{"Loading benchmarks"}</span>
+ </div>
+ </div>
+ }
+}
diff --git
a/core/bench/dashboard/frontend/src/components/layout/main_content.rs
b/core/bench/dashboard/frontend/src/components/layout/main_content.rs
index 766b53e9f..6b9a796d8 100644
--- a/core/bench/dashboard/frontend/src/components/layout/main_content.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/main_content.rs
@@ -22,7 +22,7 @@ use crate::components::layout::sweep_view::SweepView;
use crate::components::selectors::measurement_type_selector::MeasurementType;
use crate::router::AppRoute;
use crate::state::benchmark::use_benchmark;
-use crate::state::ui::{UiAction, ViewMode, use_ui};
+use crate::state::ui::{UiAction, use_ui};
use bench_dashboard_shared::BenchmarkReportLight;
use bench_report::benchmark_kind::BenchmarkKind;
use std::collections::BTreeMap;
@@ -40,7 +40,6 @@ pub fn main_content(props: &MainContentProps) -> Html {
let _ = &props.selected_gitref;
let benchmark_ctx = use_benchmark();
let ui = use_ui();
- let is_recent_view = matches!(ui.view_mode, ViewMode::RecentBenchmarks);
let selected = benchmark_ctx.state.selected_benchmark.clone();
let pinned = ui.compare_pin.clone();
let entries = benchmark_ctx.state.entries.clone();
@@ -101,7 +100,6 @@ pub fn main_content(props: &MainContentProps) -> Html {
is_dark,
&entries,
),
- (None, _) if is_recent_view => render_empty_recent(),
(None, _) => render_loading(),
};
@@ -121,7 +119,7 @@ fn render_single(
{ benchmark.title(&measurement.to_string()) }
</div>
<div class="chart-title-identifier">
- { benchmark.identifier_with_cpu_and_version() }
+ { render_benchmark_identifier(benchmark) }
</div>
</div>
<BenchmarkMeta benchmark={benchmark.clone()} />
@@ -215,7 +213,7 @@ fn render_compare_pane(
{ benchmark.title(&measurement.to_string()) }
</div>
<div class="chart-title-identifier">
- { benchmark.identifier_with_cpu_and_version() }
+ { render_benchmark_identifier(benchmark) }
</div>
</div>
<BenchmarkMeta benchmark={benchmark.clone()} />
@@ -226,27 +224,52 @@ fn render_compare_pane(
}
}
-fn render_empty_recent() -> Html {
+fn render_loading() -> Html {
html! {
<div class="content-wrapper">
<div class="empty-state">
<div class="empty-state-content">
- <h2>{"Select a recent benchmark"}</h2>
- <p>{"Choose a benchmark from the sidebar to display
performance data."}</p>
+ <h2>{"Loading benchmark..."}</h2>
</div>
</div>
</div>
}
}
-fn render_loading() -> Html {
+fn render_benchmark_identifier(benchmark: &BenchmarkReportLight) -> Html {
+ let hardware = benchmark
+ .hardware
+ .identifier
+ .as_deref()
+ .unwrap_or("identifier");
+ let cpu = benchmark.hardware.cpu_name.as_str();
+ let gitref = benchmark
+ .params
+ .gitref
+ .as_deref()
+ .filter(|gitref| !gitref.is_empty());
+
html! {
- <div class="content-wrapper">
- <div class="empty-state">
- <div class="empty-state-content">
- <h2>{"Loading benchmark..."}</h2>
- </div>
- </div>
- </div>
+ <>
+ <span>{format!("{hardware} @ {cpu}")}</span>
+ if let Some(gitref) = gitref {
+ <span class="chart-title-sep">{"·"}</span>
+ <a
+ class="chart-title-gitref"
+
href={format!("https://github.com/apache/iggy/tree/{gitref}")}
+ target="_blank"
+ rel="noopener noreferrer"
+ title={format!("Browse apache/iggy at {gitref}")}
+ >
+ {gitref}
+ <svg xmlns="http://www.w3.org/2000/svg" width="11"
height="11" viewBox="0 0 24 24"
+ fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
+ <path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2
0 0 1 2-2h6" />
+ <polyline points="15 3 21 3 21 9" />
+ <line x1="10" y1="14" x2="21" y2="3" />
+ </svg>
+ </a>
+ }
+ </>
}
}
diff --git a/core/bench/dashboard/frontend/src/components/layout/sidebar.rs
b/core/bench/dashboard/frontend/src/components/layout/sidebar.rs
index c71dd7457..72aba0a59 100644
--- a/core/bench/dashboard/frontend/src/components/layout/sidebar.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/sidebar.rs
@@ -15,29 +15,79 @@
// specific language governing permissions and limitations
// under the License.
-use crate::components::selectors::benchmark_selector::BenchmarkSelector;
-use crate::components::selectors::gitref_selector::GitrefSelector;
-use crate::components::selectors::hardware_selector::HardwareSelector;
+use crate::api;
+use crate::components::selectors::benchmarks_list::BenchmarksList;
use crate::components::selectors::param_filters_panel::ParamFiltersPanel;
-use
crate::components::selectors::recent_benchmarks_selector::RecentBenchmarksSelector;
-use crate::state::gitref::use_gitref;
-use crate::state::ui::{KindGroup, SidebarSort, UiAction, ViewMode, use_ui};
+use crate::router::AppRoute;
+use crate::state::benchmark::{BenchmarkAction, recency_cmp, use_benchmark};
+use crate::state::ui::{KindGroup, SidebarSort, UiAction, use_ui};
+use bench_dashboard_shared::BenchmarkReportLight;
+use gloo::console::log;
+use std::cell::Cell;
+use std::collections::BTreeSet;
+use std::rc::Rc;
use web_sys::HtmlInputElement;
+use web_sys::HtmlSelectElement;
+use yew::platform::spawn_local;
use yew::prelude::*;
+use yew_router::prelude::{use_navigator, use_route};
+
+const RECENT_LIMIT: u32 = 10_000;
#[derive(Properties, PartialEq)]
-pub struct SidebarProps {
- pub on_gitref_select: Callback<String>,
- pub on_hardware_select: Callback<String>,
-}
+pub struct SidebarProps;
#[function_component(Sidebar)]
-pub fn sidebar(props: &SidebarProps) -> Html {
- let gitref_ctx = use_gitref();
+pub fn sidebar(_props: &SidebarProps) -> Html {
let ui = use_ui();
- let is_recent_view = matches!(ui.view_mode, ViewMode::RecentBenchmarks);
- let active_kind_filter = ui.sidebar_kind_filter.clone();
- let current_sort = ui.sidebar_sort;
+ let benchmark_ctx = use_benchmark();
+ let navigator = use_navigator();
+ let route = use_route::<AppRoute>();
+
+ let benchmarks = use_state(Vec::<BenchmarkReportLight>::new);
+ let is_loading = use_state(|| true);
+
+ {
+ let benchmarks_handle = benchmarks.clone();
+ let is_loading_handle = is_loading.clone();
+ let dispatch = benchmark_ctx.dispatch.clone();
+ let navigator = navigator.clone();
+ let url_has_benchmark = matches!(
+ route,
+ Some(AppRoute::Benchmark { .. }) | Some(AppRoute::Compare { .. })
+ );
+ let cancelled = Rc::new(Cell::new(false));
+ let cancelled_async = cancelled.clone();
+ use_effect_with((), move |_| {
+ spawn_local(async move {
+ match api::fetch_recent_benchmarks(Some(RECENT_LIMIT)).await {
+ Ok(mut data) => {
+ data.sort_by(|left, right| {
+ recency_cmp(right, left).then_with(||
right.uuid.cmp(&left.uuid))
+ });
+ if cancelled_async.get() {
+ return;
+ }
+ if !url_has_benchmark && let Some(newest) =
data.first().cloned() {
+ if let Some(nav) = navigator.as_ref() {
+ nav.push(&AppRoute::Benchmark {
+ uuid: newest.uuid.to_string(),
+ });
+ }
+
dispatch.emit(BenchmarkAction::SelectBenchmark(Box::new(Some(newest))));
+ }
+ benchmarks_handle.set(data);
+ }
+ Err(error) => log!(format!("Sidebar:
fetch_recent_benchmarks failed: {error}")),
+ }
+ if !cancelled_async.get() {
+ is_loading_handle.set(false);
+ }
+ });
+ move || cancelled.set(true)
+ });
+ }
+
let current_search = ui.sidebar_search.clone();
let on_search = {
@@ -55,11 +105,6 @@ pub fn sidebar(props: &SidebarProps) -> Html {
})
};
- let on_scope_change = |mode: ViewMode| {
- let ui = ui.clone();
- Callback::from(move |_: MouseEvent|
ui.dispatch(UiAction::SetViewMode(mode.clone())))
- };
-
let on_kind_toggle = {
let ui = ui.clone();
Callback::from(move |group: KindGroup|
ui.dispatch(UiAction::ToggleKindFilter(group)))
@@ -81,6 +126,33 @@ pub fn sidebar(props: &SidebarProps) -> Html {
})
};
+ let on_hardware_change = {
+ let ui = ui.clone();
+ Callback::from(move |event: Event| {
+ let input: HtmlSelectElement = event.target_unchecked_into();
+ let value = input.value();
+ let next = if value.is_empty() { None } else { Some(value) };
+ ui.dispatch(UiAction::SetHardwareFilter(next));
+ })
+ };
+
+ let on_gitref_change = {
+ let ui = ui.clone();
+ Callback::from(move |event: Event| {
+ let input: HtmlSelectElement = event.target_unchecked_into();
+ let value = input.value();
+ let next = if value.is_empty() { None } else { Some(value) };
+ ui.dispatch(UiAction::SetGitrefFilter(next));
+ })
+ };
+
+ let hardware_options = collect_hardware(&benchmarks);
+ let gitref_options = collect_gitrefs(&benchmarks,
ui.hardware_filter.as_deref());
+ let active_kind_filter = ui.sidebar_kind_filter.clone();
+ let current_sort = ui.sidebar_sort;
+ let current_hardware = ui.hardware_filter.clone().unwrap_or_default();
+ let current_gitref = ui.gitref_filter.clone().unwrap_or_default();
+
html! {
<aside class="sidebar">
<div class="sidebar-fixed-header">
@@ -94,7 +166,7 @@ pub fn sidebar(props: &SidebarProps) -> Html {
<input
type="search"
class="sidebar-search-input"
- placeholder="Search benchmarks, gitrefs, hardware..."
+ placeholder="Search benchmarks..."
value={current_search.clone()}
oninput={on_search}
/>
@@ -110,35 +182,41 @@ pub fn sidebar(props: &SidebarProps) -> Html {
}
</div>
- <div class="sidebar-scope" role="tablist">
- <button
- type="button"
- role="tab"
- aria-selected={(!is_recent_view).to_string()}
- class={classes!("sidebar-scope-btn",
(!is_recent_view).then_some("active"))}
- onclick={on_scope_change(ViewMode::SingleGitref)}
- >
- {"Version"}
- </button>
- <button
- type="button"
- role="tab"
- aria-selected={is_recent_view.to_string()}
- class={classes!("sidebar-scope-btn",
is_recent_view.then_some("active"))}
- onclick={on_scope_change(ViewMode::RecentBenchmarks)}
- >
- {"Recent"}
- </button>
- </div>
+ <div class="sidebar-facet-row">
+ <label class="sidebar-facet">
+ <span class="sidebar-facet-label">{"Hardware"}</span>
+ <select class="sidebar-facet-select"
onchange={on_hardware_change}>
+ <option value=""
selected={current_hardware.is_empty()}>
+ {"All"}
+ </option>
+ { for hardware_options.iter().map(|option| html! {
+ <option
+ value={option.clone()}
+ selected={option.as_str() ==
current_hardware}
+ >
+ {option.clone()}
+ </option>
+ })}
+ </select>
+ </label>
- if !is_recent_view {
- <HardwareSelector
on_hardware_select={props.on_hardware_select.clone()} />
- <GitrefSelector
- gitrefs={gitref_ctx.state.gitrefs.clone()}
-
selected_gitref={gitref_ctx.state.selected_gitref.clone().unwrap_or_default()}
- on_gitref_select={props.on_gitref_select.clone()}
- />
- }
+ <label class="sidebar-facet">
+ <span class="sidebar-facet-label">{"Version"}</span>
+ <select class="sidebar-facet-select"
onchange={on_gitref_change}>
+ <option value=""
selected={current_gitref.is_empty()}>
+ {"All"}
+ </option>
+ { for gitref_options.iter().map(|option| html! {
+ <option
+ value={option.clone()}
+ selected={option.as_str() ==
current_gitref}
+ >
+ {option.clone()}
+ </option>
+ })}
+ </select>
+ </label>
+ </div>
<div class="sidebar-kind-chips">
{ for KindGroup::all().iter().map(|group| {
@@ -191,11 +269,10 @@ pub fn sidebar(props: &SidebarProps) -> Html {
</div>
<div class="sidebar-scrollable-content">
- if is_recent_view {
- <RecentBenchmarksSelector limit={10000} />
- } else {
- <BenchmarkSelector />
- }
+ <BenchmarksList
+ benchmarks={(*benchmarks).clone()}
+ is_loading={*is_loading}
+ />
</div>
</aside>
}
@@ -247,3 +324,51 @@ fn render_compare_hint(ui:
&yew::UseReducerHandle<crate::state::ui::UiState>) ->
fn short_name(full: &str) -> String {
full.split('(').next().unwrap_or(full).trim().to_string()
}
+
+fn collect_hardware(benchmarks: &[BenchmarkReportLight]) -> Vec<String> {
+ let mut set: BTreeSet<String> = BTreeSet::new();
+ for benchmark in benchmarks {
+ if let Some(id) = benchmark.hardware.identifier.as_deref()
+ && !id.is_empty()
+ {
+ set.insert(id.to_string());
+ }
+ }
+ set.into_iter().collect()
+}
+
+fn collect_gitrefs(
+ benchmarks: &[BenchmarkReportLight],
+ hardware_filter: Option<&str>,
+) -> Vec<String> {
+ let mut newest: std::collections::HashMap<String, &BenchmarkReportLight> =
+ std::collections::HashMap::new();
+ for benchmark in benchmarks {
+ if let Some(expected) = hardware_filter
+ && benchmark.hardware.identifier.as_deref() != Some(expected)
+ {
+ continue;
+ }
+ let Some(gitref) = benchmark.params.gitref.as_deref() else {
+ continue;
+ };
+ if gitref.is_empty() {
+ continue;
+ }
+ newest
+ .entry(gitref.to_string())
+ .and_modify(|existing| {
+ if recency_cmp(benchmark, existing).is_gt() {
+ *existing = benchmark;
+ }
+ })
+ .or_insert(benchmark);
+ }
+
+ let mut ordered: Vec<(&BenchmarkReportLight, String)> = newest
+ .into_iter()
+ .map(|(gitref, benchmark)| (benchmark, gitref))
+ .collect();
+ ordered.sort_by(|left, right| recency_cmp(right.0, left.0));
+ ordered.into_iter().map(|(_, gitref)| gitref).collect()
+}
diff --git a/core/bench/dashboard/frontend/src/components/layout/top_app_bar.rs
b/core/bench/dashboard/frontend/src/components/layout/top_app_bar.rs
index f5893097f..6b1f28e1e 100644
--- a/core/bench/dashboard/frontend/src/components/layout/top_app_bar.rs
+++ b/core/bench/dashboard/frontend/src/components/layout/top_app_bar.rs
@@ -25,10 +25,8 @@ use
crate::components::tooltips::server_stats_tooltip::ServerStatsTooltip;
use crate::router::AppRoute;
use crate::state::benchmark::{BenchmarkAction, use_benchmark};
use crate::state::ui::{TopBarPopup, UiAction, use_ui};
-use bench_dashboard_shared::BenchmarkReportLight;
-use bench_report::benchmark_kind::BenchmarkKind;
use gloo::console::log;
-use std::collections::BTreeMap;
+use gloo::timers::callback::Timeout;
use yew::platform::spawn_local;
use yew::prelude::*;
use yew_router::prelude::{Navigator, use_navigator};
@@ -115,6 +113,26 @@ pub fn top_app_bar(props: &TopAppBarProps) -> Html {
})
};
+ let share_copied = use_state(|| false);
+ let on_share = {
+ let share_copied = share_copied.clone();
+ Callback::from(move |_| {
+ let Some(window) = web_sys::window() else {
+ return;
+ };
+ let url = window.location().href().unwrap_or_else(|_|
String::new());
+ if url.is_empty() {
+ return;
+ }
+ let clipboard = window.navigator().clipboard();
+ let _ = clipboard.write_text(&url);
+ share_copied.set(true);
+ let share_copied_for_timer = share_copied.clone();
+ let timeout = Timeout::new(1_400, move ||
share_copied_for_timer.set(false));
+ timeout.forget();
+ })
+ };
+
let on_embed_toggle = {
let ui = ui.clone();
Callback::from(move |_|
ui.dispatch(UiAction::TogglePopup(TopBarPopup::Embed)))
@@ -132,18 +150,13 @@ pub fn top_app_bar(props: &TopAppBarProps) -> Html {
let on_browse_click = {
let navigator = navigator.clone();
- let entries = benchmark_ctx.state.entries.clone();
Callback::from(move |_| {
- if let Some(uuid) = latest_uuid_from_entries(&entries) {
- navigate_to_benchmark(&navigator, uuid);
- } else {
- let navigator = navigator.clone();
- spawn_local(async move {
- if let Some(uuid) = fetch_latest_uuid().await {
- navigate_to_benchmark(&navigator, uuid);
- }
- });
- }
+ let navigator = navigator.clone();
+ spawn_local(async move {
+ if let Some(uuid) = fetch_latest_uuid().await {
+ navigate_to_benchmark(&navigator, uuid);
+ }
+ });
})
};
@@ -212,6 +225,20 @@ pub fn top_app_bar(props: &TopAppBarProps) -> Html {
</button>
}
if props.show_detail_actions && selected_benchmark.is_some() {
+ <div class="app-bar-icon-wrap">
+ <button
+ type="button"
+ class={classes!("app-bar-icon-btn",
(*share_copied).then_some("active"))}
+ onclick={on_share}
+ title={if *share_copied { "Link copied" } else {
"Copy share link" }}
+ aria-label="Copy share link"
+ >
+ { render_share_icon(*share_copied) }
+ </button>
+ if *share_copied {
+ <span class="app-bar-toast" role="status">{"Link
copied"}</span>
+ }
+ </div>
<button
type="button"
class="app-bar-icon-btn"
@@ -252,7 +279,6 @@ pub fn top_app_bar(props: &TopAppBarProps) -> Html {
<ServerStatsTooltip
benchmark_report={selected_benchmark.clone()}
visible={true}
- view_mode={ui.view_mode.clone()}
/>
}
</div>
@@ -267,7 +293,6 @@ pub fn top_app_bar(props: &TopAppBarProps) -> Html {
<BenchmarkInfoTooltip
benchmark_report={benchmark.clone()}
visible={true}
- view_mode={ui.view_mode.clone()}
/>
}
</div>
@@ -354,6 +379,27 @@ fn render_download_icon() -> Html {
}
}
+fn render_share_icon(copied: bool) -> Html {
+ if copied {
+ return html! {
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18"
viewBox="0 0 24 24"
+ fill="none" stroke="currentColor" stroke-width="2.3"
stroke-linecap="round" stroke-linejoin="round">
+ <polyline points="20 6 9 17 4 12" />
+ </svg>
+ };
+ }
+ html! {
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18"
viewBox="0 0 24 24"
+ fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
+ <circle cx="18" cy="5" r="3" />
+ <circle cx="6" cy="12" r="3" />
+ <circle cx="18" cy="19" r="3" />
+ <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
+ <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
+ </svg>
+ }
+}
+
fn render_embed_icon() -> Html {
html! {
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18"
viewBox="0 0 24 24"
@@ -364,16 +410,6 @@ fn render_embed_icon() -> Html {
}
}
-fn latest_uuid_from_entries(
- entries: &BTreeMap<BenchmarkKind, Vec<BenchmarkReportLight>>,
-) -> Option<String> {
- entries
- .values()
- .flatten()
- .max_by(|left, right| left.timestamp.cmp(&right.timestamp))
- .map(|benchmark| benchmark.uuid.to_string())
-}
-
async fn fetch_latest_uuid() -> Option<String> {
match api::fetch_recent_benchmarks(Some(1)).await {
Ok(recent) => recent.into_iter().next().map(|b| b.uuid.to_string()),
diff --git
a/core/bench/dashboard/frontend/src/components/selectors/benchmark_selector.rs
b/core/bench/dashboard/frontend/src/components/selectors/benchmarks_list.rs
similarity index 73%
rename from
core/bench/dashboard/frontend/src/components/selectors/benchmark_selector.rs
rename to
core/bench/dashboard/frontend/src/components/selectors/benchmarks_list.rs
index 54f15d4ee..84a9c625f 100644
---
a/core/bench/dashboard/frontend/src/components/selectors/benchmark_selector.rs
+++ b/core/bench/dashboard/frontend/src/components/selectors/benchmarks_list.rs
@@ -18,21 +18,22 @@
use crate::components::selectors::dense_benchmark_row::DenseBenchmarkRow;
use crate::format::nan_safe_cmp;
use crate::router::AppRoute;
-use crate::state::benchmark::{BenchmarkAction, use_benchmark};
+use crate::state::benchmark::{BenchmarkAction, recency_cmp, use_benchmark};
use crate::state::ui::{KindGroup, SidebarSort, UiAction, use_ui};
use bench_dashboard_shared::BenchmarkReportLight;
use bench_report::benchmark_kind::BenchmarkKind;
-use chrono::DateTime;
-use std::cmp::Ordering;
-use std::collections::{BTreeMap, HashSet};
+use std::collections::HashSet;
use yew::prelude::*;
-use yew_router::prelude::use_navigator;
+use yew_router::prelude::{Navigator, use_navigator};
-#[derive(Properties, PartialEq, Default)]
-pub struct BenchmarkSelectorProps;
+#[derive(Properties, PartialEq)]
+pub struct BenchmarksListProps {
+ pub benchmarks: Vec<BenchmarkReportLight>,
+ pub is_loading: bool,
+}
-#[function_component(BenchmarkSelector)]
-pub fn benchmark_selector(_props: &BenchmarkSelectorProps) -> Html {
+#[function_component(BenchmarksList)]
+pub fn benchmarks_list(props: &BenchmarksListProps) -> Html {
let benchmark_ctx = use_benchmark();
let ui_state = use_ui();
let navigator = use_navigator();
@@ -44,24 +45,13 @@ pub fn benchmark_selector(_props: &BenchmarkSelectorProps)
-> Html {
.as_ref()
.map(|selected| selected.uuid);
- let filters = ui_state.param_filters.clone();
+ let param_filters = ui_state.param_filters.clone();
let search = ui_state.sidebar_search.to_lowercase();
let kind_filter = ui_state.sidebar_kind_filter.clone();
+ let hardware_filter = ui_state.hardware_filter.clone();
+ let gitref_filter = ui_state.gitref_filter.clone();
let sort = ui_state.sidebar_sort;
- let visible: Vec<BenchmarkReportLight> = benchmark_ctx
- .state
- .entries
- .values()
- .flatten()
- .filter(|benchmark| filters.matches(benchmark))
- .filter(|benchmark| kind_filter_matches(&kind_filter,
benchmark.params.benchmark_kind))
- .filter(|benchmark| search_matches(&search, benchmark))
- .cloned()
- .collect();
- let hidden_by_filter = total_benchmarks(&benchmark_ctx.state.entries) -
visible.len();
- let sorted = sort_benchmarks(visible, sort);
-
let on_select = {
let dispatch = benchmark_ctx.dispatch.clone();
let navigator = navigator.clone();
@@ -95,7 +85,7 @@ pub fn benchmark_selector(_props: &BenchmarkSelectorProps) ->
Html {
&& pinned == &clicked_uuid
&& let Some(selected) = selected_uuid.as_ref()
{
- navigate(
+ push_route(
&navigator,
AppRoute::Benchmark {
uuid: selected.clone(),
@@ -107,7 +97,7 @@ pub fn benchmark_selector(_props: &BenchmarkSelectorProps)
-> Html {
if let Some(selected) = selected_uuid
&& selected != clicked_uuid
{
- navigate(
+ push_route(
&navigator,
AppRoute::Compare {
left: selected,
@@ -124,18 +114,31 @@ pub fn benchmark_selector(_props:
&BenchmarkSelectorProps) -> Html {
})
};
- if sorted.is_empty() {
+ if props.is_loading {
+ return render_skeleton();
+ }
+
+ let visible: Vec<BenchmarkReportLight> = props
+ .benchmarks
+ .iter()
+ .filter(|benchmark| param_filters.matches(benchmark))
+ .filter(|benchmark| kind_filter_matches(&kind_filter,
benchmark.params.benchmark_kind))
+ .filter(|benchmark| hardware_matches(hardware_filter.as_deref(),
benchmark))
+ .filter(|benchmark| gitref_matches(gitref_filter.as_deref(),
benchmark))
+ .filter(|benchmark| search_matches(&search, benchmark))
+ .cloned()
+ .collect();
+
+ if visible.is_empty() {
return html! {
<div class="dense-list-empty">
- if hidden_by_filter > 0 {
- <p>{format!("{hidden_by_filter} benchmark(s) hidden.
Adjust filters or search.")}</p>
- } else {
- <p>{"No benchmarks for this gitref yet."}</p>
- }
+ <p>{"No benchmarks match the current filters."}</p>
</div>
};
}
+ let sorted = sort_benchmarks(visible, sort);
+
html! {
<div class="dense-list">
{ for sorted.iter().map(|benchmark| html! {
@@ -145,19 +148,35 @@ pub fn benchmark_selector(_props:
&BenchmarkSelectorProps) -> Html {
pinned_uuid={pinned_uuid}
on_select={on_select.clone()}
on_toggle_pin={on_toggle_pin.clone()}
- show_timestamp={false}
+ show_timestamp={true}
/>
})}
</div>
}
}
-fn navigate(navigator: &Option<yew_router::prelude::Navigator>, route:
AppRoute) {
+fn push_route(navigator: &Option<Navigator>, route: AppRoute) {
if let Some(nav) = navigator.as_ref() {
nav.push(&route);
}
}
+fn render_skeleton() -> Html {
+ html! {
+ <div class="dense-list">
+ { for (0..5).map(|index| html! {
+ <div class="dense-row-skeleton" style={format!("--stagger:
{index}")}>
+ <span class="skeleton-dot" />
+ <div class="skeleton-body">
+ <span class="skeleton-line title" />
+ <span class="skeleton-line meta" />
+ </div>
+ </div>
+ })}
+ </div>
+ }
+}
+
fn kind_filter_matches(filter: &HashSet<KindGroup>, kind: BenchmarkKind) ->
bool {
if filter.is_empty() {
return true;
@@ -165,6 +184,20 @@ fn kind_filter_matches(filter: &HashSet<KindGroup>, kind:
BenchmarkKind) -> bool
filter.iter().any(|group| group.matches(kind))
}
+fn hardware_matches(filter: Option<&str>, benchmark: &BenchmarkReportLight) ->
bool {
+ match filter {
+ Some(expected) => benchmark.hardware.identifier.as_deref() ==
Some(expected),
+ None => true,
+ }
+}
+
+fn gitref_matches(filter: Option<&str>, benchmark: &BenchmarkReportLight) ->
bool {
+ match filter {
+ Some(expected) => benchmark.params.gitref.as_deref() == Some(expected),
+ None => true,
+ }
+}
+
fn search_matches(query: &str, benchmark: &BenchmarkReportLight) -> bool {
if query.is_empty() {
return true;
@@ -191,19 +224,18 @@ fn search_matches(query: &str, benchmark:
&BenchmarkReportLight) -> bool {
{
return true;
}
+ if benchmark.timestamp.to_lowercase().contains(query) {
+ return true;
+ }
false
}
-fn total_benchmarks(entries: &BTreeMap<BenchmarkKind,
Vec<BenchmarkReportLight>>) -> usize {
- entries.values().map(|values| values.len()).sum()
-}
-
fn sort_benchmarks(
mut benchmarks: Vec<BenchmarkReportLight>,
sort: SidebarSort,
) -> Vec<BenchmarkReportLight> {
benchmarks.sort_by(|left, right| match sort {
- SidebarSort::MostRecent => compare_timestamps(&right.timestamp,
&left.timestamp),
+ SidebarSort::MostRecent => recency_cmp(right, left),
SidebarSort::PeakThroughput => nan_safe_cmp(throughput(right),
throughput(left)),
SidebarSort::LowestP99 => nan_safe_cmp(p99(left), p99(right)),
SidebarSort::Name =>
left.params.pretty_name.cmp(&right.params.pretty_name),
@@ -211,16 +243,6 @@ fn sort_benchmarks(
benchmarks
}
-fn compare_timestamps(left: &str, right: &str) -> Ordering {
- match (
- DateTime::parse_from_rfc3339(left),
- DateTime::parse_from_rfc3339(right),
- ) {
- (Ok(left_time), Ok(right_time)) => left_time.cmp(&right_time),
- _ => Ordering::Equal,
- }
-}
-
fn throughput(benchmark: &BenchmarkReportLight) -> f64 {
benchmark
.group_metrics
diff --git
a/core/bench/dashboard/frontend/src/components/selectors/gitref_selector.rs
b/core/bench/dashboard/frontend/src/components/selectors/gitref_selector.rs
deleted file mode 100644
index 440f2e5b3..000000000
--- a/core/bench/dashboard/frontend/src/components/selectors/gitref_selector.rs
+++ /dev/null
@@ -1,63 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use wasm_bindgen::JsCast;
-use web_sys::HtmlSelectElement;
-use yew::prelude::*;
-
-#[derive(Properties, PartialEq)]
-pub struct GitrefSelectorProps {
- pub gitrefs: Vec<String>,
- pub selected_gitref: String,
- pub on_gitref_select: Callback<String>,
-}
-
-#[function_component(GitrefSelector)]
-pub fn gitref_selector(props: &GitrefSelectorProps) -> Html {
- let onchange = {
- let on_gitref_select = props.on_gitref_select.clone();
- Callback::from(move |e: Event| {
- if let Some(select) = e
- .target()
- .and_then(|t| t.dyn_into::<HtmlSelectElement>().ok())
- {
- let gitref = select.value();
- on_gitref_select.emit(gitref);
- }
- })
- };
-
- html! {
- <div class="gitref-select">
- <h3>{"Version"}</h3>
- <select {onchange} value={props.selected_gitref.clone()}>
- {
- props.gitrefs.iter().map(|gitref| {
- html! {
- <option
- value={gitref.clone()}
- selected={gitref == &props.selected_gitref}
- >
- {gitref}
- </option>
- }
- }).collect::<Html>()
- }
- </select>
- </div>
- }
-}
diff --git
a/core/bench/dashboard/frontend/src/components/selectors/hardware_selector.rs
b/core/bench/dashboard/frontend/src/components/selectors/hardware_selector.rs
deleted file mode 100644
index fc2a9eb4a..000000000
---
a/core/bench/dashboard/frontend/src/components/selectors/hardware_selector.rs
+++ /dev/null
@@ -1,61 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::state::hardware::use_hardware;
-use gloo::console::log;
-use yew::prelude::*;
-
-#[derive(Properties, PartialEq)]
-pub struct HardwareSelectorProps {
- pub on_hardware_select: Callback<String>,
-}
-
-#[function_component(HardwareSelector)]
-pub fn hardware_selector(props: &HardwareSelectorProps) -> Html {
- let hardware_ctx = use_hardware();
-
- let on_hardware_change =
create_on_hardware_change_callback(props.on_hardware_select.clone());
-
- html! {
- <div class="hardware-selector">
- <h3>{"Hardware"}</h3>
- <select
- onchange={on_hardware_change.clone()}
- >
- { for hardware_ctx.state.hardware_list.iter().map(|hardware|
html! {
- <option
- value={hardware.identifier.clone().unwrap_or_else(||
"Unknown".to_string())}
- selected={hardware_ctx.state.selected_hardware ==
Some(hardware.identifier.clone().unwrap_or_else(|| "Unknown".to_string()))}
- >
- {format!("{} @ {}",
hardware.identifier.clone().unwrap_or_else(|| "Unknown".to_string()),
&hardware.cpu_name)}
- </option>
- }) }
- </select>
- </div>
- }
-}
-
-fn create_on_hardware_change_callback(on_hardware_select: Callback<String>) ->
Callback<Event> {
- Callback::from(move |e: Event| {
- let target = e.target_dyn_into::<web_sys::HtmlSelectElement>();
- if let Some(select) = target {
- let value = select.value();
- log!(format!("Hardware selected via dropdown: {}", value));
- on_hardware_select.emit(value);
- }
- })
-}
diff --git a/core/bench/dashboard/frontend/src/components/selectors/mod.rs
b/core/bench/dashboard/frontend/src/components/selectors/mod.rs
index 7c0bdef59..6eebff182 100644
--- a/core/bench/dashboard/frontend/src/components/selectors/mod.rs
+++ b/core/bench/dashboard/frontend/src/components/selectors/mod.rs
@@ -15,10 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-pub mod benchmark_selector;
+pub mod benchmarks_list;
pub mod dense_benchmark_row;
-pub mod gitref_selector;
-pub mod hardware_selector;
pub mod measurement_type_selector;
pub mod param_filters_panel;
-pub mod recent_benchmarks_selector;
diff --git
a/core/bench/dashboard/frontend/src/components/selectors/recent_benchmarks_selector.rs
b/core/bench/dashboard/frontend/src/components/selectors/recent_benchmarks_selector.rs
deleted file mode 100644
index 0c06ee5ab..000000000
---
a/core/bench/dashboard/frontend/src/components/selectors/recent_benchmarks_selector.rs
+++ /dev/null
@@ -1,302 +0,0 @@
-// Licensed to the Apache Software Foundation (ASF) under one
-// or more contributor license agreements. See the NOTICE file
-// distributed with this work for additional information
-// regarding copyright ownership. The ASF licenses this file
-// to you under the Apache License, Version 2.0 (the
-// "License"); you may not use this file except in compliance
-// with the License. You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing,
-// software distributed under the License is distributed on an
-// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
-// KIND, either express or implied. See the License for the
-// specific language governing permissions and limitations
-// under the License.
-
-use crate::api;
-use crate::components::selectors::dense_benchmark_row::DenseBenchmarkRow;
-use crate::format::nan_safe_cmp;
-use crate::router::AppRoute;
-use crate::state::benchmark::{BenchmarkAction, use_benchmark};
-use crate::state::ui::{KindGroup, SidebarSort, UiAction, use_ui};
-use bench_dashboard_shared::BenchmarkReportLight;
-use bench_report::benchmark_kind::BenchmarkKind;
-use chrono::DateTime;
-use gloo::console::log;
-use std::cell::Cell;
-use std::cmp::Ordering;
-use std::collections::HashSet;
-use std::rc::Rc;
-use yew::platform::spawn_local;
-use yew::prelude::*;
-use yew_router::prelude::{Navigator, use_navigator};
-
-#[derive(Properties, PartialEq)]
-pub struct RecentBenchmarksSelectorProps {
- pub limit: u32,
-}
-
-#[function_component(RecentBenchmarksSelector)]
-pub fn recent_benchmarks_selector(props: &RecentBenchmarksSelectorProps) ->
Html {
- let benchmark_ctx = use_benchmark();
- let ui_state = use_ui();
- let navigator = use_navigator();
-
- let pinned_uuid = ui_state.compare_pin.as_ref().map(|pin| pin.uuid);
- let selected_uuid = benchmark_ctx
- .state
- .selected_benchmark
- .as_ref()
- .map(|selected| selected.uuid);
-
- let filters = ui_state.param_filters.clone();
- let search = ui_state.sidebar_search.to_lowercase();
- let kind_filter = ui_state.sidebar_kind_filter.clone();
- let sort = ui_state.sidebar_sort;
-
- let recent_benchmarks = use_state(Vec::<BenchmarkReportLight>::new);
- let is_loading = use_state(|| true);
-
- {
- let recent_benchmarks = recent_benchmarks.clone();
- let is_loading = is_loading.clone();
- let limit = props.limit;
- let dispatch = benchmark_ctx.dispatch.clone();
- let navigator = navigator.clone();
- let has_selection = benchmark_ctx.state.selected_benchmark.is_some();
-
- let cancelled = Rc::new(Cell::new(false));
- let cancelled_async = cancelled.clone();
- use_effect_with((), move |_| {
- spawn_local(async move {
- match api::fetch_recent_benchmarks(Some(limit)).await {
- Ok(mut data) => {
- data.sort_by(|left, right| {
- compare_timestamps(&right.timestamp,
&left.timestamp)
- });
- if cancelled_async.get() {
- return;
- }
- if !has_selection && let Some(most_recent) =
data.first().cloned() {
- if let Some(nav) = navigator.as_ref() {
- nav.push(&AppRoute::Benchmark {
- uuid: most_recent.uuid.to_string(),
- });
- }
-
dispatch.emit(BenchmarkAction::SelectBenchmark(Box::new(Some(
- most_recent,
- ))));
- }
- recent_benchmarks.set(data);
- }
- Err(error) => log!(format!("Error fetching recent
benchmarks: {}", error)),
- }
- if !cancelled_async.get() {
- is_loading.set(false);
- }
- });
- move || cancelled.set(true)
- });
- }
-
- let on_select = {
- let dispatch = benchmark_ctx.dispatch.clone();
- let navigator = navigator.clone();
- Callback::from(move |benchmark: BenchmarkReportLight| {
- if let Some(nav) = navigator.as_ref() {
- nav.push(&AppRoute::Benchmark {
- uuid: benchmark.uuid.to_string(),
- });
- }
-
dispatch.emit(BenchmarkAction::SelectBenchmark(Box::new(Some(benchmark))));
- })
- };
-
- let on_toggle_pin = {
- let ui_state = ui_state.clone();
- let navigator = navigator.clone();
- let benchmark_ctx = benchmark_ctx.clone();
- Callback::from(move |benchmark: BenchmarkReportLight| {
- let clicked_uuid = benchmark.uuid.to_string();
- let selected_uuid = benchmark_ctx
- .state
- .selected_benchmark
- .as_ref()
- .map(|selected| selected.uuid.to_string());
- let pinned_uuid = ui_state
- .compare_pin
- .as_ref()
- .map(|pin| pin.uuid.to_string());
-
- if let Some(pinned) = pinned_uuid.as_ref()
- && pinned == &clicked_uuid
- && let Some(selected) = selected_uuid.as_ref()
- {
- push_route(
- &navigator,
- AppRoute::Benchmark {
- uuid: selected.clone(),
- },
- );
- return;
- }
-
- if let Some(selected) = selected_uuid
- && selected != clicked_uuid
- {
- push_route(
- &navigator,
- AppRoute::Compare {
- left: selected,
- right: clicked_uuid,
- },
- );
- return;
- }
-
- let same_pin =
- ui_state.compare_pin.as_ref().map(|pin| pin.uuid) ==
Some(benchmark.uuid);
- let next = if same_pin { None } else { Some(benchmark) };
- ui_state.dispatch(UiAction::SetComparePin(Box::new(next)));
- })
- };
-
- if *is_loading {
- return render_skeleton();
- }
-
- let visible: Vec<BenchmarkReportLight> = (*recent_benchmarks)
- .iter()
- .filter(|benchmark| filters.matches(benchmark))
- .filter(|benchmark| kind_filter_matches(&kind_filter,
benchmark.params.benchmark_kind))
- .filter(|benchmark| search_matches(&search, benchmark))
- .cloned()
- .collect();
-
- if visible.is_empty() {
- return html! {
- <div class="dense-list-empty">
- <p>{"No benchmarks match the current search or filters."}</p>
- </div>
- };
- }
-
- let sorted = sort_benchmarks(visible, sort);
-
- html! {
- <div class="dense-list">
- { for sorted.iter().map(|benchmark| html! {
- <DenseBenchmarkRow
- benchmark={benchmark.clone()}
- selected_uuid={selected_uuid}
- pinned_uuid={pinned_uuid}
- on_select={on_select.clone()}
- on_toggle_pin={on_toggle_pin.clone()}
- show_timestamp={true}
- />
- })}
- </div>
- }
-}
-
-fn push_route(navigator: &Option<Navigator>, route: AppRoute) {
- if let Some(nav) = navigator.as_ref() {
- nav.push(&route);
- }
-}
-
-fn render_skeleton() -> Html {
- html! {
- <div class="dense-list">
- { for (0..5).map(|index| html! {
- <div class="dense-row-skeleton" style={format!("--stagger:
{index}")}>
- <span class="skeleton-dot" />
- <div class="skeleton-body">
- <span class="skeleton-line title" />
- <span class="skeleton-line meta" />
- </div>
- </div>
- })}
- </div>
- }
-}
-
-fn kind_filter_matches(filter: &HashSet<KindGroup>, kind: BenchmarkKind) ->
bool {
- if filter.is_empty() {
- return true;
- }
- filter.iter().any(|group| group.matches(kind))
-}
-
-fn search_matches(query: &str, benchmark: &BenchmarkReportLight) -> bool {
- if query.is_empty() {
- return true;
- }
- let kind_label =
benchmark.params.benchmark_kind.to_string().to_lowercase();
- if kind_label.contains(query) {
- return true;
- }
- if benchmark.params.pretty_name.to_lowercase().contains(query) {
- return true;
- }
- if let Some(remark) = benchmark.params.remark.as_deref()
- && remark.to_lowercase().contains(query)
- {
- return true;
- }
- if let Some(gitref) = benchmark.params.gitref.as_deref()
- && gitref.to_lowercase().contains(query)
- {
- return true;
- }
- if let Some(hardware) = benchmark.hardware.identifier.as_deref()
- && hardware.to_lowercase().contains(query)
- {
- return true;
- }
- if benchmark.timestamp.to_lowercase().contains(query) {
- return true;
- }
- false
-}
-
-fn sort_benchmarks(
- mut benchmarks: Vec<BenchmarkReportLight>,
- sort: SidebarSort,
-) -> Vec<BenchmarkReportLight> {
- benchmarks.sort_by(|left, right| match sort {
- SidebarSort::MostRecent => compare_timestamps(&right.timestamp,
&left.timestamp),
- SidebarSort::PeakThroughput => nan_safe_cmp(throughput(right),
throughput(left)),
- SidebarSort::LowestP99 => nan_safe_cmp(p99(left), p99(right)),
- SidebarSort::Name =>
left.params.pretty_name.cmp(&right.params.pretty_name),
- });
- benchmarks
-}
-
-fn compare_timestamps(left: &str, right: &str) -> Ordering {
- match (
- DateTime::parse_from_rfc3339(left),
- DateTime::parse_from_rfc3339(right),
- ) {
- (Ok(left_time), Ok(right_time)) => left_time.cmp(&right_time),
- _ => Ordering::Equal,
- }
-}
-
-fn throughput(benchmark: &BenchmarkReportLight) -> f64 {
- benchmark
- .group_metrics
- .first()
- .map(|metrics| metrics.summary.total_throughput_megabytes_per_second)
- .unwrap_or(0.0)
-}
-
-fn p99(benchmark: &BenchmarkReportLight) -> f64 {
- benchmark
- .group_metrics
- .first()
- .map(|metrics| metrics.summary.average_p99_latency_ms)
- .unwrap_or(f64::INFINITY)
-}
diff --git
a/core/bench/dashboard/frontend/src/components/tooltips/benchmark_info_tooltip.rs
b/core/bench/dashboard/frontend/src/components/tooltips/benchmark_info_tooltip.rs
index 255e71442..d56e06bb1 100644
---
a/core/bench/dashboard/frontend/src/components/tooltips/benchmark_info_tooltip.rs
+++
b/core/bench/dashboard/frontend/src/components/tooltips/benchmark_info_tooltip.rs
@@ -15,7 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-use crate::state::ui::ViewMode;
use bench_dashboard_shared::BenchmarkReportLight;
use gloo::timers::callback::Timeout;
use web_sys::window;
@@ -25,7 +24,6 @@ use yew::prelude::*;
pub struct BenchmarkInfoTooltipProps {
pub benchmark_report: Option<BenchmarkReportLight>,
pub visible: bool,
- pub view_mode: ViewMode,
}
#[function_component(BenchmarkInfoTooltip)]
@@ -37,7 +35,6 @@ pub fn benchmark_info_tooltip(props:
&BenchmarkInfoTooltipProps) -> Html {
let benchmark_report = props.benchmark_report.as_ref().unwrap();
let hardware = &benchmark_report.hardware;
let params = &benchmark_report.params;
- let is_trend_view = matches!(props.view_mode, ViewMode::GitrefTrend);
let notification_visible = use_state(|| false);
let onclick = {
@@ -73,13 +70,7 @@ pub fn benchmark_info_tooltip(props:
&BenchmarkInfoTooltipProps) -> Html {
<div class="tooltip-section">
<h4>{"Benchmark Parameters"}</h4>
<div class="tooltip-content">
- {if !is_trend_view {
- html! {
- <p><strong>{"Time:
"}</strong>{&benchmark_report.timestamp}</p>
- }
- } else {
- html! {}
- }}
+ <p><strong>{"Time:
"}</strong>{&benchmark_report.timestamp}</p>
<p><strong>{"Kind: "}</strong>{¶ms.benchmark_kind}</p>
<p><strong>{"Transport: "}</strong>{¶ms.transport}</p>
<p><strong>{"Messages: "}</strong>{format!("{} x {} ({}
bytes)",
@@ -101,16 +92,8 @@ pub fn benchmark_info_tooltip(props:
&BenchmarkInfoTooltipProps) -> Html {
)
}
}</p>
- {if !is_trend_view {
- html! {
- <>
- <p><strong>{"Git ref:
"}</strong>{params.gitref.clone()}</p>
- <p><strong>{"Git ref date:
"}</strong>{params.gitref_date.clone()}</p>
- </>
- }
- } else {
- html! {}
- }}
+ <p><strong>{"Git ref:
"}</strong>{params.gitref.clone()}</p>
+ <p><strong>{"Git ref date:
"}</strong>{params.gitref_date.clone()}</p>
</div>
</div>
<div class="tooltip-section">
diff --git
a/core/bench/dashboard/frontend/src/components/tooltips/server_stats_tooltip.rs
b/core/bench/dashboard/frontend/src/components/tooltips/server_stats_tooltip.rs
index 878d8ca19..20587c555 100644
---
a/core/bench/dashboard/frontend/src/components/tooltips/server_stats_tooltip.rs
+++
b/core/bench/dashboard/frontend/src/components/tooltips/server_stats_tooltip.rs
@@ -15,7 +15,6 @@
// specific language governing permissions and limitations
// under the License.
-use crate::state::ui::ViewMode;
use bench_dashboard_shared::BenchmarkReportLight;
use yew::prelude::*;
@@ -23,7 +22,6 @@ use yew::prelude::*;
pub struct ServerStatsTooltipProps {
pub benchmark_report: Option<BenchmarkReportLight>,
pub visible: bool,
- pub view_mode: ViewMode,
}
#[function_component(ServerStatsTooltip)]
diff --git a/core/bench/dashboard/frontend/src/main.rs
b/core/bench/dashboard/frontend/src/main.rs
index fa5479fc8..f36373842 100644
--- a/core/bench/dashboard/frontend/src/main.rs
+++ b/core/bench/dashboard/frontend/src/main.rs
@@ -23,6 +23,7 @@ mod format;
mod hooks;
mod router;
mod state;
+mod version;
use crate::{
components::{app_content::AppContent, footer::Footer},
diff --git a/core/bench/dashboard/frontend/src/state/benchmark.rs
b/core/bench/dashboard/frontend/src/state/benchmark.rs
index 216715c64..4838b4979 100644
--- a/core/bench/dashboard/frontend/src/state/benchmark.rs
+++ b/core/bench/dashboard/frontend/src/state/benchmark.rs
@@ -16,10 +16,12 @@
// under the License.
use crate::format::{finite_or, nan_safe_cmp};
+use crate::version::{SemverRecency, parse_semver_recency};
use bench_dashboard_shared::BenchmarkReportLight;
use bench_report::benchmark_kind::BenchmarkKind;
-use chrono::{DateTime, Duration};
+use chrono::DateTime;
use gloo::console::log;
+use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::rc::Rc;
use yew::prelude::*;
@@ -258,42 +260,117 @@ impl BenchmarkState {
}
}
-/// From a set of benchmarks, pick the one that best represents "current peak":
-/// restrict to runs within 2h of the latest timestamp (avoids UTC day-split
artifacts
-/// on overnight benchmark sweeps), then pick lowest P99 latency; ties broken
by
-/// highest total throughput.
+/// Pick the best benchmark from the most recent sweep.
+///
+/// "Sweep" = runs sharing the newest (hardware, gitref). Ranked by lowest P99,
+/// then P99.9, then P99.99, then highest total throughput.
+/// NaN values rank worst so corrupt runs never win.
pub fn pick_best_from_recent_batch(
benchmarks: &[&BenchmarkReportLight],
) -> Option<BenchmarkReportLight> {
- let latest_timestamp = benchmarks
- .iter()
- .filter_map(|benchmark|
DateTime::parse_from_rfc3339(&benchmark.timestamp).ok())
- .max()?;
- let window_start = latest_timestamp - Duration::hours(2);
+ let mut sweep = latest_sweep(benchmarks);
+ sweep.sort_by_key(rank_sweep_candidate);
+ sweep.first().map(|report| (*report).clone())
+}
+pub fn latest_sweep<'a>(benchmarks: &[&'a BenchmarkReportLight]) -> Vec<&'a
BenchmarkReportLight> {
+ let Some(newest) = benchmarks
+ .iter()
+ .copied()
+ .max_by(|left, right| recency_cmp(left, right))
+ else {
+ return Vec::new();
+ };
+ let sweep_key = sweep_key_of(newest);
benchmarks
.iter()
.copied()
- .filter(|benchmark| {
- DateTime::parse_from_rfc3339(&benchmark.timestamp)
- .map(|timestamp| timestamp >= window_start)
- .unwrap_or(false)
- })
- .min_by(|left, right| {
- let metrics = |report: &BenchmarkReportLight| {
- report.group_metrics.first().map(|group| {
- (
- finite_or(group.summary.average_p99_latency_ms,
f64::INFINITY),
-
finite_or(group.summary.total_throughput_megabytes_per_second, 0.0),
- )
- })
- };
- let (left_p99, left_throughput) =
metrics(left).unwrap_or((f64::INFINITY, 0.0));
- let (right_p99, right_throughput) =
metrics(right).unwrap_or((f64::INFINITY, 0.0));
- nan_safe_cmp(left_p99, right_p99)
- .then_with(|| nan_safe_cmp(right_throughput, left_throughput))
- })
- .cloned()
+ .filter(|benchmark| sweep_key_of(benchmark) == sweep_key)
+ .collect()
+}
+
+/// Order benchmarks by "most recent" using semver-aware recency.
+///
+/// Benchmarks whose gitref parses as a semver-like tag (e.g. `0.7.0`,
+/// `0.7.0-edge.1`) are newer than any plain-commit run. Within each
+/// group we tie-break on RFC3339 timestamp so rebuilds of the same
+/// gitref still order by wall clock.
+pub fn recency_cmp(left: &BenchmarkReportLight, right: &BenchmarkReportLight)
-> Ordering {
+ let left_semver =
left.params.gitref.as_deref().and_then(parse_semver_recency);
+ let right_semver = right
+ .params
+ .gitref
+ .as_deref()
+ .and_then(parse_semver_recency);
+ compare_semver_group(left_semver.as_ref(), right_semver.as_ref())
+ .then_with(|| timestamp_cmp(&left.timestamp, &right.timestamp))
+}
+
+fn compare_semver_group(left: Option<&SemverRecency>, right:
Option<&SemverRecency>) -> Ordering {
+ match (left, right) {
+ (Some(l), Some(r)) => l.cmp(r),
+ (Some(_), None) => Ordering::Greater,
+ (None, Some(_)) => Ordering::Less,
+ (None, None) => Ordering::Equal,
+ }
+}
+
+fn timestamp_cmp(left: &str, right: &str) -> Ordering {
+ match (
+ DateTime::parse_from_rfc3339(left),
+ DateTime::parse_from_rfc3339(right),
+ ) {
+ (Ok(l), Ok(r)) => l.cmp(&r),
+ _ => Ordering::Equal,
+ }
+}
+
+fn sweep_key_of(benchmark: &BenchmarkReportLight) -> (Option<String>,
Option<String>) {
+ (
+ benchmark.hardware.identifier.clone(),
+ benchmark.params.gitref.clone(),
+ )
+}
+
+fn rank_sweep_candidate(
+ benchmark: &&BenchmarkReportLight,
+) -> (FloatKey, FloatKey, FloatKey, FloatKey) {
+ let summary = benchmark.group_metrics.first().map(|group| &group.summary);
+ let p99 = summary
+ .map(|summary| finite_or(summary.average_p99_latency_ms,
f64::INFINITY))
+ .unwrap_or(f64::INFINITY);
+ let p999 = summary
+ .map(|summary| finite_or(summary.average_p999_latency_ms,
f64::INFINITY))
+ .unwrap_or(f64::INFINITY);
+ let p9999 = summary
+ .map(|summary| finite_or(summary.average_p9999_latency_ms,
f64::INFINITY))
+ .unwrap_or(f64::INFINITY);
+ let throughput = summary
+ .map(|summary|
finite_or(summary.total_throughput_megabytes_per_second, 0.0))
+ .unwrap_or(0.0);
+ (
+ FloatKey(p99),
+ FloatKey(p999),
+ FloatKey(p9999),
+ FloatKey(-throughput),
+ )
+}
+
+#[derive(Clone, Copy, PartialEq)]
+struct FloatKey(f64);
+
+impl Eq for FloatKey {}
+
+impl Ord for FloatKey {
+ fn cmp(&self, other: &Self) -> std::cmp::Ordering {
+ nan_safe_cmp(self.0, other.0)
+ }
+}
+
+impl PartialOrd for FloatKey {
+ fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
+ Some(self.cmp(other))
+ }
}
#[derive(Debug)]
@@ -356,54 +433,281 @@ mod tests {
}
#[test]
- fn given_runs_within_window_when_picking_should_return_lowest_p99() {
- let a = benchmark("2026-04-21T10:00:00Z", 5.0, 100.0);
- let b = benchmark("2026-04-21T10:30:00Z", 2.0, 80.0);
- let c = benchmark("2026-04-21T11:00:00Z", 3.5, 120.0);
+ fn given_sweep_runs_when_picking_should_return_lowest_p99() {
+ let a = run(
+ "2026-04-21T10:00:00Z",
+ "hw-a",
+ "0.7.0",
+ [5.0, 10.0, 20.0],
+ 100.0,
+ );
+ let b = run(
+ "2026-04-21T10:30:00Z",
+ "hw-a",
+ "0.7.0",
+ [2.0, 8.0, 18.0],
+ 80.0,
+ );
+ let c = run(
+ "2026-04-21T11:00:00Z",
+ "hw-a",
+ "0.7.0",
+ [3.5, 9.0, 19.0],
+ 120.0,
+ );
let picked = pick_best_from_recent_batch(&[&a, &b,
&c]).expect("expected a pick");
assert_eq!(picked.timestamp, b.timestamp);
}
#[test]
- fn given_tie_on_p99_when_picking_should_break_tie_on_throughput() {
- let low_tput = benchmark("2026-04-21T10:00:00Z", 2.0, 100.0);
- let high_tput = benchmark("2026-04-21T10:10:00Z", 2.0, 200.0);
- let picked = pick_best_from_recent_batch(&[&low_tput,
&high_tput]).expect("expected pick");
+ fn given_tie_on_p99_when_picking_should_break_on_p999() {
+ let worse_tail = run(
+ "2026-04-21T10:00:00Z",
+ "hw-a",
+ "0.7.0",
+ [2.0, 20.0, 30.0],
+ 500.0,
+ );
+ let better_tail = run(
+ "2026-04-21T10:10:00Z",
+ "hw-a",
+ "0.7.0",
+ [2.0, 12.0, 30.0],
+ 90.0,
+ );
+ let picked = pick_best_from_recent_batch(&[&worse_tail,
&better_tail]).expect("pick");
+ assert_eq!(picked.timestamp, better_tail.timestamp);
+ }
+
+ #[test]
+ fn given_tie_on_p99_and_p999_when_picking_should_break_on_p9999() {
+ let worse = run(
+ "2026-04-21T10:00:00Z",
+ "hw-a",
+ "0.7.0",
+ [2.0, 10.0, 30.0],
+ 500.0,
+ );
+ let better = run(
+ "2026-04-21T10:10:00Z",
+ "hw-a",
+ "0.7.0",
+ [2.0, 10.0, 20.0],
+ 90.0,
+ );
+ let picked = pick_best_from_recent_batch(&[&worse,
&better]).expect("pick");
+ assert_eq!(picked.timestamp, better.timestamp);
+ }
+
+ #[test]
+ fn given_tie_on_all_latencies_when_picking_should_break_on_throughput() {
+ let low_tput = run(
+ "2026-04-21T10:00:00Z",
+ "hw-a",
+ "0.7.0",
+ [2.0, 10.0, 20.0],
+ 100.0,
+ );
+ let high_tput = run(
+ "2026-04-21T10:10:00Z",
+ "hw-a",
+ "0.7.0",
+ [2.0, 10.0, 20.0],
+ 200.0,
+ );
+ let picked = pick_best_from_recent_batch(&[&low_tput,
&high_tput]).expect("pick");
assert_eq!(picked.timestamp, high_tput.timestamp);
}
#[test]
- fn given_runs_outside_window_when_picking_should_ignore_old_runs() {
- let old = benchmark("2026-04-20T10:00:00Z", 1.0, 500.0);
- let fresh_slow = benchmark("2026-04-21T12:00:00Z", 10.0, 100.0);
- let fresh_fast = benchmark("2026-04-21T12:30:00Z", 5.0, 90.0);
- let picked = pick_best_from_recent_batch(&[&old, &fresh_slow,
&fresh_fast])
- .expect("expected a pick");
- assert_eq!(picked.timestamp, fresh_fast.timestamp);
+ fn
given_runs_from_older_release_when_picking_should_restrict_to_latest_sweep() {
+ let old_release = run(
+ "2025-01-15T10:00:00Z",
+ "hw-a",
+ "0.5.0",
+ [1.0, 5.0, 10.0],
+ 500.0,
+ );
+ let current_slow = run(
+ "2026-04-21T12:00:00Z",
+ "hw-a",
+ "0.7.0",
+ [10.0, 15.0, 25.0],
+ 100.0,
+ );
+ let current_fast = run(
+ "2026-04-21T12:30:00Z",
+ "hw-a",
+ "0.7.0",
+ [5.0, 12.0, 22.0],
+ 90.0,
+ );
+ let picked = pick_best_from_recent_batch(&[&old_release,
¤t_slow, ¤t_fast])
+ .expect("pick");
+ assert_eq!(picked.timestamp, current_fast.timestamp);
+ }
+
+ #[test]
+ fn
given_runs_on_different_hardware_when_picking_should_restrict_to_latest_sweep()
{
+ let other_hw = run(
+ "2026-04-21T12:40:00Z",
+ "hw-b",
+ "0.7.0",
+ [1.0, 5.0, 10.0],
+ 500.0,
+ );
+ let current = run(
+ "2026-04-21T12:30:00Z",
+ "hw-a",
+ "0.7.0",
+ [5.0, 12.0, 22.0],
+ 90.0,
+ );
+ let picked = pick_best_from_recent_batch(&[&other_hw,
¤t]).expect("pick");
+ assert_eq!(picked.timestamp, other_hw.timestamp);
+ }
+
+ #[test]
+ fn
given_latest_sweep_helper_when_called_should_filter_to_newest_hardware_gitref_group()
{
+ let old = run(
+ "2026-04-20T10:00:00Z",
+ "hw-a",
+ "0.6.0",
+ [1.0, 5.0, 10.0],
+ 100.0,
+ );
+ let latest_a = run(
+ "2026-04-21T12:00:00Z",
+ "hw-a",
+ "0.7.0",
+ [5.0, 10.0, 20.0],
+ 100.0,
+ );
+ let latest_b = run(
+ "2026-04-21T12:30:00Z",
+ "hw-a",
+ "0.7.0",
+ [3.0, 8.0, 18.0],
+ 120.0,
+ );
+ let sweep = latest_sweep(&[&old, &latest_a, &latest_b]);
+ assert_eq!(sweep.len(), 2);
+ assert!(
+ sweep
+ .iter()
+ .any(|report| report.timestamp == latest_a.timestamp)
+ );
+ assert!(
+ sweep
+ .iter()
+ .any(|report| report.timestamp == latest_b.timestamp)
+ );
}
#[test]
fn given_nan_p99_when_picking_should_prefer_finite_values() {
- let mut nan_run = benchmark("2026-04-21T10:00:00Z", 0.0, 100.0);
+ let mut nan_run = run(
+ "2026-04-21T10:00:00Z",
+ "hw-a",
+ "0.7.0",
+ [0.0, 0.0, 0.0],
+ 100.0,
+ );
nan_run.group_metrics[0].summary.average_p99_latency_ms = f64::NAN;
- let good = benchmark("2026-04-21T10:10:00Z", 3.0, 80.0);
- let picked = pick_best_from_recent_batch(&[&nan_run,
&good]).expect("expected a pick");
+ let good = run(
+ "2026-04-21T10:10:00Z",
+ "hw-a",
+ "0.7.0",
+ [3.0, 6.0, 12.0],
+ 80.0,
+ );
+ let picked = pick_best_from_recent_batch(&[&nan_run,
&good]).expect("pick");
assert_eq!(picked.timestamp, good.timestamp);
}
- fn benchmark(timestamp: &str, p99_ms: f64, throughput_mb_s: f64) ->
BenchmarkReportLight {
+ #[test]
+ fn given_shared_recent_feed_when_picking_should_match_sidebar_top_sweep() {
+ let recent_feed_from_api = [
+ run(
+ "2026-04-21T13:30:00Z",
+ "hw-a",
+ "0.7.0",
+ [5.0, 12.0, 22.0],
+ 140.0,
+ ),
+ run(
+ "2026-04-21T13:20:00Z",
+ "hw-a",
+ "0.7.0",
+ [2.0, 9.0, 19.0],
+ 90.0,
+ ),
+ run(
+ "2026-04-21T13:10:00Z",
+ "hw-a",
+ "0.7.0",
+ [3.0, 10.0, 21.0],
+ 120.0,
+ ),
+ run(
+ "2024-02-01T10:00:00Z",
+ "hw-legacy",
+ "0.3.0",
+ [1.0, 4.0, 8.0],
+ 999.0,
+ ),
+ ];
+ let refs: Vec<&BenchmarkReportLight> =
recent_feed_from_api.iter().collect();
+
+ let sidebar_top_sweep_key = (
+ recent_feed_from_api[0].hardware.identifier.clone(),
+ recent_feed_from_api[0].params.gitref.clone(),
+ );
+ let sweep = latest_sweep(&refs);
+ for report in &sweep {
+ assert_eq!(
+ (
+ report.hardware.identifier.clone(),
+ report.params.gitref.clone()
+ ),
+ sidebar_top_sweep_key,
+ "hero sweep leaked outside sidebar's newest (hardware, gitref)"
+ );
+ }
+
+ let picked = pick_best_from_recent_batch(&refs).expect("pick");
+ assert_eq!(picked.hardware.identifier, sidebar_top_sweep_key.0);
+ assert_eq!(picked.params.gitref, sidebar_top_sweep_key.1);
+ assert_eq!(picked.timestamp, recent_feed_from_api[1].timestamp);
+ }
+
+ fn run(
+ timestamp: &str,
+ hardware: &str,
+ gitref: &str,
+ latencies: [f64; 3],
+ throughput_mb_s: f64,
+ ) -> BenchmarkReportLight {
+ let [p99, p999, p9999] = latencies;
let mut report = BenchmarkReportLight {
timestamp: timestamp.to_string(),
..Default::default()
};
+ report.hardware.identifier = Some(hardware.to_string());
+ report.params.gitref = Some(gitref.to_string());
report.group_metrics.push(BenchmarkGroupMetricsLight {
- summary: summary_with(throughput_mb_s, p99_ms),
+ summary: summary_with(throughput_mb_s, p99, p999, p9999),
latency_distribution: None,
});
report
}
- fn summary_with(throughput_mb_s: f64, p99_ms: f64) ->
BenchmarkGroupMetricsSummary {
+ fn summary_with(
+ throughput_mb_s: f64,
+ p99: f64,
+ p999: f64,
+ p9999: f64,
+ ) -> BenchmarkGroupMetricsSummary {
BenchmarkGroupMetricsSummary {
kind: GroupMetricsKind::Producers,
total_throughput_megabytes_per_second: throughput_mb_s,
@@ -413,9 +717,9 @@ mod tests {
average_p50_latency_ms: 0.0,
average_p90_latency_ms: 0.0,
average_p95_latency_ms: 0.0,
- average_p99_latency_ms: p99_ms,
- average_p999_latency_ms: 0.0,
- average_p9999_latency_ms: 0.0,
+ average_p99_latency_ms: p99,
+ average_p999_latency_ms: p999,
+ average_p9999_latency_ms: p9999,
average_latency_ms: 0.0,
average_median_latency_ms: 0.0,
min_latency_ms: 0.0,
diff --git a/core/bench/dashboard/frontend/src/state/ui.rs
b/core/bench/dashboard/frontend/src/state/ui.rs
index 7b4dd68de..c3b241380 100644
--- a/core/bench/dashboard/frontend/src/state/ui.rs
+++ b/core/bench/dashboard/frontend/src/state/ui.rs
@@ -22,14 +22,6 @@ use std::collections::HashSet;
use std::rc::Rc;
use yew::prelude::*;
-#[derive(Clone, Debug, PartialEq)]
-#[allow(dead_code)]
-pub enum ViewMode {
- SingleGitref,
- GitrefTrend,
- RecentBenchmarks,
-}
-
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ParamRange {
pub from: Option<u32>,
@@ -201,7 +193,6 @@ impl KindGroup {
#[derive(Clone, Debug, PartialEq)]
pub struct UiState {
- pub view_mode: ViewMode,
pub selected_measurement: MeasurementType,
pub is_benchmark_tooltip_visible: bool,
pub is_server_stats_tooltip_visible: bool,
@@ -212,12 +203,13 @@ pub struct UiState {
pub sidebar_search: String,
pub sidebar_sort: SidebarSort,
pub sidebar_kind_filter: HashSet<KindGroup>,
+ pub hardware_filter: Option<String>,
+ pub gitref_filter: Option<String>,
}
impl Default for UiState {
fn default() -> Self {
Self {
- view_mode: ViewMode::SingleGitref,
selected_measurement: MeasurementType::Latency,
is_benchmark_tooltip_visible: false,
is_server_stats_tooltip_visible: false,
@@ -228,6 +220,8 @@ impl Default for UiState {
sidebar_search: String::new(),
sidebar_sort: SidebarSort::default(),
sidebar_kind_filter: HashSet::new(),
+ hardware_filter: None,
+ gitref_filter: None,
}
}
}
@@ -242,7 +236,6 @@ pub enum TopBarPopup {
pub enum UiAction {
SetMeasurementType(MeasurementType),
TogglePopup(TopBarPopup),
- SetViewMode(ViewMode),
SetParamRange(ParamField, ParamRange),
SetMetricRange(MetricField, MetricRange),
ClearParamFilters,
@@ -252,6 +245,8 @@ pub enum UiAction {
SetSidebarSearch(String),
SetSidebarSort(SidebarSort),
ToggleKindFilter(KindGroup),
+ SetHardwareFilter(Option<String>),
+ SetGitrefFilter(Option<String>),
}
impl Reducible for UiState {
@@ -285,8 +280,12 @@ impl Reducible for UiState {
..(*self).clone()
}
}
- UiAction::SetViewMode(vm) => UiState {
- view_mode: vm,
+ UiAction::SetHardwareFilter(value) => UiState {
+ hardware_filter: value,
+ ..(*self).clone()
+ },
+ UiAction::SetGitrefFilter(value) => UiState {
+ gitref_filter: value,
..(*self).clone()
},
UiAction::SetParamRange(field, range) => {
diff --git a/core/bench/dashboard/frontend/src/version.rs
b/core/bench/dashboard/frontend/src/version.rs
new file mode 100644
index 000000000..9e452b45b
--- /dev/null
+++ b/core/bench/dashboard/frontend/src/version.rs
@@ -0,0 +1,180 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use std::cmp::Ordering;
+
+/// Recency key derived from a semver-like gitref such as `0.7.0` or
+/// `0.7.0-edge.1`. Larger = more recent.
+///
+/// Ordering rules (differ from strict semver because suffixes in this
+/// project denote post-release nightlies, not pre-release candidates):
+/// - Compare `(major, minor, patch)` first.
+/// - On equal base, a suffixed build (`-edge.N`, `-dev.N`, ...) ranks
+/// ABOVE the plain release, because nightlies ship AFTER the tag.
+/// - Two suffixed builds compare by suffix build number, then tag name.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct SemverRecency {
+ pub major: u32,
+ pub minor: u32,
+ pub patch: u32,
+ pub suffix: Option<SemverSuffix>,
+}
+
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub struct SemverSuffix {
+ pub tag: String,
+ pub number: u32,
+}
+
+impl Ord for SemverRecency {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.major
+ .cmp(&other.major)
+ .then(self.minor.cmp(&other.minor))
+ .then(self.patch.cmp(&other.patch))
+ .then(cmp_suffix(self.suffix.as_ref(), other.suffix.as_ref()))
+ }
+}
+
+impl PartialOrd for SemverRecency {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.cmp(other))
+ }
+}
+
+/// Parse a gitref into a recency key. Returns `None` for plain commit
+/// hashes or anything that doesn't match `X.Y.Z` / `X.Y.Z-tag[.N]`.
+pub fn parse_semver_recency(gitref: &str) -> Option<SemverRecency> {
+ let raw = gitref.trim().trim_start_matches('v');
+ let (base, suffix_str) = match raw.split_once('-') {
+ Some((base, rest)) => (base, Some(rest)),
+ None => (raw, None),
+ };
+ let mut parts = base.split('.');
+ let major = parts.next()?.parse::<u32>().ok()?;
+ let minor = parts.next()?.parse::<u32>().ok()?;
+ let patch = parts.next()?.parse::<u32>().ok()?;
+ if parts.next().is_some() {
+ return None;
+ }
+ let suffix = suffix_str.map(parse_suffix);
+ Some(SemverRecency {
+ major,
+ minor,
+ patch,
+ suffix,
+ })
+}
+
+fn parse_suffix(raw: &str) -> SemverSuffix {
+ match raw.rsplit_once('.') {
+ Some((tag, number)) if !tag.is_empty() => SemverSuffix {
+ tag: tag.to_string(),
+ number: number.parse().unwrap_or(0),
+ },
+ _ => SemverSuffix {
+ tag: raw.to_string(),
+ number: 0,
+ },
+ }
+}
+
+fn cmp_suffix(left: Option<&SemverSuffix>, right: Option<&SemverSuffix>) ->
Ordering {
+ match (left, right) {
+ (None, None) => Ordering::Equal,
+ (Some(_), None) => Ordering::Greater,
+ (None, Some(_)) => Ordering::Less,
+ (Some(l), Some(r)) => l.number.cmp(&r.number).then_with(||
l.tag.cmp(&r.tag)),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn given_plain_commit_hash_when_parsing_should_return_none() {
+ assert!(parse_semver_recency("abc123").is_none());
+ assert!(parse_semver_recency("main").is_none());
+ assert!(parse_semver_recency("0.7").is_none());
+ assert!(parse_semver_recency("0.7.0.1").is_none());
+ }
+
+ #[test]
+ fn given_plain_semver_when_parsing_should_yield_base_only() {
+ let key = parse_semver_recency("0.7.0").expect("parse");
+ assert_eq!(key.major, 0);
+ assert_eq!(key.minor, 7);
+ assert_eq!(key.patch, 0);
+ assert!(key.suffix.is_none());
+ }
+
+ #[test]
+ fn given_suffixed_version_when_parsing_should_capture_tag_and_number() {
+ let key = parse_semver_recency("0.7.0-edge.1").expect("parse");
+ let suffix = key.suffix.as_ref().expect("suffix");
+ assert_eq!(suffix.tag, "edge");
+ assert_eq!(suffix.number, 1);
+ }
+
+ #[test]
+ fn given_leading_v_when_parsing_should_still_match() {
+ let key = parse_semver_recency("v0.7.0-dev.4").expect("parse");
+ assert_eq!(key.major, 0);
+ assert_eq!(key.suffix.as_ref().expect("suffix").tag.as_str(), "dev");
+ }
+
+ #[test]
+ fn
given_suffixed_and_plain_same_base_when_comparing_should_rank_suffixed_higher()
{
+ let edge = parse_semver_recency("0.7.0-edge.1").expect("parse");
+ let plain = parse_semver_recency("0.7.0").expect("parse");
+ assert!(edge > plain);
+ }
+
+ #[test]
+ fn given_bigger_base_when_comparing_should_win_regardless_of_suffix() {
+ let plain_newer = parse_semver_recency("0.7.0").expect("parse");
+ let edge_older = parse_semver_recency("0.6.9-dev.4").expect("parse");
+ assert!(plain_newer > edge_older);
+ }
+
+ #[test]
+ fn
given_same_suffix_tag_different_numbers_when_comparing_should_prefer_higher_number()
{
+ let first = parse_semver_recency("0.7.0-edge.1").expect("parse");
+ let fifth = parse_semver_recency("0.7.0-edge.5").expect("parse");
+ assert!(fifth > first);
+ }
+
+ #[test]
+ fn given_user_example_sequence_when_sorting_should_order_newest_first() {
+ let mut entries = [
+ parse_semver_recency("0.6.9-dev.4").expect("parse"),
+ parse_semver_recency("0.7.0-edge.1").expect("parse"),
+ parse_semver_recency("0.7.0").expect("parse"),
+ ];
+ entries.sort_by(|left, right| right.cmp(left));
+ assert_eq!(
+ entries[0],
+ parse_semver_recency("0.7.0-edge.1").expect("parse")
+ );
+ assert_eq!(entries[1], parse_semver_recency("0.7.0").expect("parse"));
+ assert_eq!(
+ entries[2],
+ parse_semver_recency("0.6.9-dev.4").expect("parse")
+ );
+ }
+}
diff --git a/core/bench/report/src/plotting/chart.rs
b/core/bench/report/src/plotting/chart.rs
index 5a2e02d67..b56b1bd04 100644
--- a/core/bench/report/src/plotting/chart.rs
+++ b/core/bench/report/src/plotting/chart.rs
@@ -77,13 +77,7 @@ impl IggyChart {
.item_height(14)
.type_(LegendType::Scroll),
)
- .grid(
- Grid::new()
- .left("5%")
- .right("20%")
- .top(grid_top)
- .bottom("8%"),
- )
+ .grid(Grid::new().left(70).right("20%").top(grid_top).bottom("8%"))
.data_zoom(
DataZoom::new()
.show(true)
@@ -145,9 +139,10 @@ impl IggyChart {
Axis::new()
.type_(AxisType::Value)
.name(axis_label)
- .name_location(NameLocation::End)
+ .name_location(NameLocation::Middle)
+ .name_rotation(90)
.name_text_style(TextStyle::new().font_size(AXIS_TEXT_SIZE))
- .name_gap(15)
+ .name_gap(44)
.position("left")
.axis_label(AxisLabel::new())
.split_line(SplitLine::new().show(true)),
@@ -162,8 +157,9 @@ impl IggyChart {
Axis::new()
.type_(AxisType::Value)
.name(y1_label)
- .name_location(NameLocation::End)
- .name_gap(15)
+ .name_location(NameLocation::Middle)
+ .name_rotation(90)
+ .name_gap(44)
.name_text_style(TextStyle::new().font_size(AXIS_TEXT_SIZE))
.position("left")
.axis_label(AxisLabel::new())
@@ -174,9 +170,10 @@ impl IggyChart {
Axis::new()
.type_(AxisType::Value)
.name(y2_label)
- .name_location(NameLocation::End)
+ .name_location(NameLocation::Middle)
+ .name_rotation(-90)
.name_text_style(TextStyle::new().font_size(AXIS_TEXT_SIZE))
- .name_gap(15)
+ .name_gap(54)
.position("right")
.axis_label(AxisLabel::new())
.split_line(SplitLine::new().show(true)),