use std::alloc::{GlobalAlloc, Layout, System};
use std::hint::black_box;
use std::sync::atomic::{AtomicU64, Ordering};
use graph_explorer_style::{Animator, Easing, Frame, HaloEmphasis, Scene, SceneEdge, SceneHalo, SceneNode};
struct Counting;
static ALLOCS: AtomicU64 = AtomicU64::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(l) }
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) { unsafe { System.dealloc(p, l) } }
unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.realloc(p, l, n) }
}
}
#[global_allocator]
static A: Counting = Counting;
fn build_scene(n: u32) -> Scene {
let mut s = Scene::default();
for i in 0..n {
s.set(i, SceneNode {
pos: [(i % 100) as f32 * 10.0, (i / 100) as f32 * 10.0],
radius: 5.0, color: [0.5; 4], opacity: 1.0, shape: 0, label: 0,
});
}
for i in 0..n.saturating_sub(1) {
s.edges.push(SceneEdge { a: i, b: i + 1, color: [1.0; 4], width: 1.0, opacity: 1.0 });
}
for i in 0..n.min(8) {
s.halos.push(SceneHalo { node: i, emphasis: if i == 0 { HaloEmphasis::Primary } else { HaloEmphasis::Secondary } });
}
s
}
#[test]
fn steady_state_allocates_nothing() {
const N: u32 = 2000;
let mut a = Animator::new();
let mut f = Frame::default();
a.set_target(build_scene(N), 0.0, 0.0, Easing::Linear, false);
for t in 0..60 { a.tick_into(t as f64 * 16.7, &mut f); black_box(&f); } let before = ALLOCS.load(Ordering::SeqCst);
for t in 60..660 { a.tick_into(t as f64 * 16.7, &mut f); black_box(&f); }
assert_eq!(ALLOCS.load(Ordering::SeqCst) - before, 0, "at-rest ticks allocated");
a.set_target(build_scene(N), 0.0, 1_000_000.0, Easing::EaseInOut, true);
for t in 0..60 { a.tick_into(t as f64 * 16.7, &mut f); black_box(&f); }
let before = ALLOCS.load(Ordering::SeqCst);
for t in 60..660 { a.tick_into(t as f64 * 16.7, &mut f); black_box(&f); }
assert_eq!(ALLOCS.load(Ordering::SeqCst) - before, 0, "mid-transition ticks allocated");
let over: Vec<Option<[f32; 2]>> = (0..N)
.map(|i| if i % 7 == 0 { None } else { Some([i as f32, -(i as f32)]) })
.collect();
for t in 0..60 { a.tick_positioned_into(t as f64 * 16.7, &over, &mut f); black_box(&f); }
black_box(&over);
let before = ALLOCS.load(Ordering::SeqCst);
for t in 60..660 { a.tick_positioned_into(t as f64 * 16.7, &over, &mut f); black_box(&f); }
black_box(&over);
assert_eq!(ALLOCS.load(Ordering::SeqCst) - before, 0, "override ticks allocated");
let t2 = build_scene(N);
let before = ALLOCS.load(Ordering::SeqCst);
a.set_target(t2, 20_000.0, 100.0, Easing::Linear, true);
assert_eq!(ALLOCS.load(Ordering::SeqCst) - before, 0, "warm retarget allocated");
a.tick_into(20_001.0, &mut f);
black_box(&f);
let before = ALLOCS.load(Ordering::SeqCst);
for t in 2..95 { a.tick_into(20_000.0 + t as f64, &mut f); black_box(&f); }
assert_eq!(ALLOCS.load(Ordering::SeqCst) - before, 0, "post-retarget in-flight ticks allocated");
let before = ALLOCS.load(Ordering::SeqCst);
for t in 95..105 { a.tick_into(20_000.0 + t as f64, &mut f); black_box(&f); }
assert_eq!(ALLOCS.load(Ordering::SeqCst) - before, 0, "transition-commit tick allocated");
let before = ALLOCS.load(Ordering::SeqCst);
for t in 105..300 { a.tick_into(20_000.0 + t as f64, &mut f); black_box(&f); }
assert_eq!(ALLOCS.load(Ordering::SeqCst) - before, 0, "post-commit at-rest ticks allocated");
}