Files
filmcraft/crates/cfb/tests/writer_scale.rs
WbunkerandClaude Sonnet 5.5 a711c61840 fix(cfb): adding a child is O(1), not a scan of every sibling
Writer::add rejected a duplicate name by comparing the new name with every
sibling through compare_names, which allocates two Vec<u16> per call, so n
children in one storage cost O(n^2) allocating comparisons. An AAF export
puts every marker and component in one storage: 20 000 markers took 58 s,
20 000 clips 56 s, 20 000 connected clips 87 s and 100 000 markers never
finished (an FCPXML of 1 MB is enough). Doubling the input quadrupled the
time; every other exporter stayed linear.

Each storage keeps a set of its children's names as sibling order compares
them (UTF-16 code units, upper-cased), so the duplicate check is a hash
lookup. Same result: case-insensitive, also beyond ASCII, per storage.

20 000 markers now export to AAF in 0.38 s, 100 000 in 2.0 s, 20 000 clips in
0.23 s and 20 000 connected clips in 0.45 s.

Tests: adding 40 000 children must take under 3 s (0.02 s now, 21 s before)
and the duplicate rules are unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-17 10:05:53 -04:00

34 lines
1.8 KiB
Rust

//! The writer scales: adding many children to one storage, and the duplicate-name rules that check keeps.
use filmcraft_cfb::{Version, Writer};
/// Adding a child compared its name with every sibling (allocating twice per comparison), so n children
/// cost O(n^2): 20 000 AAF markers in one storage took about a minute. The duplicate check is a hash
/// lookup now. 40 000 plain names take milliseconds now and about 18 s before (a minute for real marker
/// objects), so the limit separates them by orders of magnitude on any machine.
#[test]
fn adding_many_children_is_not_quadratic() {
let mut w = Writer::new(Version::V3);
let start = std::time::Instant::now();
for i in 0..40_000 {
w.stream(Writer::ROOT, &format!("{i}"), vec![1]).expect("unique name");
}
assert!(start.elapsed() < std::time::Duration::from_secs(3), "{:?}", start.elapsed());
}
/// The faster duplicate check still means what sibling comparison means: case-insensitive, also beyond ASCII,
/// per storage (the same name in another storage is fine), and a name that differs only in length is distinct.
#[test]
fn duplicate_names_are_still_rejected() {
let mut w = Writer::new(Version::V3);
let a = w.storage(Writer::ROOT, "Alpha", [0; 16]).unwrap();
assert!(w.stream(Writer::ROOT, "ALPHA", vec![]).is_err(), "case-insensitive");
assert!(w.stream(Writer::ROOT, "alpha", vec![]).is_err());
assert!(w.stream(Writer::ROOT, "Alph", vec![]).is_ok());
assert!(w.stream(Writer::ROOT, "Alpha2", vec![]).is_ok());
assert!(w.stream(a, "Alpha", vec![]).is_ok(), "same name in another storage");
assert!(w.stream(a, "Alpha", vec![]).is_err());
w.stream(Writer::ROOT, "Ünï", vec![]).unwrap();
assert!(w.stream(Writer::ROOT, "ÜNÏ", vec![]).is_err(), "non-ASCII case");
}