Skip to content

Commit dc26a51

Browse files
improve: harden startup report run-id workflows
Co-authored-by: Richie McIlroy <richiemcilroy@users.noreply.github.com>
1 parent 25b42f5 commit dc26a51

3 files changed

Lines changed: 159 additions & 19 deletions

File tree

‎crates/editor/PLAYBACK-BENCHMARKS.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ cargo run -p cap-editor --example playback-startup-report -- --log /path/to/edit
117117
# Filter startup CSV events to a specific labeled run id
118118
cargo run -p cap-editor --example playback-startup-report -- --log /tmp/playback-startup.csv --run-id macos-pass-1
119119

120+
# List run-id sample counts discovered in startup CSV logs
121+
cargo run -p cap-editor --example playback-startup-report -- --log /tmp/playback-startup.csv --list-runs
122+
120123
# Aggregate multiple session logs
121124
cargo run -p cap-editor --example playback-startup-report -- --log /path/to/macos.log --log /path/to/windows.log
122125

@@ -178,6 +181,19 @@ cargo run -p cap-recording --example playback-test-runner -- full
178181
- `cargo run -p cap-editor --example playback-startup-report -- --log crates/editor/PLAYBACK-BENCHMARKS.md --run-id sample-run`
179182
- Completed successfully with filtered metric output path active.
180183

184+
### Benchmark Run: 2026-02-14 00:00:00 UTC (startup report run-id listing + strict filtering)
185+
186+
**Environment:** Linux runner, startup report parser validation
187+
**Commands:** `playback-startup-report --list-runs`, `playback-startup-report --run-id ...`
188+
189+
#### Startup Report CLI Validation
190+
- `--list-runs` mode prints grouped run-id sample counts from CSV traces.
191+
- Requesting a `--run-id` with zero matched startup samples now exits with an explicit failure.
192+
- Validation commands:
193+
- `cargo run -p cap-editor --example playback-startup-report -- --log crates/editor/PLAYBACK-BENCHMARKS.md --list-runs`
194+
- `cargo run -p cap-editor --example playback-startup-report -- --log crates/editor/PLAYBACK-BENCHMARKS.md --run-id missing-run` (expected non-zero exit)
195+
- Unit tests remain green: `cargo test -p cap-editor --example playback-startup-report` (**6 passed**).
196+
181197
### Benchmark Run: 2026-02-14 00:00:00 UTC (supersession span retune to 20)
182198

183199
**Environment:** Linux runner with synthetic 4k60 and 1080p60 MP4 assets

‎crates/editor/PLAYBACK-FINDINGS.md‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,7 +1009,9 @@ The CPU RGBA→NV12 conversion was taking 15-25ms per frame for 3024x1964 resolu
10091009
**What was done**:
10101010
1. Extended startup report parser to read optional CSV run-id column.
10111011
2. Added run-id filters for aggregate mode and baseline/candidate comparison mode.
1012-
3. Added parser tests that validate run-id filtering behavior on mixed-run CSV traces.
1012+
3. Added `--list-runs` mode to enumerate run-id sample counts from startup CSV traces.
1013+
4. Added strict failures when a requested run-id filter matches zero startup samples.
1014+
5. Added parser tests that validate run-id filtering behavior on mixed-run CSV traces.
10131015

10141016
**Changes Made**:
10151017
- `crates/editor/examples/playback-startup-report.rs`
@@ -1018,18 +1020,22 @@ The CPU RGBA→NV12 conversion was taking 15-25ms per frame for 3024x1964 resolu
10181020
- `--run-id`
10191021
- `--baseline-run-id`
10201022
- `--candidate-run-id`
1023+
- `--list-runs`
10211024
- run-id filter now excludes non-matching CSV rows before metric aggregation
1025+
- run-id filtered queries now return explicit non-zero exit on zero matches
10221026
- added unit test coverage for run-id-filtered parsing
10231027
- `crates/editor/PLAYBACK-BENCHMARKS.md`
1024-
- added command examples for run-id filtering and same-file baseline/candidate comparisons
1028+
- added command examples for run-id filtering, run listing, and same-file baseline/candidate comparisons
10251029

10261030
**Verification**:
10271031
- `cargo +1.88.0 test -p cap-editor --example playback-startup-report`
1028-
- `cargo +1.88.0 run -p cap-editor --example playback-startup-report -- --log /workspace/crates/editor/PLAYBACK-BENCHMARKS.md --run-id sample-run`
1032+
- `cargo +1.88.0 run -p cap-editor --example playback-startup-report -- --log /workspace/crates/editor/PLAYBACK-BENCHMARKS.md --list-runs`
1033+
- `cargo +1.88.0 run -p cap-editor --example playback-startup-report -- --log /workspace/crates/editor/PLAYBACK-BENCHMARKS.md --run-id missing-run` (expected non-zero exit)
10291034

10301035
**Results**:
10311036
- ✅ Startup parser supports grouped analysis across repeated sessions in one CSV file.
10321037
- ✅ Baseline/candidate deltas can now target specific labeled runs in shared trace files.
1038+
- ✅ Run-id inventory can be listed before comparisons to avoid manual CSV inspection.
10331039
- ✅ All startup report example tests passing (6/6).
10341040

10351041
**Stopping point**: macOS/Windows startup captures can remain in a single trace file while still enabling precise per-run before/after reporting.

‎crates/editor/examples/playback-startup-report.rs‎

Lines changed: 134 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use 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)]
1626
struct 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

169196
fn 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+
181226
fn 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

Comments
 (0)