Compare commits

...
3 Commits
Author SHA1 Message Date
Nils Andreas Svee b98d1001de perf(tui): optimize graph rendering and increase UI responsiveness
Rust CI / cargo fmt (push) Successful in 18s
Rust CI / cargo check (x86_64-unknown-linux-gnu) (push) Successful in 33s
Rust CI / cargo clippy (push) Successful in 34s
Rust CI / cargo check (x86_64-unknown-linux-musl) (push) Failing after 7s
Rust CI / cargo test (x86_64-unknown-linux-musl) (push) Failing after 8s
Rust CI / cargo test (x86_64-unknown-linux-gnu) (push) Successful in 1m14s
Rust CI / cargo doc (push) Successful in 24s
Fuzz Smoke / cargo fuzz smoke (push) Failing after 29s
Supply chain / cargo-deny advisories (push) Successful in 6s
Supply chain / cargo-deny bans licenses sources (push) Successful in 5s
Supply chain / actionlint (push) Successful in 2s
Netem Interop / netem interop (push) Successful in 1m5s
Efficiently sample data within the viewport for graph series and reduce the rendering interval for a smoother user experience.
2026-10-06 14:39:05 +07:00
Nils Andreas Svee 8e4424764d feat(tui): interpolate graph series at viewport boundaries
Ensure graph lines extend smoothly to the edges of the visible viewport, preventing abrupt truncations of data series.
2026-10-06 13:20:35 +07:00
Nils Andreas Svee 1e61f4168d refactor(tui): remove superfluous name field from chart series 2026-10-06 12:39:43 +07:00
2 changed files with 54 additions and 24 deletions

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ use tokio::{
time::{sleep, Instant},
};
const RENDER_INTERVAL: Duration = Duration::from_millis(250);
const RENDER_INTERVAL: Duration = Duration::from_millis(100);
pub async fn run_tui(
args: TuiArgs,
+53 -23
View File
@@ -1256,7 +1256,6 @@ fn render_graph_area(frame: &mut Frame<'_>, area: Rect, state: &TuiState) {
#[derive(Debug, Clone, PartialEq)]
struct ChartSeries {
name: String,
style: Style,
data: Vec<(f64, f64)>,
}
@@ -1273,31 +1272,65 @@ fn target_metric_series(
viewport: GraphViewportRange,
metric: GraphMetric,
) -> Option<ChartSeries> {
let data = target
// Histories are monotonic. Skip old samples without converting every
// retained measurement on each repaint, then keep the nearest valid
// predecessor for interpolation across the left boundary.
let first = target
.graph_history
.iter()
.filter(|sample| {
sample.timestamp.mono >= viewport.start && sample.timestamp.mono <= viewport.end
})
.filter_map(|sample| {
.partition_point(|sample| sample.timestamp.mono < viewport.start);
let mut previous = target
.graph_history
.range(..first)
.rev()
.find_map(|sample| {
metric
.value_ns(sample)
.map(|ns| (sample_x(sample, viewport), ns as f64 / 1_000_000.0))
})
.collect::<Vec<_>>();
.map(|ns| (sample.timestamp.mono, ns as f64 / 1_000_000.0))
});
let points = target.graph_history.range(first..).filter_map(|sample| {
metric
.value_ns(sample)
.map(|ns| (sample.timestamp.mono, ns as f64 / 1_000_000.0))
});
let mut data = Vec::new();
for point in points {
if let Some(before) = previous {
// Only genuine crossing segments contribute boundary points. In
// particular, never extend the latest sample to the live edge.
if before.0 < viewport.start && point.0 > viewport.start {
data.push((0.0, interpolate_graph_value(before, point, viewport.start)));
}
if before.0 < viewport.end && point.0 > viewport.end {
data.push((
viewport.window.as_secs_f64(),
interpolate_graph_value(before, point, viewport.end),
));
}
}
if point.0 > viewport.end {
break;
}
if point.0 >= viewport.start {
data.push((
point.0.duration_since(viewport.start).as_secs_f64(),
point.1,
));
}
previous = Some(point);
}
(!data.is_empty()).then_some(ChartSeries {
name: target.label.clone(),
style: target_style(target_idx),
data,
})
}
fn sample_x(sample: &GraphSample, viewport: GraphViewportRange) -> f64 {
sample
.timestamp
.mono
.saturating_duration_since(viewport.start)
.as_secs_f64()
fn interpolate_graph_value(a: (Instant, f64), b: (Instant, f64), at: Instant) -> f64 {
let whole = b.0.duration_since(a.0).as_secs_f64();
if whole == 0.0 {
return b.1;
}
let part = at.duration_since(a.0).as_secs_f64();
a.1 + (b.1 - a.1) * (part / whole)
}
fn chart_datasets(series: &[ChartSeries]) -> Vec<Dataset<'_>> {
@@ -1305,7 +1338,6 @@ fn chart_datasets(series: &[ChartSeries]) -> Vec<Dataset<'_>> {
.iter()
.map(|series| {
Dataset::default()
.name(series.name.as_str())
.marker(symbols::Marker::Braille)
.graph_type(GraphType::Line)
.style(series.style)
@@ -1405,12 +1437,10 @@ fn graph_timestamp_anchor(state: &TuiState, end: Instant) -> Option<ClientTimest
.targets
.iter()
.filter_map(|target| {
target
let after = target
.graph_history
.iter()
.rev()
.find(|sample| sample.timestamp.mono <= end)
.or_else(|| target.graph_history.front())
.partition_point(|sample| sample.timestamp.mono <= end);
target.graph_history.get(after.saturating_sub(1))
})
.map(|sample| sample.timestamp)
.min_by_key(|timestamp| {