mod smoke_common;
use std::fmt::Write as FmtWrite;
use std::time::Instant;
use manasight_parser::{parse_whole_log, GameEvent};
struct BlobTiming {
filename: String,
bytes: usize,
elapsed_ms: f64,
match_count: u32,
}
impl BlobTiming {
fn per_match_ms(&self) -> Option<f64> {
if self.match_count == 0 {
None
} else {
Some(self.elapsed_ms / f64::from(self.match_count))
}
}
}
fn count_completed_matches(events: &[GameEvent]) -> u32 {
events
.iter()
.filter(|e| {
if let GameEvent::MatchState(ms) = e {
ms.payload()["type"] == "match_completed"
} else {
false
}
})
.fold(0u32, |acc, _| acc.saturating_add(1))
}
fn measure_blob(path: &std::path::Path) -> Option<BlobTiming> {
let filename = path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_default();
let content = std::fs::read_to_string(path).ok()?;
let bytes = content.len();
let start = Instant::now();
let events = parse_whole_log(&content);
let elapsed_ms = start.elapsed().as_secs_f64() * 1000.0;
let match_count = count_completed_matches(&events);
Some(BlobTiming {
filename,
bytes,
elapsed_ms,
match_count,
})
}
fn fmt_bytes(bytes: usize) -> String {
if bytes >= 1_000_000 {
let mb_tenths = bytes / 100_000; format!("{}.{} MB", mb_tenths / 10, mb_tenths % 10)
} else if bytes >= 1_000 {
let kb_tenths = bytes / 100; format!("{}.{} KB", kb_tenths / 10, kb_tenths % 10)
} else {
format!("{bytes} B")
}
}
fn format_timing_report(timings: &[BlobTiming]) -> String {
let mut out = String::new();
let _ = writeln!(out, "\n=== Corpus Parse Timing Report ===\n");
let _ = writeln!(
out,
"{:<52} {:>8} {:>10} {:>8} {:>12}",
"File", "Size", "Total ms", "Matches", "Per-match ms",
);
let _ = writeln!(out, "{}", "-".repeat(98));
for t in timings {
let per_match = t
.per_match_ms()
.map_or_else(|| "n/a".to_owned(), |ms| format!("{ms:.1}"));
let _ = writeln!(
out,
"{:<52} {:>8} {:>10.1} {:>8} {:>12}",
t.filename,
fmt_bytes(t.bytes),
t.elapsed_ms,
t.match_count,
per_match,
);
}
let worst = timings.iter().filter(|t| t.match_count > 0).max_by(|a, b| {
a.per_match_ms()
.unwrap_or(0.0)
.partial_cmp(&b.per_match_ms().unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
let _ = writeln!(out);
if let Some(w) = worst {
let _ = writeln!(
out,
"Worst-case per-match: {:.1} ms/match [{} · {} · {} matches · {:.1} ms total]",
w.per_match_ms().unwrap_or(0.0),
w.filename,
fmt_bytes(w.bytes),
w.match_count,
w.elapsed_ms,
);
let _ = writeln!(
out,
" (native --release; wasm32 in the deployed runtime is ~1.5–3× slower)"
);
} else {
let _ = writeln!(
out,
"Worst-case: no completed matches found in any blob — check corpus contents."
);
}
let _ = writeln!(out, "\n=== PASS ===");
out
}
#[test]
fn corpus_parse_timing() {
let Some(logs_dir) = smoke_common::logs_dir_or_skip("corpus_parse_timing") else {
return;
};
let log_files = smoke_common::assert_logs_dir(&logs_dir);
let mut timings: Vec<BlobTiming> = Vec::new();
for path in &log_files {
let Some(t) = measure_blob(path) else {
let msg = format!(
"corpus_parse_timing: could not read {}, skipping\n",
path.display()
);
let _ = std::io::Write::write_all(&mut std::io::stdout(), msg.as_bytes());
continue;
};
timings.push(t);
}
assert!(
!timings.is_empty(),
"no corpus files could be measured — check MANASIGHT_TEST_LOGS path"
);
let report = format_timing_report(&timings);
let _ = std::io::Write::write_all(&mut std::io::stdout(), report.as_bytes());
}