diff --git a/crates/irtt-cli/src/cmd/tui/run.rs b/crates/irtt-cli/src/cmd/tui/run.rs index 58f4912..5587159 100644 --- a/crates/irtt-cli/src/cmd/tui/run.rs +++ b/crates/irtt-cli/src/cmd/tui/run.rs @@ -318,7 +318,7 @@ fn handle_input(state: &mut TuiState, shutdown_requested: &AtomicBool) -> io::Re state.toggle_pause(); force_render = true; } - KeyCode::Char('g') => { + KeyCode::Char('g') | KeyCode::Char('m') => { state.cycle_graph_mode(); force_render = true; } @@ -330,6 +330,42 @@ fn handle_input(state: &mut TuiState, shutdown_requested: &AtomicBool) -> io::Re state.toggle_full_graph(); force_render = true; } + KeyCode::Left => { + state.pan_graph_left(); + force_render = true; + } + KeyCode::Right => { + state.pan_graph_right(); + force_render = true; + } + KeyCode::PageUp => { + state.pan_graph_page_left(); + force_render = true; + } + KeyCode::PageDown => { + state.pan_graph_page_right(); + force_render = true; + } + KeyCode::Home => { + state.jump_graph_oldest(); + force_render = true; + } + KeyCode::End => { + state.jump_graph_live(); + force_render = true; + } + KeyCode::Char('+') | KeyCode::Char('=') => { + state.zoom_graph_in(); + force_render = true; + } + KeyCode::Char('-') => { + state.zoom_graph_out(); + force_render = true; + } + KeyCode::Char('0') => { + state.reset_graph_window(); + force_render = true; + } _ => {} } } diff --git a/crates/irtt-cli/src/cmd/tui/ui.rs b/crates/irtt-cli/src/cmd/tui/ui.rs index e318240..53693c8 100644 --- a/crates/irtt-cli/src/cmd/tui/ui.rs +++ b/crates/irtt-cli/src/cmd/tui/ui.rs @@ -26,11 +26,15 @@ use crate::{ shared::client::{expected_probe_count, GroupPacingArg}, }; -const HISTORY_LIMIT: usize = 240; +const HISTORY_LIMIT: usize = 100_000; const RECENT_EVENT_LIMIT: usize = 80; -const MIN_CHART_POINTS: usize = 12; const MIN_WIDTH: u16 = 56; const MIN_HEIGHT: u16 = 18; +const DEFAULT_GRAPH_WINDOW: Duration = Duration::from_secs(60); +const MIN_GRAPH_WINDOW: Duration = Duration::from_secs(5); +const MAX_GRAPH_WINDOW: Duration = Duration::from_secs(60 * 60); +const PAN_STEP_NUMERATOR: u32 = 1; +const PAN_STEP_DENOMINATOR: u32 = 4; pub(super) struct TuiTerminal { terminal: Terminal>, @@ -91,6 +95,7 @@ pub(super) struct TuiState { last_warning: Option, graph_mode: GraphMode, graph_scale: GraphScale, + graph_viewport: GraphViewport, full_graph: bool, pub(super) paused: bool, pub(super) quit_requested: bool, @@ -125,6 +130,7 @@ impl TuiState { last_warning: None, graph_mode: GraphMode::Rtt, graph_scale: GraphScale::Auto, + graph_viewport: GraphViewport::default(), full_graph: false, paused: false, quit_requested: false, @@ -202,6 +208,7 @@ impl TuiState { } ClientEvent::EchoReply { seq, + received_at, rtt, one_way, server_timing, @@ -217,6 +224,7 @@ impl TuiState { .and_then(|timing| timing.processing) .map(duration_ns); target.push_graph_sample(GraphSample { + timestamp: received_at.mono, seq: *seq, effective_ns: rtt.effective.as_nanos(), raw_ns: duration_ns(rtt.raw), @@ -315,6 +323,7 @@ impl TuiState { for target in &mut self.targets { target.graph_history.clear(); } + self.graph_viewport.follow_live(); self.push_event("visible graph history reset".to_owned()); } @@ -334,6 +343,52 @@ impl TuiState { self.full_graph = !self.full_graph; } + pub(super) fn pan_graph_left(&mut self) { + let oldest = self.oldest_graph_sample_time(); + let newest = self.newest_graph_sample_time(); + self.graph_viewport.pan_backward(oldest, newest); + } + + pub(super) fn pan_graph_right(&mut self) { + let newest = self.newest_graph_sample_time(); + self.graph_viewport.pan_forward(newest); + } + + pub(super) fn pan_graph_page_left(&mut self) { + let oldest = self.oldest_graph_sample_time(); + let newest = self.newest_graph_sample_time(); + self.graph_viewport.page_backward(oldest, newest); + } + + pub(super) fn pan_graph_page_right(&mut self) { + let newest = self.newest_graph_sample_time(); + self.graph_viewport.page_forward(newest); + } + + pub(super) fn jump_graph_oldest(&mut self) { + let oldest = self.oldest_graph_sample_time(); + let newest = self.newest_graph_sample_time(); + self.graph_viewport.jump_oldest(oldest, newest); + } + + pub(super) fn jump_graph_live(&mut self) { + self.graph_viewport.follow_live(); + } + + pub(super) fn zoom_graph_in(&mut self) { + self.graph_viewport.zoom_in(self.newest_graph_sample_time()); + } + + pub(super) fn zoom_graph_out(&mut self) { + self.graph_viewport + .zoom_out(self.newest_graph_sample_time()); + } + + pub(super) fn reset_graph_window(&mut self) { + self.graph_viewport + .reset_window(self.newest_graph_sample_time()); + } + fn push_event(&mut self, event: String) { push_bounded(&mut self.recent_events, event, RECENT_EVENT_LIMIT); } @@ -351,6 +406,20 @@ impl TuiState { fn is_multi_target(&self) -> bool { self.targets.len() > 1 } + + fn oldest_graph_sample_time(&self) -> Option { + self.targets + .iter() + .filter_map(|target| target.graph_history.front().map(|sample| sample.timestamp)) + .min() + } + + fn newest_graph_sample_time(&self) -> Option { + self.targets + .iter() + .filter_map(|target| target.graph_history.back().map(|sample| sample.timestamp)) + .max() + } } #[derive(Debug)] @@ -600,6 +669,178 @@ impl GraphScale { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct GraphViewport { + mode: GraphViewportMode, + window: Duration, +} + +impl Default for GraphViewport { + fn default() -> Self { + Self { + mode: GraphViewportMode::Follow, + window: DEFAULT_GRAPH_WINDOW, + } + } +} + +impl GraphViewport { + fn range(self, now: Instant, newest_sample: Option) -> GraphViewportRange { + let end = match self.mode { + GraphViewportMode::Follow => newest_sample.unwrap_or(now).max(now), + GraphViewportMode::Historical { end } => end, + }; + GraphViewportRange { + start: end.checked_sub(self.window).unwrap_or(end), + end, + window: self.window, + is_live: matches!(self.mode, GraphViewportMode::Follow), + } + } + + fn follow_live(&mut self) { + self.mode = GraphViewportMode::Follow; + } + + fn pan_backward(&mut self, oldest: Option, newest: Option) { + self.pan_by( + -duration_fraction(self.window, PAN_STEP_NUMERATOR, PAN_STEP_DENOMINATOR), + oldest, + newest, + ); + } + + fn pan_forward(&mut self, newest: Option) { + self.pan_by( + duration_fraction(self.window, PAN_STEP_NUMERATOR, PAN_STEP_DENOMINATOR), + None, + newest, + ); + } + + fn page_backward(&mut self, oldest: Option, newest: Option) { + self.pan_by(-signed_duration(self.window), oldest, newest); + } + + fn page_forward(&mut self, newest: Option) { + self.pan_by(signed_duration(self.window), None, newest); + } + + fn jump_oldest(&mut self, oldest: Option, newest: Option) { + let Some(oldest) = oldest else { + return; + }; + let live_end = newest.unwrap_or(oldest); + self.mode = GraphViewportMode::Historical { + end: (oldest + self.window).min(live_end), + }; + } + + fn zoom_in(&mut self, newest: Option) { + self.set_window(duration_fraction(self.window, 2, 3).duration, newest); + } + + fn zoom_out(&mut self, newest: Option) { + self.set_window(duration_fraction(self.window, 3, 2).duration, newest); + } + + fn reset_window(&mut self, newest: Option) { + self.set_window(DEFAULT_GRAPH_WINDOW, newest); + } + + fn set_window(&mut self, window: Duration, newest: Option) { + self.window = window.clamp(MIN_GRAPH_WINDOW, MAX_GRAPH_WINDOW); + if let GraphViewportMode::Historical { end } = &mut self.mode { + if let Some(newest) = newest { + *end = (*end).min(newest); + } + } + } + + fn pan_by( + &mut self, + delta: SignedViewportDuration, + oldest: Option, + newest: Option, + ) { + let Some(newest) = newest else { + return; + }; + let current_end = match self.mode { + GraphViewportMode::Follow => newest, + GraphViewportMode::Historical { end } => end, + }; + let mut end = delta.apply(current_end); + if let Some(oldest) = oldest { + end = end.max(oldest + self.window); + } + end = end.min(newest); + self.mode = GraphViewportMode::Historical { end }; + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GraphViewportMode { + Follow, + Historical { end: Instant }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct GraphViewportRange { + start: Instant, + end: Instant, + window: Duration, + is_live: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct SignedViewportDuration { + duration: Duration, + negative: bool, +} + +impl SignedViewportDuration { + fn apply(self, instant: Instant) -> Instant { + if self.negative { + instant.checked_sub(self.duration).unwrap_or(instant) + } else { + instant + self.duration + } + } +} + +impl std::ops::Neg for SignedViewportDuration { + type Output = Self; + + fn neg(self) -> Self::Output { + Self { + duration: self.duration, + negative: !self.negative, + } + } +} + +fn signed_duration(duration: Duration) -> SignedViewportDuration { + SignedViewportDuration { + duration, + negative: false, + } +} + +fn duration_fraction( + duration: Duration, + numerator: u32, + denominator: u32, +) -> SignedViewportDuration { + let nanos = + duration.as_nanos().saturating_mul(u128::from(numerator)) / u128::from(denominator.max(1)); + let nanos = nanos.max(1).min(u128::from(u64::MAX)); + SignedViewportDuration { + duration: Duration::from_nanos(nanos as u64), + negative: false, + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum MultiTargetGraphMetric { EffectiveRtt, @@ -664,6 +905,7 @@ impl MultiTargetGraphMetric { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) struct GraphSample { + timestamp: Instant, seq: u32, effective_ns: i128, raw_ns: i128, @@ -1037,8 +1279,11 @@ fn timing_panel(state: &TuiState, snapshot: &Snapshot) -> Paragraph<'static> { } fn render_graph_area(frame: &mut Frame<'_>, area: Rect, state: &TuiState) { + let viewport = state + .graph_viewport + .range(Instant::now(), state.newest_graph_sample_time()); if state.is_multi_target() { - render_multi_target_graph(frame, area, state); + render_multi_target_graph(frame, area, state, viewport); return; } @@ -1049,22 +1294,28 @@ fn render_graph_area(frame: &mut Frame<'_>, area: Rect, state: &TuiState) { .selected_target() .map(|target| &target.graph_history) .expect("TuiState always has at least one target"); - let visible = visible_history_window(history, area.width); - let series = graph_series(mode, &visible); + let visible = visible_history_window(history, viewport); + let series = graph_series(mode, &visible, viewport); render_chart( frame, area, - mode.title(), &visible, &series, - mode.axis_kind(), - state.graph_scale, + ChartRenderConfig { + title: mode.title(), + axis_kind: mode.axis_kind(), + scale: state.graph_scale, + viewport, + }, ); } } } fn render_split_graph(frame: &mut Frame<'_>, area: Rect, state: &TuiState) { + let viewport = state + .graph_viewport + .range(Instant::now(), state.newest_graph_sample_time()); let rows = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) @@ -1073,38 +1324,48 @@ fn render_split_graph(frame: &mut Frame<'_>, area: Rect, state: &TuiState) { .selected_target() .map(|target| &target.graph_history) .expect("TuiState always has at least one target"); - let visible = visible_history_window(history, area.width); - let rtt = graph_series(GraphMode::Rtt, &visible); - let one_way = graph_series(GraphMode::OneWay, &visible); + let visible = visible_history_window(history, viewport); + let rtt = graph_series(GraphMode::Rtt, &visible, viewport); + let one_way = graph_series(GraphMode::OneWay, &visible, viewport); render_chart( frame, rows[0], - GraphMode::Rtt.title(), &visible, &rtt, - GraphMode::Rtt.axis_kind(), - state.graph_scale, + ChartRenderConfig { + title: GraphMode::Rtt.title(), + axis_kind: GraphMode::Rtt.axis_kind(), + scale: state.graph_scale, + viewport, + }, ); render_chart( frame, rows[1], - GraphMode::OneWay.title(), &visible, &one_way, - GraphMode::OneWay.axis_kind(), - state.graph_scale, + ChartRenderConfig { + title: GraphMode::OneWay.title(), + axis_kind: GraphMode::OneWay.axis_kind(), + scale: state.graph_scale, + viewport, + }, ); } -fn render_multi_target_graph(frame: &mut Frame<'_>, area: Rect, state: &TuiState) { +fn render_multi_target_graph( + frame: &mut Frame<'_>, + area: Rect, + state: &TuiState, + viewport: GraphViewportRange, +) { let metric = MultiTargetGraphMetric::from_graph_mode(state.graph_mode); - let capacity = visible_sample_capacity(area.width); let series = state .targets .iter() .enumerate() - .filter_map(|(idx, target)| target_metric_series(target, idx, capacity, metric)) + .filter_map(|(idx, target)| target_metric_series(target, idx, viewport, metric)) .collect::>(); if series.is_empty() { frame.render_widget( @@ -1116,10 +1377,6 @@ fn render_multi_target_graph(frame: &mut Frame<'_>, area: Rect, state: &TuiState return; } - let x_max = series - .iter() - .flat_map(|series| series.data.iter().map(|(x, _)| *x)) - .fold(1.0, f64::max); let (min_y, max_y) = chart_y_bounds(&series, metric.axis_kind(), state.graph_scale); let datasets = chart_datasets(&series); let chart = Chart::new(datasets) @@ -1134,8 +1391,8 @@ fn render_multi_target_graph(frame: &mut Frame<'_>, area: Rect, state: &TuiState ) .x_axis( Axis::default() - .bounds([0.0, x_max]) - .labels(vec![Span::raw("old"), Span::raw("new")]) + .bounds(viewport_x_bounds(viewport)) + .labels(viewport_x_axis_labels(viewport)) .style(Style::default().fg(Color::Gray)), ) .y_axis( @@ -1153,11 +1410,9 @@ fn render_multi_target_graph(frame: &mut Frame<'_>, area: Rect, state: &TuiState fn render_chart( frame: &mut Frame<'_>, area: Rect, - title: &'static str, visible: &[&GraphSample], series: &[ChartSeries], - axis_kind: ChartAxisKind, - scale: GraphScale, + config: ChartRenderConfig, ) { if series.is_empty() { let note = if visible.is_empty() { @@ -1167,26 +1422,25 @@ fn render_chart( }; frame.render_widget( Paragraph::new(note) - .block(Block::default().title(title).borders(Borders::ALL)) + .block(Block::default().title(config.title).borders(Borders::ALL)) .wrap(Wrap { trim: true }), area, ); return; } - let (min_y, max_y) = chart_y_bounds(series, axis_kind, scale); - let x_max = visible.len().saturating_sub(1).max(1) as f64; + let (min_y, max_y) = chart_y_bounds(series, config.axis_kind, config.scale); let datasets = chart_datasets(series); let chart = Chart::new(datasets) .block( Block::default() - .title(format!("{title} ({})", scale.label())) + .title(format!("{} ({})", config.title, config.scale.label())) .borders(Borders::ALL), ) .x_axis( Axis::default() - .bounds([0.0, x_max]) - .labels(x_axis_labels(visible)) + .bounds(viewport_x_bounds(config.viewport)) + .labels(viewport_x_axis_labels(config.viewport)) .style(Style::default().fg(Color::Gray)), ) .y_axis( @@ -1201,6 +1455,14 @@ fn render_chart( frame.render_widget(chart, area); } +#[derive(Debug, Clone, Copy)] +struct ChartRenderConfig { + title: &'static str, + axis_kind: ChartAxisKind, + scale: GraphScale, + viewport: GraphViewportRange, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum GraphValue { EffectiveRtt, @@ -1268,18 +1530,21 @@ enum ChartAxisKind { Mixed, } -fn visible_sample_capacity(chart_width: u16) -> usize { - let derived = usize::from(chart_width.saturating_sub(10)).max(MIN_CHART_POINTS); - derived.min(HISTORY_LIMIT) +fn visible_history_window( + history: &VecDeque, + viewport: GraphViewportRange, +) -> Vec<&GraphSample> { + history + .iter() + .filter(|sample| sample.timestamp >= viewport.start && sample.timestamp <= viewport.end) + .collect() } -fn visible_history_window(history: &VecDeque, chart_width: u16) -> Vec<&GraphSample> { - let capacity = visible_sample_capacity(chart_width); - let start = history.len().saturating_sub(capacity); - history.iter().skip(start).collect() -} - -fn graph_series(mode: GraphMode, visible: &[&GraphSample]) -> Vec { +fn graph_series( + mode: GraphMode, + visible: &[&GraphSample], + viewport: GraphViewportRange, +) -> Vec { let values: &[GraphValue] = match mode { GraphMode::Rtt => &[GraphValue::EffectiveRtt], GraphMode::OneWay => &[GraphValue::ClientToServer, GraphValue::ServerToClient], @@ -1292,18 +1557,21 @@ fn graph_series(mode: GraphMode, visible: &[&GraphSample]) -> Vec { }; values .iter() - .filter_map(|value| chart_series(*value, visible)) + .filter_map(|value| chart_series(*value, visible, viewport)) .collect() } -fn chart_series(value: GraphValue, visible: &[&GraphSample]) -> Option { +fn chart_series( + value: GraphValue, + visible: &[&GraphSample], + viewport: GraphViewportRange, +) -> Option { let data: Vec<(f64, f64)> = visible .iter() - .enumerate() - .filter_map(|(idx, sample)| { + .filter_map(|sample| { value .value_ns(sample) - .map(|ns| (idx as f64, ns as f64 / 1_000_000.0)) + .map(|ns| (sample_x(sample, viewport), ns as f64 / 1_000_000.0)) }) .collect(); @@ -1318,19 +1586,17 @@ fn chart_series(value: GraphValue, visible: &[&GraphSample]) -> Option Option { - let start = target.graph_history.len().saturating_sub(capacity); let data = target .graph_history .iter() - .skip(start) - .enumerate() - .filter_map(|(idx, sample)| { + .filter(|sample| sample.timestamp >= viewport.start && sample.timestamp <= viewport.end) + .filter_map(|sample| { metric .value_ns(sample) - .map(|ns| (idx as f64, ns as f64 / 1_000_000.0)) + .map(|ns| (sample_x(sample, viewport), ns as f64 / 1_000_000.0)) }) .collect::>(); @@ -1342,6 +1608,13 @@ fn target_metric_series( }) } +fn sample_x(sample: &GraphSample, viewport: GraphViewportRange) -> f64 { + sample + .timestamp + .saturating_duration_since(viewport.start) + .as_secs_f64() +} + fn chart_datasets(series: &[ChartSeries]) -> Vec> { series .iter() @@ -1459,14 +1732,14 @@ fn nice_upper_ms(value: f64) -> f64 { 10.0 * magnitude } -fn x_axis_labels(visible: &[&GraphSample]) -> Vec> { - let Some(first) = visible.first() else { - return Vec::new(); - }; - let last = visible.last().unwrap_or(first); +fn viewport_x_bounds(viewport: GraphViewportRange) -> [f64; 2] { + [0.0, viewport.window.as_secs_f64().max(1.0)] +} + +fn viewport_x_axis_labels(viewport: GraphViewportRange) -> Vec> { vec![ - Span::raw(format!("#{}", first.seq)), - Span::raw(format!("#{}", last.seq)), + Span::raw(format!("-{}", format_duration(viewport.window))), + Span::raw(if viewport.is_live { "live" } else { "end" }), ] } @@ -1568,18 +1841,38 @@ fn status_line(state: &TuiState) -> Paragraph<'_> { } else { state.graph_mode.label() }; + let mode = graph_viewport_status(state); + let layout = if state.full_graph { " | full" } else { "" }; + let controls = if state.graph_viewport.mode == GraphViewportMode::Follow { + " | q quit | r reset | p pause | m/g metric | s scale | f full | arrows pan | +/- zoom" + } else { + " | End live | ←/→ pan | PgUp/PgDn page | +/- zoom | q quit" + }; let lines = vec![Line::from(format!( - "{}{}{} | graph {}{} scale {} | q quit | Ctrl-C quit | r reset | p pause | g graph | s scale | f full", + "{}{}{} | {mode} | window {} | metric {graph_label}{layout} | scale {}{controls}", state.status.label(), paused, quitting, - graph_label, - if state.full_graph { " full" } else { "" }, + format_duration(state.graph_viewport.window), state.graph_scale.label() ))]; Paragraph::new(lines).block(Block::default().borders(Borders::ALL)) } +fn graph_viewport_status(state: &TuiState) -> String { + match state.graph_viewport.mode { + GraphViewportMode::Follow => "live".to_owned(), + GraphViewportMode::Historical { end } => { + let now = Instant::now(); + let live_end = state.newest_graph_sample_time().unwrap_or(now).max(now); + format!( + "history -{}", + format_duration(live_end.saturating_duration_since(end)) + ) + } + } +} + fn push_time_line(lines: &mut Vec>, label: &str, stats: &TimeStats) { if stats.count == 0 { lines.push(Line::from(format!( @@ -1785,6 +2078,7 @@ mod tests { fn graph_sample(seq: u32, effective_ns: i128) -> GraphSample { GraphSample { + timestamp: Instant::now() + Duration::from_secs(u64::from(seq)), seq, effective_ns, raw_ns: effective_ns + 1_000, @@ -1797,6 +2091,7 @@ mod tests { fn graph_sample_with_timing(seq: u32, effective_ns: i128) -> GraphSample { GraphSample { + timestamp: Instant::now() + Duration::from_secs(u64::from(seq)), seq, effective_ns, raw_ns: effective_ns + 1_000, @@ -1816,6 +2111,23 @@ mod tests { } } + fn viewport(start: Instant, end: Instant) -> GraphViewportRange { + GraphViewportRange { + start, + end, + window: end + .saturating_duration_since(start) + .max(Duration::from_secs(1)), + is_live: true, + } + } + + fn viewport_for_visible(visible: &[&GraphSample]) -> GraphViewportRange { + let start = visible.first().unwrap().timestamp; + let end = visible.last().unwrap().timestamp; + viewport(start, end) + } + fn primary_target(state: &TuiState) -> &TuiTargetState { &state.targets[0] } @@ -2110,17 +2422,15 @@ mod tests { } #[test] - fn visible_window_length_scales_with_chart_width_and_remains_bounded() { + fn visible_window_selects_samples_inside_viewport() { let history: VecDeque<_> = (0..300).map(|seq| graph_sample(seq, seq.into())).collect(); + let start = history[100].timestamp; + let end = history[120].timestamp; - let narrow = visible_history_window(&history, 20); - let wide = visible_history_window(&history, 80); - let huge = visible_history_window(&history, 1_000); + let visible = visible_history_window(&history, viewport(start, end)); - assert_eq!(narrow.len(), MIN_CHART_POINTS); - assert!(wide.len() > narrow.len(), "wide={}", wide.len()); - assert_eq!(huge.len(), HISTORY_LIMIT); - assert_eq!(huge.first().unwrap().seq, 60); + assert_eq!(visible.first().unwrap().seq, 100); + assert_eq!(visible.last().unwrap().seq, 120); } #[test] @@ -2225,9 +2535,10 @@ mod tests { fn optional_missing_one_way_series_are_omitted_not_zero_filled() { let visible_samples = [graph_sample(1, 2_000_000)]; let visible: Vec<_> = visible_samples.iter().collect(); + let viewport = viewport_for_visible(&visible); - let one_way = graph_series(GraphMode::OneWay, &visible); - let rtt = graph_series(GraphMode::Rtt, &visible); + let one_way = graph_series(GraphMode::OneWay, &visible, viewport); + let rtt = graph_series(GraphMode::Rtt, &visible, viewport); assert!(one_way.is_empty()); assert_eq!( @@ -2242,23 +2553,24 @@ mod tests { fn graph_modes_use_readable_default_series() { let visible_samples = [graph_sample_with_timing(1, 3_000_000)]; let visible: Vec<_> = visible_samples.iter().collect(); + let viewport = viewport_for_visible(&visible); assert_eq!( - graph_series(GraphMode::Rtt, &visible) + graph_series(GraphMode::Rtt, &visible, viewport) .iter() .filter_map(|series| series.value) .collect::>(), vec![GraphValue::EffectiveRtt] ); assert_eq!( - graph_series(GraphMode::OneWay, &visible) + graph_series(GraphMode::OneWay, &visible, viewport) .iter() .filter_map(|series| series.value) .collect::>(), vec![GraphValue::ClientToServer, GraphValue::ServerToClient] ); assert_eq!( - graph_series(GraphMode::Combined, &visible) + graph_series(GraphMode::Combined, &visible, viewport) .iter() .filter_map(|series| series.value) .collect::>(), @@ -2273,7 +2585,9 @@ mod tests { #[test] fn multi_target_metric_series_uses_selected_sample_field() { let mut target = TuiTargetState::new("alpha".to_owned(), stats_config(true)); + let timestamp = Instant::now(); target.push_graph_sample(GraphSample { + timestamp, seq: 1, effective_ns: 1_000_000, raw_ns: 2_000_000, @@ -2282,29 +2596,35 @@ mod tests { server_to_client_ns: None, server_processing_ns: Some(4_000_000), }); + let viewport = viewport(timestamp, timestamp + Duration::from_secs(1)); assert_eq!( - target_metric_series(&target, 0, 10, MultiTargetGraphMetric::EffectiveRtt) + target_metric_series(&target, 0, viewport, MultiTargetGraphMetric::EffectiveRtt) .unwrap() .data, vec![(0.0, 1.0)] ); assert_eq!( - target_metric_series(&target, 0, 10, MultiTargetGraphMetric::RawRtt) + target_metric_series(&target, 0, viewport, MultiTargetGraphMetric::RawRtt) .unwrap() .data, vec![(0.0, 2.0)] ); assert_eq!( - target_metric_series(&target, 0, 10, MultiTargetGraphMetric::AdjustedRtt) + target_metric_series(&target, 0, viewport, MultiTargetGraphMetric::AdjustedRtt) .unwrap() .data, vec![(0.0, 3.0)] ); assert_eq!( - target_metric_series(&target, 0, 10, MultiTargetGraphMetric::ServerProcessing) - .unwrap() - .data, + target_metric_series( + &target, + 0, + viewport, + MultiTargetGraphMetric::ServerProcessing + ) + .unwrap() + .data, vec![(0.0, 4.0)] ); } @@ -2312,7 +2632,9 @@ mod tests { #[test] fn multi_target_metric_series_skips_missing_optional_samples() { let mut target = TuiTargetState::new("alpha".to_owned(), stats_config(true)); + let start = Instant::now(); target.push_graph_sample(GraphSample { + timestamp: start, seq: 1, effective_ns: 1_000_000, raw_ns: 2_000_000, @@ -2322,6 +2644,7 @@ mod tests { server_processing_ns: None, }); target.push_graph_sample(GraphSample { + timestamp: start + Duration::from_secs(1), seq: 2, effective_ns: 2_000_000, raw_ns: 3_000_000, @@ -2330,17 +2653,23 @@ mod tests { server_to_client_ns: None, server_processing_ns: Some(5_000_000), }); + let viewport = viewport(start, start + Duration::from_secs(2)); assert_eq!( - target_metric_series(&target, 0, 10, MultiTargetGraphMetric::AdjustedRtt) + target_metric_series(&target, 0, viewport, MultiTargetGraphMetric::AdjustedRtt) .unwrap() .data, vec![(1.0, 4.0)] ); assert_eq!( - target_metric_series(&target, 0, 10, MultiTargetGraphMetric::ServerProcessing) - .unwrap() - .data, + target_metric_series( + &target, + 0, + viewport, + MultiTargetGraphMetric::ServerProcessing + ) + .unwrap() + .data, vec![(1.0, 5.0)] ); }