@@ -11,7 +11,8 @@ pub struct App {
1111 pub phase_started : Option < Instant > ,
1212 pub download_samples : Vec < ( f64 , f64 ) > , // (elapsed_secs, mbps)
1313 pub upload_samples : Vec < ( f64 , f64 ) > ,
14- pub latency_samples : Vec < f64 > , // ms, bounded to SPARKLINE_LEN
14+ pub unloaded_latency_samples : Vec < f64 > , // ms, bounded to SPARKLINE_LEN
15+ pub loaded_latency_samples : Vec < f64 > , // ms, bounded to SPARKLINE_LEN
1516 pub current_dl_mbps : f64 ,
1617 pub current_ul_mbps : f64 ,
1718 pub peak_dl_mbps : f64 ,
@@ -30,7 +31,8 @@ impl App {
3031 phase_started : None ,
3132 download_samples : Vec :: new ( ) ,
3233 upload_samples : Vec :: new ( ) ,
33- latency_samples : Vec :: new ( ) ,
34+ unloaded_latency_samples : Vec :: new ( ) ,
35+ loaded_latency_samples : Vec :: new ( ) ,
3436 current_dl_mbps : 0.0 ,
3537 current_ul_mbps : 0.0 ,
3638 peak_dl_mbps : 0.0 ,
@@ -49,17 +51,14 @@ impl App {
4951 Progress :: PhaseStart ( phase) => {
5052 self . current_phase = Some ( phase) ;
5153 self . phase_started = Some ( Instant :: now ( ) ) ;
52- if matches ! ( phase, Phase :: UnloadedLatency | Phase :: LoadedLatency ) {
53- self . latency_samples . clear ( ) ;
54- }
5554 }
5655 Progress :: PhaseEnd ( phase) => {
5756 match phase {
58- Phase :: UnloadedLatency if !self . latency_samples . is_empty ( ) => {
59- self . unloaded_latency_ms = Some ( min_f64 ( & self . latency_samples ) ) ;
57+ Phase :: UnloadedLatency if !self . unloaded_latency_samples . is_empty ( ) => {
58+ self . unloaded_latency_ms = Some ( min_f64 ( & self . unloaded_latency_samples ) ) ;
6059 }
61- Phase :: LoadedLatency if !self . latency_samples . is_empty ( ) => {
62- self . loaded_latency_ms = Some ( min_f64 ( & self . latency_samples ) ) ;
60+ Phase :: LoadedLatency if !self . loaded_latency_samples . is_empty ( ) => {
61+ self . loaded_latency_ms = Some ( min_f64 ( & self . loaded_latency_samples ) ) ;
6362 }
6463 _ => { }
6564 }
@@ -85,10 +84,17 @@ impl App {
8584 _ => { }
8685 } ,
8786 Progress :: Latency { ms } => {
88- self . latency_samples . push ( ms) ;
89- if self . latency_samples . len ( ) > SPARKLINE_LEN {
90- let drop = self . latency_samples . len ( ) - SPARKLINE_LEN ;
91- self . latency_samples . drain ( 0 ..drop) ;
87+ let buf = match self . current_phase {
88+ Some ( Phase :: UnloadedLatency ) => Some ( & mut self . unloaded_latency_samples ) ,
89+ Some ( Phase :: LoadedLatency ) => Some ( & mut self . loaded_latency_samples ) ,
90+ _ => None ,
91+ } ;
92+ if let Some ( buf) = buf {
93+ buf. push ( ms) ;
94+ if buf. len ( ) > SPARKLINE_LEN {
95+ let drop = buf. len ( ) - SPARKLINE_LEN ;
96+ buf. drain ( 0 ..drop) ;
97+ }
9298 }
9399 }
94100 }
@@ -116,7 +122,7 @@ use ratatui::layout::{Constraint, Direction, Layout, Rect};
116122use ratatui:: style:: { Color , Modifier , Style } ;
117123use ratatui:: symbols;
118124use ratatui:: text:: { Line , Span } ;
119- use ratatui:: widgets:: { Axis , Block , Borders , Chart , Dataset , Paragraph , Sparkline } ;
125+ use ratatui:: widgets:: { Axis , Block , Borders , Chart , Dataset , GraphType , Paragraph , Sparkline } ;
120126use ratatui:: Frame ;
121127
122128impl App {
@@ -135,9 +141,9 @@ impl App {
135141 let chunks = Layout :: default ( )
136142 . direction ( Direction :: Vertical )
137143 . constraints ( [
138- Constraint :: Min ( 8 ) , // chart
139- Constraint :: Length ( 5 ) , // latency
140- Constraint :: Length ( 7 ) , // stats + footer
144+ Constraint :: Min ( 8 ) , // chart (gets all leftover space)
145+ Constraint :: Length ( 4 ) , // latency row (two sparklines side-by-side)
146+ Constraint :: Length ( 6 ) , // stats + footer
141147 ] )
142148 . split ( f. area ( ) ) ;
143149
@@ -168,11 +174,13 @@ impl App {
168174 Dataset :: default ( )
169175 . name( "download" )
170176 . marker( symbols:: Marker :: Braille )
177+ . graph_type( GraphType :: Line )
171178 . style( Style :: default ( ) . fg( Color :: Cyan ) )
172179 . data( & dl) ,
173180 Dataset :: default ( )
174181 . name( "upload" )
175182 . marker( symbols:: Marker :: Braille )
183+ . graph_type( GraphType :: Line )
176184 . style( Style :: default ( ) . fg( Color :: Magenta ) )
177185 . data( & ul) ,
178186 ] ;
@@ -205,21 +213,46 @@ impl App {
205213 }
206214
207215 fn render_latency ( & self , f : & mut Frame , area : Rect ) {
208- let data: Vec < u64 > = self . latency_samples . iter ( ) . map ( |x| * x as u64 ) . collect ( ) ;
209- let label = format ! (
210- "Latency unloaded {} loaded {}" ,
216+ let cols = Layout :: default ( )
217+ . direction ( Direction :: Horizontal )
218+ . constraints ( [ Constraint :: Percentage ( 50 ) , Constraint :: Percentage ( 50 ) ] )
219+ . split ( area) ;
220+
221+ let unloaded_data: Vec < u64 > = self
222+ . unloaded_latency_samples
223+ . iter ( )
224+ . map ( |x| * x as u64 )
225+ . collect ( ) ;
226+ let loaded_data: Vec < u64 > = self
227+ . loaded_latency_samples
228+ . iter ( )
229+ . map ( |x| * x as u64 )
230+ . collect ( ) ;
231+
232+ let unloaded_label = format ! (
233+ "Unloaded latency {}" ,
211234 self . unloaded_latency_ms
212235 . map( |x| format!( "{x:.0} ms" ) )
213236 . unwrap_or_else( || "—" . into( ) ) ,
237+ ) ;
238+ let loaded_label = format ! (
239+ "Loaded latency {}" ,
214240 self . loaded_latency_ms
215241 . map( |x| format!( "{x:.0} ms" ) )
216242 . unwrap_or_else( || "—" . into( ) ) ,
217243 ) ;
218- let sparkline = Sparkline :: default ( )
219- . block ( Block :: default ( ) . borders ( Borders :: ALL ) . title ( label) )
220- . data ( & data)
244+
245+ let unloaded = Sparkline :: default ( )
246+ . block ( Block :: default ( ) . borders ( Borders :: ALL ) . title ( unloaded_label) )
247+ . data ( & unloaded_data)
221248 . style ( Style :: default ( ) . fg ( Color :: Yellow ) ) ;
222- f. render_widget ( sparkline, area) ;
249+ let loaded = Sparkline :: default ( )
250+ . block ( Block :: default ( ) . borders ( Borders :: ALL ) . title ( loaded_label) )
251+ . data ( & loaded_data)
252+ . style ( Style :: default ( ) . fg ( Color :: Red ) ) ;
253+
254+ f. render_widget ( unloaded, cols[ 0 ] ) ;
255+ f. render_widget ( loaded, cols[ 1 ] ) ;
223256 }
224257
225258 fn render_stats ( & self , f : & mut Frame , area : Rect ) {
@@ -420,31 +453,44 @@ mod tests {
420453 }
421454
422455 #[ test]
423- fn loaded_latency_clears_unloaded_buffer_then_records_its_own_min ( ) {
456+ fn loaded_and_unloaded_buffers_are_independent ( ) {
424457 let mut app = App :: new ( ) ;
425458 app. apply ( Progress :: PhaseStart ( Phase :: UnloadedLatency ) ) ;
426459 app. apply ( Progress :: Latency { ms : 10.0 } ) ;
427460 app. apply ( Progress :: PhaseEnd ( Phase :: UnloadedLatency ) ) ;
428461 app. apply ( Progress :: PhaseStart ( Phase :: LoadedLatency ) ) ;
429- assert ! ( app. latency_samples. is_empty( ) ) ;
462+ // Unloaded samples are preserved across the loaded phase so the UI
463+ // can render both sparklines simultaneously.
464+ assert_eq ! ( app. unloaded_latency_samples, vec![ 10.0 ] ) ;
465+ assert ! ( app. loaded_latency_samples. is_empty( ) ) ;
430466 app. apply ( Progress :: Latency { ms : 35.0 } ) ;
431467 app. apply ( Progress :: Latency { ms : 38.0 } ) ;
432468 app. apply ( Progress :: PhaseEnd ( Phase :: LoadedLatency ) ) ;
433469 assert_eq ! ( app. unloaded_latency_ms, Some ( 10.0 ) ) ;
434470 assert_eq ! ( app. loaded_latency_ms, Some ( 35.0 ) ) ;
471+ assert_eq ! ( app. unloaded_latency_samples, vec![ 10.0 ] ) ;
472+ assert_eq ! ( app. loaded_latency_samples, vec![ 35.0 , 38.0 ] ) ;
435473 }
436474
437475 #[ test]
438- fn latency_buffer_is_bounded ( ) {
476+ fn latency_outside_latency_phase_is_ignored ( ) {
439477 let mut app = App :: new ( ) ;
440478 app. apply ( Progress :: PhaseStart ( Phase :: Download ) ) ;
479+ app. apply ( Progress :: Latency { ms : 99.0 } ) ;
480+ assert ! ( app. unloaded_latency_samples. is_empty( ) ) ;
481+ assert ! ( app. loaded_latency_samples. is_empty( ) ) ;
482+ }
483+
484+ #[ test]
485+ fn latency_buffer_is_bounded ( ) {
486+ let mut app = App :: new ( ) ;
487+ app. apply ( Progress :: PhaseStart ( Phase :: UnloadedLatency ) ) ;
441488 for i in 0 ..( SPARKLINE_LEN + 10 ) {
442489 app. apply ( Progress :: Latency { ms : i as f64 } ) ;
443490 }
444- assert_eq ! ( app. latency_samples. len( ) , SPARKLINE_LEN ) ;
445- // Most-recent value should be the last we pushed.
491+ assert_eq ! ( app. unloaded_latency_samples. len( ) , SPARKLINE_LEN ) ;
446492 assert_eq ! (
447- * app. latency_samples . last( ) . unwrap( ) ,
493+ * app. unloaded_latency_samples . last( ) . unwrap( ) ,
448494 ( SPARKLINE_LEN + 10 - 1 ) as f64
449495 ) ;
450496 }
0 commit comments