,
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 {
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 878d8ca196..20587c5551 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,
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 fa5479fc8a..f36373842b 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 216715c647..4838b49796 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 {
- 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, Option) {
+ (
+ 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 {
+ 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 7b4dd68def..c3b241380d 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,
@@ -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,
+ pub hardware_filter: Option,
+ pub gitref_filter: Option,
}
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),
+ SetGitrefFilter(Option),
}
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 0000000000..9e452b45b4
--- /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,
+}
+
+#[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 {
+ 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 {
+ 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::().ok()?;
+ let minor = parts.next()?.parse::().ok()?;
+ let patch = parts.next()?.parse::().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 5a2e02d674..b56b1bd04a 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)),