use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use wavekat_vad::{FrameAdapter, VadCapabilities, VadError, VoiceActivityDetector};
thread_local! {
static ALLOC_COUNT: Cell<usize> = const { Cell::new(0) };
static COUNTING: Cell<bool> = const { Cell::new(false) };
}
fn record_allocation() {
let _ = COUNTING.try_with(|counting| {
if counting.get() {
let _ = ALLOC_COUNT.try_with(|n| n.set(n.get() + 1));
}
});
}
struct CountingAllocator;
unsafe impl GlobalAlloc for CountingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
record_allocation();
unsafe { System.alloc(layout) }
}
unsafe fn alloc_zeroed(&self, layout: Layout) -> *mut u8 {
record_allocation();
unsafe { System.alloc_zeroed(layout) }
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
record_allocation();
unsafe { System.realloc(ptr, layout, new_size) }
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) }
}
}
#[global_allocator]
static GLOBAL: CountingAllocator = CountingAllocator;
fn count_allocs<R>(f: impl FnOnce() -> R) -> (R, usize) {
ALLOC_COUNT.with(|n| n.set(0));
COUNTING.with(|c| c.set(true));
let result = f();
COUNTING.with(|c| c.set(false));
let count = ALLOC_COUNT.with(|n| n.get());
(result, count)
}
struct MockVad {
frame_size: usize,
}
impl VoiceActivityDetector for MockVad {
fn capabilities(&self) -> VadCapabilities {
VadCapabilities {
sample_rate: 16_000,
frame_size: self.frame_size,
frame_duration_ms: (self.frame_size as u32 * 1000) / 16_000,
}
}
fn process(&mut self, samples: &[i16], _sample_rate: u32) -> Result<f32, VadError> {
assert_eq!(samples.len(), self.frame_size);
Ok(0.5)
}
fn reset(&mut self) {}
}
fn adapter(frame_size: usize) -> FrameAdapter {
FrameAdapter::new(Box::new(MockVad { frame_size }))
}
#[test]
fn counter_actually_observes_allocations() {
let (v, allocs) = count_allocs(|| Vec::<u8>::with_capacity(4096));
assert_eq!(v.capacity(), 4096);
assert!(
allocs >= 1,
"counting allocator saw {allocs} allocations for a 4096-byte Vec"
);
let (_, allocs) = count_allocs(|| {
let mut v: Vec<u64> = Vec::new();
for i in 0..10_000u64 {
v.push(i);
}
v
});
assert!(allocs >= 2, "growing a Vec should realloc; saw {allocs}");
let (sum, allocs) = count_allocs(|| (0u64..1000).sum::<u64>());
assert_eq!(sum, 499_500);
assert_eq!(allocs, 0, "arithmetic must not allocate");
}
#[test]
fn process_each_does_not_allocate_after_warm_up() {
let mut adapter = adapter(256);
let chunk = vec![0i16; 320];
for _ in 0..8 {
adapter.process_each(&chunk, 16_000, |_| {}).unwrap();
}
let (_, allocs) = count_allocs(|| {
for _ in 0..500 {
adapter.process_each(&chunk, 16_000, |_| {}).unwrap();
}
});
assert_eq!(allocs, 0, "process_each allocated {allocs} times");
}
#[test]
fn process_each_does_not_allocate_for_any_chunk_shape() {
let mut adapter = adapter(256);
let sizes = [0usize, 1, 7, 255, 256, 257, 320, 1024, 4096];
let audio = vec![0i16; 4096];
for _ in 0..4 {
for &n in &sizes {
adapter.process_each(&audio[..n], 16_000, |_| {}).unwrap();
}
}
let (_, allocs) = count_allocs(|| {
for _ in 0..20 {
for &n in &sizes {
adapter.process_each(&audio[..n], 16_000, |_| {}).unwrap();
}
}
});
assert_eq!(allocs, 0, "process_each allocated {allocs} times");
}
#[test]
fn process_latest_does_not_allocate() {
let mut adapter = adapter(256);
let chunk = vec![0i16; 320];
for _ in 0..8 {
adapter.process_latest(&chunk, 16_000).unwrap();
}
let (_, allocs) = count_allocs(|| {
for _ in 0..500 {
adapter.process_latest(&chunk, 16_000).unwrap();
}
});
assert_eq!(allocs, 0, "process_latest allocated {allocs} times");
}
#[test]
fn process_does_not_allocate() {
let mut adapter = adapter(256);
let chunk = vec![0i16; 1024];
for _ in 0..8 {
adapter.process(&chunk, 16_000).unwrap();
}
let (_, allocs) = count_allocs(|| {
for _ in 0..500 {
adapter.process(&chunk, 16_000).unwrap();
}
});
assert_eq!(allocs, 0, "process allocated {allocs} times");
}
#[test]
fn process_all_allocates_exactly_one_vec() {
let mut adapter = adapter(256);
let chunk = vec![0i16; 1024];
for _ in 0..8 {
adapter.process_all(&chunk, 16_000).unwrap();
}
let (scores, allocs) = count_allocs(|| adapter.process_all(&chunk, 16_000).unwrap());
assert_eq!(scores.len(), 4);
assert_eq!(
allocs, 1,
"process_all should allocate its result Vec exactly once, saw {allocs}"
);
}
#[cfg(feature = "earshot")]
mod earshot {
use super::*;
use wavekat_vad::backends::earshot::EarshotVad;
#[test]
fn earshot_process_does_not_allocate_per_frame() {
let mut vad = EarshotVad::new();
let frame = vec![0i16; 256];
for _ in 0..8 {
vad.process(&frame, 16_000).unwrap();
}
let (_, allocs) = count_allocs(|| {
for _ in 0..500 {
vad.process(&frame, 16_000).unwrap();
}
});
assert_eq!(allocs, 0, "EarshotVad::process allocated {allocs} times");
}
#[test]
fn earshot_process_latest_on_320_sample_chunks_does_not_allocate() {
let mut adapter = FrameAdapter::new(Box::new(EarshotVad::new()));
let chunk = vec![0i16; 320];
for _ in 0..8 {
adapter.process_latest(&chunk, 16_000).unwrap();
}
let (_, allocs) = count_allocs(|| {
for _ in 0..1000 {
let score = adapter.process_latest(&chunk, 16_000).unwrap();
assert!(score.is_finite());
}
});
assert_eq!(
allocs, 0,
"the 20 ms ingress hot path allocated {allocs} times"
);
}
#[test]
fn earshot_behind_the_adapter_does_not_allocate() {
let mut adapter = FrameAdapter::new(Box::new(EarshotVad::new()));
let chunk = vec![0i16; 320];
for _ in 0..8 {
adapter.process_each(&chunk, 16_000, |_| {}).unwrap();
}
let (_, allocs) = count_allocs(|| {
for _ in 0..500 {
adapter.process_each(&chunk, 16_000, |_| {}).unwrap();
}
});
assert_eq!(allocs, 0, "adapter + EarshotVad allocated {allocs} times");
}
}