44//! TUI and `--headless` can never disagree about what happened.
55
66use std:: collections:: VecDeque ;
7+ use std:: time:: Instant ;
78
89use pwm_testkit:: report:: PredictorReport ;
910
@@ -32,6 +33,53 @@ pub(crate) struct ExportSubRow {
3233 pub ( crate ) ms : Option < f64 > ,
3334}
3435
36+ /// Typed key facts parsed out of the exporter's kv stream, so the dashboard
37+ /// can show live numbers before the stage-2 report exists.
38+ #[ derive( Default ) ]
39+ pub ( crate ) struct Dash {
40+ /// Checkpoint size in bytes (load stage).
41+ pub ( crate ) checkpoint_bytes : Option < u64 > ,
42+ /// Checkpoint tensor count (load stage).
43+ pub ( crate ) tensors : Option < u64 > ,
44+ /// Checkpoint float parameter count (load stage).
45+ pub ( crate ) float_params : Option < u64 > ,
46+ /// Quantized matrices in the exported manifest (quantize stage).
47+ pub ( crate ) matrices : Option < u64 > ,
48+ /// Exported int8 parameter count (quantize stage; wider scope than the
49+ /// proven relation, which the report narrows to its own count).
50+ pub ( crate ) int8_params : Option < u64 > ,
51+ /// Measured max |int - float| (calibrate stage).
52+ pub ( crate ) error : Option < f64 > ,
53+ /// Bundle-carried float tolerance (calibrate stage).
54+ pub ( crate ) tolerance : Option < f64 > ,
55+ /// Predictor-scoped weights root carried in the bundle (bundle stage).
56+ pub ( crate ) weights_root : Option < String > ,
57+ /// Predictor bundle size in bytes (bundle stage).
58+ pub ( crate ) bundle_bytes : Option < u64 > ,
59+ /// Input provenance string (encode stage).
60+ pub ( crate ) source : Option < String > ,
61+ }
62+
63+ impl Dash {
64+ fn absorb ( & mut self , stage : & str , key : & str , value : & str ) {
65+ let u = || value. parse :: < u64 > ( ) . ok ( ) ;
66+ let f = || value. parse :: < f64 > ( ) . ok ( ) ;
67+ match ( stage, key) {
68+ ( "load" , "checkpoint_bytes" ) => self . checkpoint_bytes = u ( ) ,
69+ ( "load" , "tensors" ) => self . tensors = u ( ) ,
70+ ( "load" , "float_params" ) => self . float_params = u ( ) ,
71+ ( "quantize" , "matrices" ) => self . matrices = u ( ) ,
72+ ( "quantize" , "int8_params" ) => self . int8_params = u ( ) ,
73+ ( "calibrate" , "error" ) => self . error = f ( ) ,
74+ ( "calibrate" , "tolerance" ) => self . tolerance = f ( ) ,
75+ ( "bundle" , "weights_root" ) => self . weights_root = Some ( value. to_string ( ) ) ,
76+ ( "bundle" , "predictor_bundle_bytes" ) => self . bundle_bytes = u ( ) ,
77+ ( "encode" , "source" ) => self . source = Some ( value. to_string ( ) ) ,
78+ _ => { }
79+ }
80+ }
81+ }
82+
3583/// Everything the front ends render.
3684pub ( crate ) struct App {
3785 /// Per-stage lifecycle, indexed by [`Stage::idx`].
@@ -58,6 +106,16 @@ pub(crate) struct App {
58106 pub ( crate ) cache_root : String ,
59107 /// Spinner frame counter (advanced by the draw loop).
60108 pub ( crate ) spin : usize ,
109+ /// Typed dashboard facts parsed from the exporter kv stream.
110+ pub ( crate ) dash : Dash ,
111+ /// Wall-clock start of the run.
112+ pub ( crate ) started : Instant ,
113+ /// Wall-clock end of the run (freezes the elapsed display).
114+ pub ( crate ) finished_at : Option < Instant > ,
115+ /// Per-tick downloaded-bytes deltas, newest last (throughput sparkline).
116+ pub ( crate ) net_history : Vec < u64 > ,
117+ /// Total fetched bytes at the previous tick.
118+ last_net : u64 ,
61119}
62120
63121impl App {
@@ -84,7 +142,33 @@ impl App {
84142 show_logs : false ,
85143 cache_root,
86144 spin : 0 ,
145+ dash : Dash :: default ( ) ,
146+ started : Instant :: now ( ) ,
147+ finished_at : None ,
148+ net_history : Vec :: new ( ) ,
149+ last_net : 0 ,
150+ }
151+ }
152+
153+ /// Advance animation state and sample the download throughput. Called once
154+ /// per draw tick.
155+ pub ( crate ) fn on_tick ( & mut self ) {
156+ self . spin = self . spin . wrapping_add ( 1 ) ;
157+ let total: u64 = self . fetch . iter ( ) . map ( |r| r. done ) . sum ( ) ;
158+ let fetching = matches ! ( self . stages[ Stage :: Fetch . idx( ) ] , StageState :: Running ) ;
159+ if fetching {
160+ self . net_history . push ( total. saturating_sub ( self . last_net ) ) ;
161+ if self . net_history . len ( ) > 72 {
162+ self . net_history . remove ( 0 ) ;
163+ }
87164 }
165+ self . last_net = total;
166+ }
167+
168+ /// Elapsed wall time, frozen once the pipeline finishes.
169+ pub ( crate ) fn elapsed_secs ( & self ) -> f64 {
170+ let end = self . finished_at . unwrap_or_else ( Instant :: now) ;
171+ end. duration_since ( self . started ) . as_secs_f64 ( )
88172 }
89173
90174 /// Fold one pipeline event into the state.
@@ -119,15 +203,21 @@ impl App {
119203 }
120204 }
121205 Event :: ExportProgress { done, total } => self . export_progress = Some ( ( done, total) ) ,
122- Event :: Kv { stage, key, value } => self . kvs . push ( ( stage, key, value) ) ,
206+ Event :: Kv { stage, key, value } => {
207+ self . dash . absorb ( & stage, & key, & value) ;
208+ self . kvs . push ( ( stage, key, value) ) ;
209+ }
123210 Event :: Log ( line) => {
124211 self . logs . push_back ( line) ;
125212 while self . logs . len ( ) > 500 {
126213 self . logs . pop_front ( ) ;
127214 }
128215 }
129216 Event :: Report ( r) => self . report = Some ( * r) ,
130- Event :: Finished { ok } => self . finished = Some ( ok) ,
217+ Event :: Finished { ok } => {
218+ self . finished = Some ( ok) ;
219+ self . finished_at = Some ( Instant :: now ( ) ) ;
220+ }
131221 }
132222 }
133223
0 commit comments