use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use abstracttui::base::{Rgba, Size};
use abstracttui::render::{Cell as RenderCell, FrameDiff, PresentCaps, Presenter, Style, Surface};
use abstracttui::testing::VtScreen;
thread_local! {
static TL_ALLOCS: Cell<u64> = const { Cell::new(0) };
static TL_REALLOCS: Cell<u64> = const { Cell::new(0) };
static TL_BYTES: Cell<u64> = const { Cell::new(0) };
}
struct CountingAlloc;
impl CountingAlloc {
fn snapshot(&self) -> (u64, u64, u64) {
(
TL_ALLOCS.try_with(Cell::get).unwrap_or(0),
TL_REALLOCS.try_with(Cell::get).unwrap_or(0),
TL_BYTES.try_with(Cell::get).unwrap_or(0),
)
}
}
unsafe impl GlobalAlloc for CountingAlloc {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let _ = TL_ALLOCS.try_with(|c| c.set(c.get() + 1));
let _ = TL_BYTES.try_with(|c| c.set(c.get() + layout.size() as u64));
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout)
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let _ = TL_REALLOCS.try_with(|c| c.set(c.get() + 1));
let _ =
TL_BYTES.try_with(|c| c.set(c.get() + new_size.saturating_sub(layout.size()) as u64));
System.realloc(ptr, layout, new_size)
}
}
#[global_allocator]
static ALLOC: CountingAlloc = CountingAlloc;
static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn serial() -> std::sync::MutexGuard<'static, ()> {
SERIAL.lock().unwrap_or_else(|poison| poison.into_inner())
}
fn alloc_delta(f: impl FnOnce()) -> (u64, u64, u64) {
let before = ALLOC.snapshot();
f();
let after = ALLOC.snapshot();
(after.0 - before.0, after.1 - before.1, after.2 - before.2)
}
#[test]
fn allocator_counts_and_measures_deltas() {
let _serial = serial();
let (a, _, bytes) = alloc_delta(|| {
let v: Vec<u64> = Vec::with_capacity(100);
std::hint::black_box(&v);
});
assert!(a >= 1, "one Vec allocation must be visible");
assert!(bytes >= 800, "100 u64s = at least 800 bytes, saw {bytes}");
let (a2, r2, _) = alloc_delta(|| {
std::hint::black_box(42u64);
});
assert_eq!((a2, r2), (0, 0), "an empty region must measure zero");
}
fn styled_frame(size: Size, tick: u8) -> Surface {
let mut s = Surface::new(size, RenderCell::default());
for y in 0..size.h {
let style = Style::new().fg(Rgba::rgb(tick, (y * 4) as u8, 255 - tick));
for chunk in 0..(size.w / 20).max(1) {
s.draw_text(chunk * 20, y, "abcdefghij0123456789", style);
}
}
s
}
#[test]
fn diff_present_steady_state_allocates_nothing() {
let _serial = serial();
let (d, p) = measure_stages();
assert_eq!(
(d.0, d.1),
(0, 0),
"steady-state DIFF allocated: {} allocs / {} reallocs / {} bytes",
d.0,
d.1,
d.2
);
assert_eq!(
(p.0, p.1),
(0, 0),
"steady-state PRESENT allocated: {} allocs / {} reallocs / {} bytes",
p.0,
p.1,
p.2
);
}
#[test]
fn diff_present_allocation_attribution_ratchet() {
let _serial = serial();
let (d, p) = measure_stages();
eprintln!(
"alloc attribution: diff = {} allocs/{} reallocs/{} B; present = {} allocs/{} reallocs/{} B",
d.0, d.1, d.2, p.0, p.1, p.2
);
assert!(d.0 <= 8_000, "diff allocation REGRESSED: {} allocs", d.0);
assert!(p.0 <= 2_000, "present allocation REGRESSED: {} allocs", p.0);
}
fn measure_stages() -> ((u64, u64, u64), (u64, u64, u64)) {
let size = Size::new(200, 60);
let caps = PresentCaps::FULL;
let mut diff = FrameDiff::new();
let mut presenter = Presenter::new();
let mut out: Vec<u8> = Vec::new();
let frames: Vec<Surface> = (0..6).map(|i| styled_frame(size, i * 40)).collect();
for w in [0usize, 1, 2] {
let runs = diff.compute_full(&frames[w], &frames[w + 1]);
out.clear();
presenter.emit(runs, &frames[w + 1], &caps, &mut out);
}
let prev = &frames[3];
let next = &frames[4];
let mut d = (0, 0, 0);
let mut runs_len = 0;
let d1 = alloc_delta(|| {
let runs = diff.compute_full(prev, next);
runs_len = runs.len();
});
d.0 += d1.0;
d.1 += d1.1;
d.2 += d1.2;
let runs = diff.compute_full(prev, next);
out.clear();
let p = alloc_delta(|| {
presenter.emit(runs, next, &caps, &mut out);
});
assert!(runs_len > 0, "the measured frames really did change");
(d, p)
}
#[test]
fn presenter_no_change_frame_emits_and_allocates_nothing() {
let _serial = serial();
let size = Size::new(80, 24);
let frame = styled_frame(size, 7);
let caps = PresentCaps::FULL;
let mut diff = FrameDiff::new();
let mut presenter = Presenter::new();
let mut out: Vec<u8> = Vec::new();
let runs = diff.compute_full(&frame, &frame);
presenter.emit(runs, &frame, &caps, &mut out);
out.clear();
let (allocs, reallocs, _) = alloc_delta(|| {
let runs = diff.compute_full(&frame, &frame);
presenter.emit(runs, &frame, &caps, &mut out);
});
assert_eq!((allocs, reallocs), (0, 0), "identical frames must be free");
assert!(out.is_empty(), "identical frames must emit zero bytes");
}
#[test]
fn vt_model_feed_allocation_is_bounded() {
let _serial = serial();
let mut screen = VtScreen::new(Size::new(200, 60));
let mut frame_bytes = Vec::new();
for y in 1..=60 {
frame_bytes.extend_from_slice(format!("\x1b[{y};1H\x1b[38;2;1;2;3m").as_bytes());
frame_bytes.extend_from_slice("x".repeat(200).as_bytes());
}
screen.feed(&frame_bytes); let (allocs, _, bytes) = alloc_delta(|| {
screen.feed(&frame_bytes);
});
let cells = 200 * 60;
assert!(
allocs <= 2 * cells,
"VT model allocation blew up: {allocs} allocs / {bytes} bytes for {cells} cells"
);
}
#[test]
fn jpeg_dimension_bomb_allocates_within_budget() {
let _serial = serial();
use abstracttui::gfx::jpeg;
use abstracttui::testing::jpeg_build::FlatJpeg;
let mut bytes = FlatJpeg::grayscale(16, 16).build();
let sof = bytes
.windows(2)
.position(|w| w[0] == 0xFF && (w[1] == 0xC0 || w[1] == 0xC1))
.expect("SOF present");
for i in 0..4 {
bytes[sof + 5 + i] = 0xFF;
}
let (allocs, _, alloc_bytes) = alloc_delta(|| {
let r = jpeg::decode(&bytes);
assert!(r.is_err(), "dimension bomb must be rejected");
std::hint::black_box(&r);
});
assert!(
alloc_bytes < 64 * 1024,
"dimension-bomb rejection allocated {alloc_bytes} bytes in {allocs} allocs — guard fired too late"
);
}
#[test]
fn gltf_animation_sampling_is_allocation_free_per_frame() {
let _serial = serial();
use abstracttui::three::animation::{Animation, Interpolation, NodePose, Track, TrackValues};
use abstracttui::three::Vec3;
let mut tracks = Vec::new();
for node in 0..4 {
let times: Vec<f32> = (0..8).map(|k| k as f32 * 0.5).collect();
tracks.push(Track {
node,
times: times.clone(),
values: TrackValues::Translation(
(0..8).map(|k| [k as f32, node as f32, 0.0]).collect(),
),
interpolation: Interpolation::Linear,
});
tracks.push(Track {
node,
times: times.clone(),
values: TrackValues::Rotation((0..8).map(|_| [0.0, 0.0, 0.0, 1.0]).collect()),
interpolation: Interpolation::Linear,
});
}
let anim = Animation::new(None, tracks);
let rest = NodePose {
translation: Vec3::ZERO,
rotation: [0.0, 0.0, 0.0, 1.0],
scale: Vec3::new(1.0, 1.0, 1.0),
};
let mut poses = vec![rest; 4];
anim.sample(1.0, &mut poses);
let (allocs, reallocs, _) = alloc_delta(|| {
for i in 0..240 {
let t = (i as f32) * 0.01;
anim.sample(t, &mut poses);
}
});
assert_eq!(
(allocs, reallocs),
(0, 0),
"animation sampling allocated on the hot path: {allocs} allocs, {reallocs} reallocs over 240 frames"
);
}
#[test]
fn jpeg_hostile_corpus_allocation_is_bounded() {
let _serial = serial();
use abstracttui::gfx::jpeg;
use abstracttui::testing::jpeg_build::FlatJpeg;
let base = FlatJpeg::grayscale(16, 16).with_flat_code_len(16).build();
let (allocs, _, alloc_bytes) = alloc_delta(|| {
for cut in (2..base.len()).step_by(3) {
let _ = std::hint::black_box(jpeg::decode(&base[..cut]));
}
});
let attempts = (base.len() - 2).div_ceil(3);
assert!(
alloc_bytes < attempts as u64 * 128 * 1024,
"hostile-corpus decode allocated {alloc_bytes} bytes over {attempts} attempts ({allocs} allocs)"
);
}