11use std:: {
2+ collections:: BTreeMap ,
23 fs:: File ,
34 io:: { BufRead , BufReader } ,
45 path:: PathBuf ,
@@ -12,6 +13,15 @@ struct EventStats {
1213 audio_prerender_startup_ms : Vec < f64 > ,
1314}
1415
16+ impl EventStats {
17+ fn total_samples ( & self ) -> usize {
18+ self . decode_startup_ms . len ( )
19+ + self . render_startup_ms . len ( )
20+ + self . audio_stream_startup_ms . len ( )
21+ + self . audio_prerender_startup_ms . len ( )
22+ }
23+ }
24+
1525#[ derive( Clone , Copy ) ]
1626struct MetricSummary {
1727 samples : usize ,
@@ -121,9 +131,10 @@ fn parse_log(
121131 path : & PathBuf ,
122132 stats : & mut EventStats ,
123133 run_id_filter : Option < & str > ,
124- ) -> Result < ( ) , String > {
134+ ) -> Result < usize , String > {
125135 let file = File :: open ( path) . map_err ( |error| format ! ( "open {} / {error}" , path. display( ) ) ) ?;
126136 let reader = BufReader :: new ( file) ;
137+ let mut matched = 0usize ;
127138
128139 for line in reader. lines ( ) {
129140 let line = line. map_err ( |error| format ! ( "read {} / {error}" , path. display( ) ) ) ?;
@@ -135,10 +146,22 @@ fn parse_log(
135146 }
136147 }
137148 match event {
138- "first_decoded_frame" => stats. decode_startup_ms . push ( startup_ms) ,
139- "first_rendered_frame" => stats. render_startup_ms . push ( startup_ms) ,
140- "audio_streaming_callback" => stats. audio_stream_startup_ms . push ( startup_ms) ,
141- "audio_prerender_callback" => stats. audio_prerender_startup_ms . push ( startup_ms) ,
149+ "first_decoded_frame" => {
150+ stats. decode_startup_ms . push ( startup_ms) ;
151+ matched += 1 ;
152+ }
153+ "first_rendered_frame" => {
154+ stats. render_startup_ms . push ( startup_ms) ;
155+ matched += 1 ;
156+ }
157+ "audio_streaming_callback" => {
158+ stats. audio_stream_startup_ms . push ( startup_ms) ;
159+ matched += 1 ;
160+ }
161+ "audio_prerender_callback" => {
162+ stats. audio_prerender_startup_ms . push ( startup_ms) ;
163+ matched += 1 ;
164+ }
142165 _ => { }
143166 }
144167 continue ;
@@ -154,16 +177,20 @@ fn parse_log(
154177
155178 if line. contains ( "Playback first decoded frame ready" ) {
156179 stats. decode_startup_ms . push ( startup_ms) ;
180+ matched += 1 ;
157181 } else if line. contains ( "Playback first frame rendered" ) {
158182 stats. render_startup_ms . push ( startup_ms) ;
183+ matched += 1 ;
159184 } else if line. contains ( "Audio streaming callback started" ) {
160185 stats. audio_stream_startup_ms . push ( startup_ms) ;
186+ matched += 1 ;
161187 } else if line. contains ( "Audio pre-rendered callback started" ) {
162188 stats. audio_prerender_startup_ms . push ( startup_ms) ;
189+ matched += 1 ;
163190 }
164191 }
165192
166- Ok ( ( ) )
193+ Ok ( matched )
167194}
168195
169196fn parse_csv_startup_event ( line : & str ) -> Option < ( & str , f64 , Option < & str > ) > {
@@ -178,11 +205,29 @@ fn parse_csv_startup_event(line: &str) -> Option<(&str, f64, Option<&str>)> {
178205 Some ( ( event, startup_ms, run_id) )
179206}
180207
208+ fn collect_run_id_counts ( path : & PathBuf ) -> Result < BTreeMap < String , usize > , String > {
209+ let file = File :: open ( path) . map_err ( |error| format ! ( "open {} / {error}" , path. display( ) ) ) ?;
210+ let reader = BufReader :: new ( file) ;
211+ let mut counts = BTreeMap :: < String , usize > :: new ( ) ;
212+
213+ for line in reader. lines ( ) {
214+ let line = line. map_err ( |error| format ! ( "read {} / {error}" , path. display( ) ) ) ?;
215+ if let Some ( ( _, _, run_id) ) = parse_csv_startup_event ( & line)
216+ && let Some ( run_id) = run_id
217+ {
218+ let entry = counts. entry ( run_id. to_string ( ) ) . or_insert ( 0 ) ;
219+ * entry += 1 ;
220+ }
221+ }
222+
223+ Ok ( counts)
224+ }
225+
181226fn main ( ) {
182227 let args = std:: env:: args ( ) . skip ( 1 ) . collect :: < Vec < _ > > ( ) ;
183228 if args. is_empty ( ) {
184229 eprintln ! (
185- "Usage: playback-startup-report [--log <path> ...] [--run-id <id>] [--baseline-log <path> ... --candidate-log <path> ...] [--baseline-run-id <id>] [--candidate-run-id <id>]"
230+ "Usage: playback-startup-report [--log <path> ...] [--run-id <id>] [--list-runs] [-- baseline-log <path> ... --candidate-log <path> ...] [--baseline-run-id <id>] [--candidate-run-id <id>]"
186231 ) ;
187232 std:: process:: exit ( 1 ) ;
188233 }
@@ -193,6 +238,7 @@ fn main() {
193238 let mut run_id: Option < String > = None ;
194239 let mut baseline_run_id: Option < String > = None ;
195240 let mut candidate_run_id: Option < String > = None ;
241+ let mut list_runs = false ;
196242 let mut index = 0usize ;
197243
198244 while index < args. len ( ) {
@@ -251,6 +297,11 @@ fn main() {
251297 eprintln ! ( "Missing value for --candidate-run-id" ) ;
252298 std:: process:: exit ( 1 ) ;
253299 }
300+ "--list-runs" => {
301+ list_runs = true ;
302+ index += 1 ;
303+ continue ;
304+ }
254305 _ => {
255306 eprintln ! ( "Unknown argument: {}" , args[ index] ) ;
256307 std:: process:: exit ( 1 ) ;
@@ -268,6 +319,11 @@ fn main() {
268319 std:: process:: exit ( 1 ) ;
269320 }
270321
322+ if list_runs && ( !baseline_logs. is_empty ( ) || !candidate_logs. is_empty ( ) ) {
323+ eprintln ! ( "--list-runs supports only --log inputs" ) ;
324+ std:: process:: exit ( 1 ) ;
325+ }
326+
271327 if baseline_logs. is_empty ( ) && baseline_run_id. is_some ( ) {
272328 eprintln ! ( "--baseline-run-id requires --baseline-log" ) ;
273329 std:: process:: exit ( 1 ) ;
@@ -279,13 +335,51 @@ fn main() {
279335 }
280336
281337 if !logs. is_empty ( ) {
338+ if list_runs {
339+ let mut aggregated = BTreeMap :: < String , usize > :: new ( ) ;
340+ for log in & logs {
341+ match collect_run_id_counts ( log) {
342+ Ok ( counts) => {
343+ for ( run_id_key, count) in counts {
344+ let entry = aggregated. entry ( run_id_key) . or_insert ( 0 ) ;
345+ * entry += count;
346+ }
347+ }
348+ Err ( error) => {
349+ eprintln ! ( "{error}" ) ;
350+ std:: process:: exit ( 1 ) ;
351+ }
352+ }
353+ }
354+
355+ println ! ( "Startup trace run-id counts" ) ;
356+ if aggregated. is_empty ( ) {
357+ println ! ( "no run ids found" ) ;
358+ } else {
359+ for ( run_id_key, count) in aggregated {
360+ println ! ( "{run_id_key}: {count}" ) ;
361+ }
362+ }
363+ return ;
364+ }
365+
282366 let mut stats = EventStats :: default ( ) ;
367+ let mut matched = 0usize ;
283368 for log in & logs {
284- if let Err ( error) = parse_log ( log, & mut stats, run_id. as_deref ( ) ) {
285- eprintln ! ( "{error}" ) ;
286- std:: process:: exit ( 1 ) ;
369+ match parse_log ( log, & mut stats, run_id. as_deref ( ) ) {
370+ Ok ( count) => {
371+ matched += count;
372+ }
373+ Err ( error) => {
374+ eprintln ! ( "{error}" ) ;
375+ std:: process:: exit ( 1 ) ;
376+ }
287377 }
288378 }
379+ if run_id. is_some ( ) && matched == 0 {
380+ eprintln ! ( "No startup samples matched the requested --run-id" ) ;
381+ std:: process:: exit ( 1 ) ;
382+ }
289383
290384 println ! ( "Playback startup metrics" ) ;
291385 print_metric ( "first decoded frame" , & stats. decode_startup_ms ) ;
@@ -300,20 +394,44 @@ fn main() {
300394 if !baseline_logs. is_empty ( ) {
301395 let mut baseline_stats = EventStats :: default ( ) ;
302396 let baseline_filter = baseline_run_id. as_deref ( ) . or ( run_id. as_deref ( ) ) ;
397+ let mut baseline_matched = 0usize ;
303398 for log in & baseline_logs {
304- if let Err ( error) = parse_log ( log, & mut baseline_stats, baseline_filter) {
305- eprintln ! ( "{error}" ) ;
306- std:: process:: exit ( 1 ) ;
399+ match parse_log ( log, & mut baseline_stats, baseline_filter) {
400+ Ok ( count) => {
401+ baseline_matched += count;
402+ }
403+ Err ( error) => {
404+ eprintln ! ( "{error}" ) ;
405+ std:: process:: exit ( 1 ) ;
406+ }
307407 }
308408 }
409+ if baseline_filter. is_some ( ) && baseline_matched == 0 {
410+ eprintln ! ( "No baseline startup samples matched the requested run id filter" ) ;
411+ std:: process:: exit ( 1 ) ;
412+ }
309413 let mut candidate_stats = EventStats :: default ( ) ;
310414 let candidate_filter = candidate_run_id. as_deref ( ) . or ( run_id. as_deref ( ) ) ;
415+ let mut candidate_matched = 0usize ;
311416 for log in & candidate_logs {
312- if let Err ( error) = parse_log ( log, & mut candidate_stats, candidate_filter) {
313- eprintln ! ( "{error}" ) ;
314- std:: process:: exit ( 1 ) ;
417+ match parse_log ( log, & mut candidate_stats, candidate_filter) {
418+ Ok ( count) => {
419+ candidate_matched += count;
420+ }
421+ Err ( error) => {
422+ eprintln ! ( "{error}" ) ;
423+ std:: process:: exit ( 1 ) ;
424+ }
315425 }
316426 }
427+ if candidate_filter. is_some ( ) && candidate_matched == 0 {
428+ eprintln ! ( "No candidate startup samples matched the requested run id filter" ) ;
429+ std:: process:: exit ( 1 ) ;
430+ }
431+ if baseline_stats. total_samples ( ) == 0 || candidate_stats. total_samples ( ) == 0 {
432+ eprintln ! ( "No startup samples available for baseline/candidate comparison" ) ;
433+ std:: process:: exit ( 1 ) ;
434+ }
317435
318436 println ! ( "Startup delta (candidate - baseline)" ) ;
319437 print_delta (
0 commit comments