//! Where a big addon data file's load cost goes, and what would take it away. //! //! `cargo run --release -p benilla-ui --example chunkbench -- …` runs three passes over //! the same files and prints each: //! //! 1. **fresh VM per file** — the floor: parse, execute, dump to 5.1 bytecode, re-compile that //! bytecode. A bytecode cache can only ever remove the *parse* column, so this says whether //! one is worth building. //! 2. **one VM, stock GC** — the real shape of a load, where every later file allocates against //! the heap the earlier ones left live and Lua's incremental collector walks all of it. //! 3. **one VM, collector stopped** for the whole load and collected once at the end — the same //! work with the GC's share of it named. //! //! Globals autovivify at every depth (`pfDB["items"]["data"] = {…}` reads two levels before it //! writes), so a database file runs outside its addon. use std::time::Instant; fn ms(t: Instant) -> f64 { t.elapsed().as_secs_f64() * 1000.0 } /// A VM whose global reads mint tables on demand, so an addon's data file needs no addon. fn vm() -> mlua::Lua { let lua = mlua::Lua::new(); lua.load( r#"local mt = {} mt.__index = function(t, k) local v = setmetatable({}, mt); rawset(t, k, v); return v end setmetatable(_G, mt)"#, ) .exec() .expect("the autovivify metatable"); lua } fn main() -> mlua::Result<()> { let files: Vec = std::env::args().skip(1).collect(); if files.is_empty() { eprintln!("usage: chunkbench …"); return Ok(()); } let sources: Vec<(String, Vec)> = files .iter() .map(|p| (p.clone(), std::fs::read(p).expect("the file"))) .collect(); let mib: f64 = sources.iter().map(|(_, s)| s.len() as f64).sum::() / (1024.0 * 1024.0); // 1 · a VM per file. println!( "{:>8} {:>9} {:>9} {:>9} {:>9} {:>9} {:>6} file", "MiB", "parse ms", "exec ms", "dump ms", "bc ms", "bc+dbg", "MiB" ); let mut tot = [0.0f64; 6]; // MiB, parse, exec, dump, bytecode-compile, with debug info for (path, src) in &sources { let lua = vm(); let t = Instant::now(); // A file the client never compiles as a chunk (a `#!` tool script, say) is counted and // skipped rather than ending the run. let Ok(f) = lua.load(&src[..]).set_name(path.as_str()).into_function() else { println!( "{:8.2} {:>49} {path}", src.len() as f64 / (1024.0 * 1024.0), "[did not parse]" ); continue; }; let parse = ms(t); // An addon file outside its addon often cannot run (it calls what the client installs); // the parse column is measured either way, and a file that raised is marked. let t = Instant::now(); let ran = f.call::<()>(()).is_ok(); let exec = if ran { ms(t) } else { 0.0 }; let t = Instant::now(); let bc = f.dump(true); let dump = ms(t); // The cache cannot strip: `debugstack` is an addon-facing verb, so the kept-debug-info // dump is the one that matters. Both are timed, because the difference decides whether a // cache can pay for itself at all. let bc_full = f.dump(false); drop(lua); let lua = vm(); let t = Instant::now(); lua.load(&bc_full[..]) .set_name(path.as_str()) .set_mode(mlua::ChunkMode::Binary) .into_function()?; let bcfull_load = ms(t); let t = Instant::now(); lua.load(&bc[..]) .set_name(path.as_str()) .set_mode(mlua::ChunkMode::Binary) .into_function()?; let bcload = ms(t); drop(lua); let fmib = src.len() as f64 / (1024.0 * 1024.0); println!( "{fmib:8.2} {parse:9.1} {exec:9.1} {dump:9.1} {bcload:9.1} {bcfull_load:9.1} {:7.2} {path}{}", bc_full.len() as f64 / (1024.0 * 1024.0), if ran { "" } else { " [did not run]" } ); for (slot, v) in tot .iter_mut() .zip([fmib, parse, exec, dump, bcload, bcfull_load]) { *slot += v; } } let [mib_sum, parse, exec, dump, bcload, bcfull] = tot; println!( "{mib_sum:8.2} {parse:9.1} {exec:9.1} {dump:9.1} {bcload:9.1} {bcfull:9.1} — a VM per file, {} files", files.len() ); println!( " parse+exec {:.0} ms; from a STRIPPED cache {:.0} ms; from one that KEEPS debug info \ (what `debugstack` needs) {:.0} ms\n", parse + exec, bcload + exec, bcfull + exec ); // 2 and 3 · one VM for the whole set, as a load does it. for stop_gc in [false, true] { let lua = vm(); if stop_gc { lua.gc_stop(); } let before = lua.used_memory(); let t0 = Instant::now(); let mut worst = (0.0f64, ""); for (path, src) in &sources { let t = Instant::now(); if let Ok(f) = lua.load(&src[..]).set_name(path.as_str()).into_function() { let _ = f.call::<()>(()); } let each = ms(t); if each > worst.0 { worst = (each, path); } } let load = ms(t0); let t = Instant::now(); lua.gc_collect()?; let collect = ms(t); let heap = (lua.used_memory() - before) as f64 / (1024.0 * 1024.0); println!( "one VM, GC {:7}: load {load:7.0} ms final collect {collect:6.0} ms \ heap {heap:6.1} MiB worst file {:.0} ms ({})", if stop_gc { "stopped" } else { "stock" }, worst.0, worst.1, ); drop(lua); } println!("{mib:.1} MiB of source in all"); Ok(()) }