use std::alloc::{GlobalAlloc, Layout, System};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use hound::WavReader;
use lac::{decode_frame, encode_frame};
const CORPUS_DIR: &str = "corpus";
struct TrackingAllocator;
static MEASUREMENT_LOCK: Mutex<()> = Mutex::new(());
static CURRENT_BYTES: AtomicUsize = AtomicUsize::new(0);
static PEAK_BYTES: AtomicUsize = AtomicUsize::new(0);
static CALL_COUNT: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = unsafe { System.alloc(layout) };
if !ptr.is_null() {
let new = CURRENT_BYTES.fetch_add(layout.size(), Ordering::Relaxed) + layout.size();
PEAK_BYTES.fetch_max(new, Ordering::Relaxed);
CALL_COUNT.fetch_add(1, Ordering::Relaxed);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) };
CURRENT_BYTES.fetch_sub(layout.size(), Ordering::Relaxed);
}
}
#[global_allocator]
static ALLOC: TrackingAllocator = TrackingAllocator;
fn reset_peak() {
PEAK_BYTES.store(CURRENT_BYTES.load(Ordering::Relaxed), Ordering::Relaxed);
CALL_COUNT.store(0, Ordering::Relaxed);
}
fn peak_delta_since_reset() -> usize {
let peak = PEAK_BYTES.load(Ordering::Relaxed);
let baseline = CURRENT_BYTES.load(Ordering::Relaxed);
peak.saturating_sub(baseline)
}
fn call_count_since_reset() -> usize {
CALL_COUNT.load(Ordering::Relaxed)
}
fn corpus(name: &str) -> PathBuf {
Path::new(CORPUS_DIR).join(name)
}
fn load_mono(path: &Path) -> Option<Vec<i32>> {
let mut reader = WavReader::open(path).ok()?;
let spec = reader.spec();
if spec.sample_format != hound::SampleFormat::Int
|| spec.channels != 1
|| spec.bits_per_sample > 24
{
return None;
}
let samples: Result<Vec<i32>, _> = reader.samples::<i32>().collect();
samples.ok()
}
macro_rules! require {
($path:expr) => {
if !$path.exists() {
eprintln!("skipping: corpus file not found: {}", $path.display());
return;
}
};
}
struct Dist {
count: usize,
p50: Duration,
p95: Duration,
p99: Duration,
max: Duration,
mean: Duration,
}
fn dist_of(mut samples: Vec<Duration>) -> Dist {
samples.sort();
let count = samples.len();
let p = |frac: f64| samples[((count as f64 - 1.0) * frac).round() as usize];
let total: Duration = samples.iter().sum();
Dist {
count,
p50: p(0.50),
p95: p(0.95),
p99: p(0.99),
max: *samples.last().unwrap(),
mean: total / count as u32,
}
}
fn fmt_us(d: Duration) -> String {
format!("{:.1}µs", d.as_nanos() as f64 / 1000.0)
}
fn run_latency(name: &str, samples: &[i32], frame_size: usize, sample_rate: u32) {
let _guard = MEASUREMENT_LOCK
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let frames: Vec<&[i32]> = samples.chunks_exact(frame_size).collect();
let warmup = 32.min(frames.len());
for f in &frames[..warmup] {
std::hint::black_box(encode_frame(f));
}
let mut encode_times = Vec::with_capacity(frames.len() - warmup);
reset_peak();
let mut peak_encode_bytes = 0usize;
let mut peak_encode_allocs = 0usize;
let mut encoded_bytes_total = 0usize;
let mut encoded_frames: Vec<Vec<u8>> = Vec::with_capacity(frames.len() - warmup);
for f in &frames[warmup..] {
reset_peak();
let t = Instant::now();
let encoded = encode_frame(f);
encode_times.push(t.elapsed());
encoded_bytes_total += encoded.len();
peak_encode_bytes = peak_encode_bytes.max(peak_delta_since_reset());
peak_encode_allocs = peak_encode_allocs.max(call_count_since_reset());
encoded_frames.push(encoded);
}
let mut decode_times = Vec::with_capacity(encoded_frames.len());
let mut peak_decode_bytes = 0usize;
let mut peak_decode_allocs = 0usize;
for ef in &encoded_frames {
reset_peak();
let t = Instant::now();
let _samples = decode_frame(ef).expect("decode");
decode_times.push(t.elapsed());
peak_decode_bytes = peak_decode_bytes.max(peak_delta_since_reset());
peak_decode_allocs = peak_decode_allocs.max(call_count_since_reset());
}
let enc = dist_of(encode_times);
let dec = dist_of(decode_times);
let frame_period =
Duration::from_nanos((frame_size as u64 * 1_000_000_000) / sample_rate as u64);
eprintln!();
eprintln!("== {name} ({frame_size}-sample frames @ {sample_rate} Hz) ==");
eprintln!(
" encode latency: p50={} p95={} p99={} max={} mean={}",
fmt_us(enc.p50),
fmt_us(enc.p95),
fmt_us(enc.p99),
fmt_us(enc.max),
fmt_us(enc.mean)
);
eprintln!(
" headroom at p99 vs frame period ({:.1}ms): {:.1}×",
frame_period.as_micros() as f64 / 1000.0,
frame_period.as_nanos() as f64 / enc.p99.as_nanos() as f64,
);
eprintln!(
" decode latency: p50={} p95={} p99={} max={} mean={}",
fmt_us(dec.p50),
fmt_us(dec.p95),
fmt_us(dec.p99),
fmt_us(dec.max),
fmt_us(dec.mean)
);
eprintln!(
" peak heap / frame: encode={}B decode={}B",
peak_encode_bytes, peak_decode_bytes
);
eprintln!(
" peak allocs / frame: encode={} decode={}",
peak_encode_allocs, peak_decode_allocs
);
eprintln!(
" throughput: encoded_frames={} total_encoded_bytes={} avg_bytes/frame={:.1}",
enc.count,
encoded_bytes_total,
encoded_bytes_total as f64 / enc.count as f64
);
assert!(
enc.p99 < frame_period,
"encode P99 {} exceeds frame period {} — real-time deadline missed",
fmt_us(enc.p99),
fmt_us(frame_period),
);
assert!(
dec.p99 < frame_period,
"decode P99 {} exceeds frame period {} — real-time deadline missed",
fmt_us(dec.p99),
fmt_us(frame_period),
);
}
#[test]
fn latency_headset_speech_320() {
let path = corpus("ES2002a.Headset-0.wav");
require!(path);
let samples = load_mono(&path).expect("load");
let cap = (16_000 * 60).min(samples.len());
run_latency("headset_speech", &samples[..cap], 320, 16_000);
}
#[test]
fn latency_headset_speech_160() {
let path = corpus("ES2002a.Headset-0.wav");
require!(path);
let samples = load_mono(&path).expect("load");
let cap = (16_000 * 60).min(samples.len());
run_latency("headset_speech_10ms", &samples[..cap], 160, 16_000);
}
#[test]
fn latency_headset_speech_480() {
let path = corpus("ES2002a.Headset-0.wav");
require!(path);
let samples = load_mono(&path).expect("load");
let cap = (16_000 * 60).min(samples.len());
run_latency("headset_speech_480", &samples[..cap], 480, 16_000);
}
#[test]
fn latency_headset_speech_prime() {
let path = corpus("ES2002a.Headset-0.wav");
require!(path);
let samples = load_mono(&path).expect("load");
let cap = (16_000 * 60).min(samples.len());
run_latency("headset_speech_prime503", &samples[..cap], 503, 16_000);
}
#[test]
fn latency_mixed_meeting_320() {
let path = corpus("ES2002a.Mix-Headset.wav");
require!(path);
let samples = load_mono(&path).expect("load");
let cap = (16_000 * 60).min(samples.len());
run_latency("mixed_meeting", &samples[..cap], 320, 16_000);
}
#[test]
fn latency_array_speech_320() {
let path = corpus("ES2002a.Array1-01.wav");
require!(path);
let samples = load_mono(&path).expect("load");
let cap = (16_000 * 60).min(samples.len());
run_latency("array_speech", &samples[..cap], 320, 16_000);
}