153 lines
6.3 KiB
Rust
153 lines
6.3 KiB
Rust
//! Reading an MPD (`extensions/http-delivery.md` Appendix I) holds what the document
|
|
//! writes, not what its `@r` says, and reads an element's attributes in linear time.
|
|
//! Measured with a global allocator that counts the bytes live and their peak, and the
|
|
//! calling thread's CPU time.
|
|
//!
|
|
//! - A few kilobytes of 32 timelines, each one `S` of 2^20 segments, are read and
|
|
//! 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
|
|
//! 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
|
|
//! the windows it measures.
|
|
#![cfg(feature = "http")]
|
|
|
|
use std::alloc::{GlobalAlloc, Layout, System};
|
|
use std::sync::atomic::{AtomicUsize, Ordering::Relaxed};
|
|
use std::time::Duration;
|
|
|
|
use wai::http_binding::dash_read::{check_dash_adjacent, read_mpd, read_xml};
|
|
|
|
/// The system allocator, counting the bytes live and their peak.
|
|
struct Count;
|
|
static CUR: AtomicUsize = AtomicUsize::new(0);
|
|
static PEAK: AtomicUsize = AtomicUsize::new(0);
|
|
|
|
fn grow(n: usize) {
|
|
PEAK.fetch_max(CUR.fetch_add(n, Relaxed) + n, Relaxed);
|
|
}
|
|
|
|
unsafe impl GlobalAlloc for Count {
|
|
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
|
|
grow(l.size());
|
|
unsafe { System.alloc(l) }
|
|
}
|
|
unsafe fn alloc_zeroed(&self, l: Layout) -> *mut u8 {
|
|
grow(l.size());
|
|
unsafe { System.alloc_zeroed(l) }
|
|
}
|
|
unsafe fn realloc(&self, p: *mut u8, l: Layout, new: usize) -> *mut u8 {
|
|
if new >= l.size() {
|
|
grow(new - l.size());
|
|
} else {
|
|
CUR.fetch_sub(l.size() - new, Relaxed);
|
|
}
|
|
unsafe { System.realloc(p, l, new) }
|
|
}
|
|
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
|
|
CUR.fetch_sub(l.size(), Relaxed);
|
|
unsafe { System.dealloc(p, l) }
|
|
}
|
|
}
|
|
|
|
#[global_allocator]
|
|
static ALLOC: Count = Count;
|
|
|
|
/// The calling thread's CPU time: on a loaded host wall-clock time grows with the load,
|
|
/// the thread's own CPU time only with its work.
|
|
#[cfg(unix)]
|
|
fn now() -> Duration {
|
|
#[repr(C)]
|
|
struct Timespec {
|
|
tv_sec: i64,
|
|
tv_nsec: i64,
|
|
}
|
|
unsafe extern "C" {
|
|
fn clock_gettime(clock: i32, tp: *mut Timespec) -> i32;
|
|
}
|
|
#[cfg(target_vendor = "apple")]
|
|
const CLOCK_THREAD_CPUTIME_ID: i32 = 16;
|
|
#[cfg(not(target_vendor = "apple"))]
|
|
const CLOCK_THREAD_CPUTIME_ID: i32 = 3;
|
|
let mut ts = Timespec { tv_sec: 0, tv_nsec: 0 };
|
|
assert_eq!(unsafe { clock_gettime(CLOCK_THREAD_CPUTIME_ID, &mut ts) }, 0, "clock_gettime");
|
|
Duration::new(ts.tv_sec as u64, ts.tv_nsec as u32)
|
|
}
|
|
|
|
/// Elsewhere, wall-clock time from a fixed origin.
|
|
#[cfg(not(unix))]
|
|
fn now() -> Duration {
|
|
static ORIGIN: std::sync::OnceLock<std::time::Instant> = std::sync::OnceLock::new();
|
|
ORIGIN.get_or_init(std::time::Instant::now).elapsed()
|
|
}
|
|
|
|
/// The most bytes live at once beyond the start, while `f` runs.
|
|
fn peak<T>(f: impl FnOnce() -> T) -> (T, usize) {
|
|
let start = CUR.load(Relaxed);
|
|
PEAK.store(start, Relaxed);
|
|
let out = f();
|
|
(out, PEAK.load(Relaxed) - start)
|
|
}
|
|
|
|
/// The CPU time of the best of three runs of `f`.
|
|
fn cpu(f: impl Fn()) -> Duration {
|
|
(0..3).map(|_| {
|
|
let t = now();
|
|
f();
|
|
now() - t
|
|
}).min().expect("three runs")
|
|
}
|
|
|
|
/// `n` Representations, each a timeline of one `S` of 2^20 segments.
|
|
fn bomb(n: usize) -> String {
|
|
bomb_of(n, 1 << 20)
|
|
}
|
|
|
|
/// `n` Representations, each a timeline of one `S` of `segments` segments.
|
|
fn bomb_of(n: usize, segments: u64) -> String {
|
|
let r = segments - 1;
|
|
let reps: String = (0..n)
|
|
.map(|i| format!("<Representation id=\"r{i}\" bandwidth=\"1\"><SegmentList timescale=\"1\"><SegmentTimeline><S t=\"0\" d=\"1\" r=\"{r}\"/></SegmentTimeline></SegmentList></Representation>"))
|
|
.collect();
|
|
format!("<MPD><Period id=\"p\"><AdaptationSet>{reps}</AdaptationSet></Period></MPD>")
|
|
}
|
|
|
|
/// One element of `n` attributes.
|
|
fn wide(n: usize) -> String {
|
|
let attrs: String = (0..n).map(|i| format!(" a{i}=\"\"")).collect();
|
|
format!("<MPD{attrs}/>")
|
|
}
|
|
|
|
#[test]
|
|
fn an_mpd_is_held_as_written_and_read_in_linear_time() {
|
|
// 32 timelines of 2^20 segments in about 5 KB: an expanded reader holds 2^25
|
|
// segments (hundreds of megabytes); this one, a run each.
|
|
for n in [1usize, 8, 32] {
|
|
let text = bomb(n);
|
|
let (periods, held) = peak(|| read_mpd(&text).unwrap());
|
|
let tls = &periods[0].sets[0].timelines;
|
|
assert_eq!(tls.len(), n);
|
|
assert!(tls.iter().all(|t| t.runs.len() == 1 && t.segment_count() == 1 << 20));
|
|
let bound = 64 * text.len() + (1 << 20);
|
|
assert!(held <= bound, "{n} timelines in {} bytes held {held} bytes (bound {bound})", text.len());
|
|
// Compared with themselves, each of 32 WAI timelines against 32 classical ones.
|
|
let (adjacent, held) = peak(|| check_dash_adjacent(&text, &text));
|
|
adjacent.unwrap();
|
|
assert!(held <= 2 * bound, "comparing {n} timelines held {held} bytes");
|
|
}
|
|
// The comparison is run by run: 32 timelines of 2^20 segments each, compared with
|
|
// 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()));
|
|
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))));
|
|
assert!(t_long <= t_short * 8 + Duration::from_millis(20), "40000 attributes took {t_long:?}, 10000 took {t_short:?}");
|
|
}
|