use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use blue_lang_runtime::alloc::{allocations, Allocations, Counting};
use tatara_lisp_eval::observe::{Call, CallObserver, Exit};
use tatara_lisp_eval::Value;
#[global_allocator]
static ALLOC: Counting = Counting;
fn now() -> Allocations {
allocations().expect("this binary installs the counting allocator")
}
#[test]
fn a_known_allocation_is_counted_exactly() {
let before = now();
let v: Vec<u64> = Vec::with_capacity(100);
let after = now();
drop(v);
let freed = now();
assert_eq!(
after.since(before),
Allocations {
count: 1,
bytes: 800,
freed: 0
}
);
assert_eq!(freed.since(after).freed, 800, "dropping it returns all 800");
assert_eq!(freed.since(before).retained(), 0, "nothing left live");
}
#[test]
fn another_threads_allocations_are_not_charged_here() {
let before = now();
let other = std::thread::spawn(|| {
let start = now();
let v: Vec<Vec<u8>> = (0..1000).map(|_| vec![0u8; 64]).collect();
drop(v);
now().since(start)
})
.join()
.expect("the other thread runs");
let here = now().since(before);
assert!(
other.count >= 1000,
"the other thread counted its own: {other:?}"
);
assert!(
here.count < 100,
"spawning and joining costs a handful here, never the other thread's 1000: {here:?}"
);
}
#[derive(Default)]
struct PerDefinition {
stack: Mutex<Vec<Allocations>>,
inclusive: Mutex<HashMap<String, Allocations>>,
}
impl CallObserver for PerDefinition {
fn enter(&self, _call: &Call<'_>, _args: &[Value]) -> Option<Value> {
let mut stack = self.stack.lock().unwrap();
stack.reserve(1);
stack.push(now());
None
}
fn leave(&self, call: &Call<'_>, _exit: Exit<'_>) {
let end = now();
let start = self.stack.lock().unwrap().pop().expect("balanced");
let name = call
.name
.map_or_else(|| "fn".to_string(), |n| n.to_string());
*self.inclusive.lock().unwrap().entry(name).or_default() += end.since(start);
}
}
#[test]
fn each_definition_is_charged_its_own_allocations() {
let mut host = ();
let mut interp = blue_lang_runtime::interpreter(&mut host);
let observer = Arc::new(PerDefinition::default());
interp.set_call_observer(Some(observer.clone()));
let src = "(define (small x) (+ x 1)) \
(define (grow n) (range 0 n)) \
(small 1) (grow 2000) (small 2)";
let forms = tatara_lisp_eval::read_spanned(src).expect("reads");
interp.eval_program(&forms, &mut host).expect("runs");
let inclusive = observer.inclusive.lock().unwrap().clone();
let small = inclusive["small"];
let grow = inclusive["grow"];
eprintln!("small (2 calls): {small:?}; grow 2000: {grow:?}");
assert!(
grow.bytes >= 2000 * 48,
"a 2000-element list is at least 2000 values of 48 bytes: {grow:?}"
);
assert!(
small.bytes * 20 < grow.bytes,
"an addition must not be charged for the list another definition built: \
small {small:?}, grow {grow:?}"
);
}