From e3616f4e3831a7a779a1cd98836d0a81269d29a8 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 12:51:39 -0400 Subject: [PATCH 1/2] dash_read_bounds: time the short and long runs in turn, the least of seven each The linear-time check failed on a loaded linux-arm64 runner with both figures about ten times their usual size (10000 attributes 10.8 ms, 40000 181 ms): on a virtual machine a thread's CPU clock also counts the time the host held the virtual CPU, so a busy host slows every run of one size before the other is timed. Taking the two in turn lets a busy spell slow both alike, and seven runs each make the least of each the run it disturbed least. The bounds are unchanged: a reader that scans the names read so far still fails them. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/tests/dash_read_bounds.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/wai/wai-rs/tests/dash_read_bounds.rs b/wai/wai-rs/tests/dash_read_bounds.rs index 3e9f53320..05a5037ce 100644 --- a/wai/wai-rs/tests/dash_read_bounds.rs +++ b/wai/wai-rs/tests/dash_read_bounds.rs @@ -92,12 +92,17 @@ fn peak(f: impl FnOnce() -> T) -> (T, usize) { } /// The CPU time of the best of three runs of `f`. -fn cpu(f: impl Fn()) -> Duration { - (0..3).map(|_| { +/// The least CPU time of `f` and of `g` over seven runs each, taken in turn. On a virtual +/// machine a thread's CPU clock also counts the time its host held the virtual CPU, so a +/// busy host slows every run: taking the two in turn lets a busy spell slow both alike, and +/// the least of each is the run it disturbed least. +fn cpu_pair(f: impl Fn(), g: impl Fn()) -> (Duration, Duration) { + let time = |h: &dyn Fn()| { let t = now(); - f(); + h(); now() - t - }).min().expect("three runs") + }; + (0..7).map(|_| (time(&f), time(&g))).fold((Duration::MAX, Duration::MAX), |(a, b), (x, y)| (a.min(x), b.min(y))) } /// `n` Representations, each a timeline of one `S` of 2^20 segments. @@ -141,13 +146,13 @@ fn an_mpd_is_held_as_written_and_read_in_linear_time() { // themselves, take about the CPU time of 32 timelines of 2^10; one that steps a // segment at a time takes about a thousand times as long. let (few, many) = (bomb_of(32, 1 << 10), bomb_of(32, 1 << 20)); - let (t_few, t_many) = (cpu(|| check_dash_adjacent(&few, &few).unwrap()), cpu(|| check_dash_adjacent(&many, &many).unwrap())); + let (t_few, t_many) = cpu_pair(|| check_dash_adjacent(&few, &few).unwrap(), || check_dash_adjacent(&many, &many).unwrap()); assert!(t_many <= t_few * 4 + Duration::from_millis(20), "2^20 segments a timeline took {t_many:?}, 2^10 took {t_few:?}"); // One element of 40000 attributes against one of 10000: linear, so about four times // the CPU time; a reader that scans the names read so far for each one takes about // sixteen times. The margin keeps a loaded host from deciding it. let (short, long) = (wide(10_000), wide(40_000)); read_xml(&long).unwrap(); - let (t_short, t_long) = (cpu(|| drop(read_xml(&short))), cpu(|| drop(read_xml(&long)))); + let (t_short, t_long) = cpu_pair(|| drop(read_xml(&short)), || drop(read_xml(&long))); assert!(t_long <= t_short * 8 + Duration::from_millis(20), "40000 attributes took {t_long:?}, 10000 took {t_short:?}"); } -- 2.54.0 (Apple Git-157) From a4784fc68ff95912a1e6bee316c8538f27d9914e Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Wed, 21 Oct 2026 14:23:25 -0400 Subject: [PATCH 2/2] dash_read_bounds: 1000 attributes against 32000, bound at 256 times Interleaving was not enough: the linux-arm64 runner failed it again at 40000 against 10000 (120 ms, 11 ms). Linear reading of the larger element costs up to about three times more per attribute when its sets leave a cache a busy host shares, which puts linear near 12 times and a quadratic reader at 16: too close to bound. With sizes 32 apart, linear is 32 times (about 100 with that cache effect) and quadratic about a thousand, and the bound sits between them. Co-Authored-By: Claude Opus 5.5 --- wai/wai-rs/tests/dash_read_bounds.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/wai/wai-rs/tests/dash_read_bounds.rs b/wai/wai-rs/tests/dash_read_bounds.rs index 05a5037ce..7f6792913 100644 --- a/wai/wai-rs/tests/dash_read_bounds.rs +++ b/wai/wai-rs/tests/dash_read_bounds.rs @@ -7,7 +7,7 @@ //! compared with each other within a footprint bounded by the document's size: the //! runs are held as written and compared run by run, never expanded. //! - Comparing them takes CPU time that does not grow with the segments a run lists. -//! - One element of 40000 attributes is read in time linear in its attributes: the +//! - One element of 32000 attributes is read in time linear in its attributes: the //! names already read are kept in a set, not scanned again for each attribute. //! //! Its own test binary with a single test, so that no other test's allocations land in @@ -91,7 +91,6 @@ fn peak(f: impl FnOnce() -> T) -> (T, usize) { (out, PEAK.load(Relaxed) - start) } -/// The CPU time of the best of three runs of `f`. /// The least CPU time of `f` and of `g` over seven runs each, taken in turn. On a virtual /// machine a thread's CPU clock also counts the time its host held the virtual CPU, so a /// busy host slows every run: taking the two in turn lets a busy spell slow both alike, and @@ -148,11 +147,13 @@ fn an_mpd_is_held_as_written_and_read_in_linear_time() { let (few, many) = (bomb_of(32, 1 << 10), bomb_of(32, 1 << 20)); let (t_few, t_many) = cpu_pair(|| check_dash_adjacent(&few, &few).unwrap(), || check_dash_adjacent(&many, &many).unwrap()); assert!(t_many <= t_few * 4 + Duration::from_millis(20), "2^20 segments a timeline took {t_many:?}, 2^10 took {t_few:?}"); - // One element of 40000 attributes against one of 10000: linear, so about four times - // the CPU time; a reader that scans the names read so far for each one takes about - // sixteen times. The margin keeps a loaded host from deciding it. - let (short, long) = (wide(10_000), wide(40_000)); + // One element of 32000 attributes against one of 1000: linear, so about 32 times the + // CPU time, and up to about three times that again where the larger element's sets + // leave a cache a busy host shares; a reader that scans the names read so far for each + // one takes about a thousand times. The sizes are 32 apart so that no cache effect + // reaches the bound between the two. + let (short, long) = (wide(1_000), wide(32_000)); read_xml(&long).unwrap(); let (t_short, t_long) = cpu_pair(|| drop(read_xml(&short)), || drop(read_xml(&long))); - assert!(t_long <= t_short * 8 + Duration::from_millis(20), "40000 attributes took {t_long:?}, 10000 took {t_short:?}"); + assert!(t_long <= t_short * 256 + Duration::from_millis(20), "32000 attributes took {t_long:?}, 1000 took {t_short:?}"); } -- 2.54.0 (Apple Git-157)