makepad/platform/script/tests/loop_heap_steady.rs

168 lines
5.6 KiB
Rust

//! A Splash VM called over and over keeps a flat heap. Loop scopes do not pin the
//! scope they run in, a `for` over a range literal frees the range, and what does
//! escape is collected by the host's GC cadence (the event loop's
//! `needs_gc` -> `gc`). Before, every loop run left its call scope (or outer
//! iteration scope) and its range object behind, and a long-running eval loop
//! filled the 2^24-slot object table ("GenVec<ScriptObjectData> is full").
use makepad_script::*;
fn test_vm() -> ScriptVm<'static> {
let host = Box::leak(Box::new(ScriptVmHost::new(0i32, ())));
let mut vm = ScriptVm { host: host.host_context(), bx: Box::new(ScriptVmBase::new()) };
vm.capture_errors();
vm
}
/// Evaluates `code` (ending in a fn) and roots the fn.
fn entry(vm: &mut ScriptVm, code: &str) -> (ScriptValue, ScriptObjectRef) {
let f = vm.eval(ScriptMod::new("", "", "loop_heap_steady.splash", 0, 0, code, &[]));
let errors = vm.take_errors();
assert!(errors.is_empty(), "{errors:?}");
let obj = f.as_object().expect("the script returns its entry fn");
let root = vm.bx.heap.new_object_ref(obj);
(f, root)
}
fn object_slots(vm: &ScriptVm) -> u64 {
vm.heap().heap_stat(HEAP_STAT_OBJECTS_LEN)
}
/// No GC at all: loops free their own scopes and range literals, so the object
/// table never grows. 9000 calls x 1000 outer iterations: before, each outer
/// iteration left two objects (the inner range and the pinned outer iteration
/// scope), 18M in all, past the table's 2^24 slots.
#[test]
fn loops_free_their_scopes_and_ranges_without_gc() {
let mut vm = test_vm();
let (f, _root) = entry(
&mut vm,
"let f = fn() { let c = 0 for i in 0..1000 { for j in 0..2 { c += j } } let k = 0 while k < 3 { for q in 0..2 { k += 1 } } return c + k }\nf",
);
let mut warm = 0;
for n in 0..9000 {
let v = vm.call(f, &[]);
assert_eq!(v.as_number(), Some(1004.0));
if n == 10 {
warm = object_slots(&vm);
}
}
assert_eq!(object_slots(&vm), warm, "the object table grew without GC: loops leaked objects");
}
/// The rwbench splash_eval workload under the host GC cadence: the live heap
/// is the same after 300 calls as after 20, and the table stays small.
#[test]
fn repeated_eval_reaches_a_steady_heap() {
let mut vm = test_vm();
let (f, _root) = entry(&mut vm, BENCH);
let mut live_warm = 0;
let mut slots_warm = 0;
for n in 0..300 {
let v = vm.call(f, &[]);
assert!(v.as_number().is_some());
if vm.heap().needs_gc() {
vm.gc();
}
if n == 20 {
vm.gc();
live_warm = vm.heap().gc_live_len();
slots_warm = object_slots(&vm);
}
}
vm.gc();
assert_eq!(vm.heap().gc_live_len(), live_warm, "live heap grew across calls");
assert!(object_slots(&vm) <= slots_warm.max(4096), "object table grew: {} slots", object_slots(&vm));
}
/// What escapes a loop keeps what it reads: closures made in nested loop bodies
/// (reading the loop variables, a body `let` and the call scope) and a loop
/// scope handed out with `scope` stay valid after the call returns, across GC.
#[test]
fn escaped_loop_scopes_keep_their_parents() {
let mut vm = test_vm();
let (f, _root) = entry(
&mut vm,
"let mk = fn() { let fs = [] let base = 10 for i in 0..3 { for j in 0..2 { let k = i * 2 fs.push(fn() { return base + k + i + j }) } } return fs }
let sc = fn() { for i in 0..2 { let q = 5 + i return scope } }
let run = fn() { let fs = mk() let s = sc() let t = 0 for f in fs { t += f() } return t + s.q + s.i }
run",
);
for _ in 0..5 {
assert_eq!(vm.call(f, &[]).as_number(), Some(86.0));
vm.gc();
}
assert!(vm.take_errors().is_empty());
}
/// A range that is not a literal `for` source is not freed by the loop: the
/// same range object iterates twice through a fn argument and is read after.
#[test]
fn a_shared_range_survives_its_loops() {
let mut vm = test_vm();
let (f, _root) = entry(
&mut vm,
"let walk = fn(r) { let t = 0 for i in r { t += i } for i in r { t += i } return t + r.end }
let pick = fn(c) { let r = 2..4 for i in if c { r } else { 0..1 } { } return r.start }
fn() { return walk(0..4) + pick(true) }",
);
for _ in 0..3 {
assert_eq!(vm.call(f, &[]).as_number(), Some(18.0));
vm.gc();
}
assert!(vm.take_errors().is_empty());
}
const BENCH: &str = r#"
let fib = fn(n) {
if n < 2 { return n }
return fib(n - 1) + fib(n - 2)
}
let sieve = fn(n) {
let flags = []
for i in 0..n { flags.push(1) }
let count = 0
for i in 2..n {
if flags[i] == 1 {
count += 1
let j = i * 2
for k in 0..n {
if j >= n { break }
flags[j] = 0
j += i
}
}
}
return count
}
let bodies = fn(steps) {
let ps = []
for i in 0..5 { ps.push({x: i * 1.5, y: i * 0.5 - 1.0, vx: 0.0, vy: 0.1 * i}) }
for s in 0..steps {
for i in 0..5 {
let a = ps[i]
for j in 0..5 {
if i != j {
let b = ps[j]
let dx = b.x - a.x
let dy = b.y - a.y
let d2 = dx * dx + dy * dy + 0.01
a.vx += dx / d2 * 0.001
a.vy += dy / d2 * 0.001
}
}
}
for i in 0..5 {
let a = ps[i]
a.x += a.vx
a.y += a.vy
}
}
let e = 0.0
for i in 0..5 { e += ps[i].x * 3.0 + ps[i].y }
return e
}
fn() {
return fib(12) * 1000000.0 + sieve(500) * 1000.0 + bodies(10)
}
"#;