makepad/libs/wasm_strip/examples/wasm_units.rs
Admin 9054bba68d wasm_strip units: table entries are pruned by a liveness mark -- a function is live when it is exported or the start function, a live function calls it, or live code or the data it reaches names its table slot: an i32.const that is not an operand of arithmetic, a comparison, a branch or a load address; the method slots of a vtable ([drop fn or 0, size, align, method...]) at an address live code or the data it reaches names, up to the next vtable header (code that loads a method from a known vtable names an address inside it); the first words of another named static (a thread local's key) up to the next address anything named before the strip. Every other entry points at a trapping stub of its type and dead-code elimination drops the function. Counting every word of live data named almost every slot (small numbers in the data) and a cycle (a closure's vtable named by the function that only that closure reaches) kept dead widgets alive. OptimizeOptions.prune_exported_table prunes a table the module exports for a host that only grows it (the film's kernel linker); data::DataLive::of_code leaves dead code's anchors out. Examples: wasm_units modules --prune-exported-table, wasm_units report (per unit: code bytes and functions a coverage run entered, matched by demangled name), wasm_why --root (the call chain from a function back to the table slot or export that keeps it). Checked: cargo test --release -p makepad-wasm-strip --lib (42) and --test fuzz stripped (OPT2's stripped_modules_replay_the_same); counter collect-run strip pixel-identical in headless Chrome incl. two clicks; the P(DOOM) film with its analysis census (42 of 119 script modules used): optimised 18.65 MB / 3.799 MB br -> 18.05 MB / 3.733 MB br, widgets_core code 993 -> 882 KB, plays the full 156 s in the safety gate the same as the unstripped build.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-01 13:45:53 +02:00

101 lines
5.6 KiB
Rust

//! The module-wise strip by hand:
//! - `wasm_units instrument <in.wasm> <out.wasm> [--phase-mark <name part>]...`
//! - `wasm_units coverage <instrumented.wasm> <coverage memory dump> <out.txt>`
//! - `wasm_units strip <in.wasm> <coverage.txt> <out.wasm> [--keep <unit>]... [--keep-names]`
//! - `wasm_units modules <in.wasm> <collect manifest dir> <out.wasm> [--keep <module>]... [--keep-names] [--prune-exported-table]`:
//! the registration strip from the manifest's `modules.txt` and
//! `modules-registered.txt`
use makepad_wasm_strip::*;
fn flag_values(args: &[String], flag: &str) -> Vec<String> {
args.windows(2).filter(|w| w[0] == flag).map(|w| w[1].clone()).collect()
}
fn main() {
let args: Vec<String> = std::env::args().collect();
let read = |p: &str| std::fs::read(p).unwrap_or_else(|e| panic!("{p}: {e}"));
match args.get(1).map(|s| s.as_str()) {
Some("instrument") => {
let out = wasm_instrument_coverage(&read(&args[2]), &flag_values(&args, "--phase-mark")).unwrap_or_else(|e| panic!("{e}"));
std::fs::write(&args[3], out).expect("write");
}
Some("coverage") => {
let cov = wasm_coverage(&read(&args[2]), &read(&args[3])).unwrap_or_else(|e| panic!("{e}"));
let used = cov.entered.values().filter(|p| **p >= 2).count();
println!("{} functions entered, {} after registration", cov.entered.len(), used);
std::fs::write(&args[4], cov.to_text()).expect("write");
}
Some("strip") => {
let coverage = Coverage::parse(&String::from_utf8(read(&args[3])).expect("utf8"));
let opts = OptimizeOptions {
coverage: Some(coverage),
keep_units: flag_values(&args, "--keep"),
keep_names: args.iter().any(|a| a == "--keep-names"),
prune_exported_table: args.iter().any(|a| a == "--prune-exported-table"),
..OptimizeOptions::default()
};
let start = std::time::Instant::now();
let (out, report) = wasm_optimize_checked(&read(&args[2]), &opts).unwrap_or_else(|e| panic!("{e}"));
print!("{}", report.units.clone().unwrap_or_default());
print!("{}", report.to_text());
println!("took {:?}", start.elapsed());
std::fs::write(&args[4], out).expect("write");
}
Some("modules") => {
let dir = std::path::Path::new(&args[3]);
let lines = |f: &str| -> std::collections::BTreeSet<String> {
std::fs::read_to_string(dir.join(f)).unwrap_or_else(|e| panic!("{f}: {e}")).lines().filter_map(|l| l.split('\t').next()).filter(|l| !l.is_empty()).map(String::from).collect()
};
let modules = ModuleUse { registered: lines("modules-registered.txt"), used: lines("modules.txt") };
let opts = OptimizeOptions {
modules: Some(modules),
keep_units: flag_values(&args, "--keep"),
keep_names: args.iter().any(|a| a == "--keep-names"),
prune_exported_table: args.iter().any(|a| a == "--prune-exported-table"),
..OptimizeOptions::default()
};
let (out, report) = wasm_optimize_checked(&read(&args[2]), &opts).unwrap_or_else(|e| panic!("{e}"));
print!("{}", report.units.clone().unwrap_or_default());
print!("{}", report.to_text());
std::fs::write(&args[4], out).expect("write");
}
Some("report") => {
// Per unit of a named module: code bytes, functions, and how many
// a coverage run (of a build with the same code, matched by
// demangled name) entered.
use makepad_wasm_strip::opt::demangle::demangle;
let module = makepad_wasm_strip::opt::ir::decode(&read(&args[2])).expect("decode");
let coverage = Coverage::parse(&String::from_utf8(read(&args[3])).expect("utf8"));
let readable = |s: &str| demangle(s).map(|d| d.name).unwrap_or_else(|| s.to_string());
let entered: std::collections::HashMap<String, u8> = coverage.entered.iter().map(|(s, p)| (readable(s), *p)).collect();
let imported = module.num_imported_funcs();
let mut units: std::collections::BTreeMap<String, (usize, usize, usize)> = Default::default();
for (i, sym) in &module.names.as_ref().expect("names").funcs {
if *i < imported {
continue;
}
let Some(unit) = makepad_wasm_strip::opt::units::unit_of(sym) else { continue };
let mut body = Vec::new();
makepad_wasm_strip::opt::encode::func_body(&mut body, &module.funcs[(*i - imported) as usize]);
let e = units.entry(unit).or_default();
e.0 += body.len();
e.1 += 1;
e.2 += usize::from(entered.contains_key(&readable(sym)));
}
let mut rows: Vec<_> = units.into_iter().collect();
rows.sort_by(|a, b| b.1 .0.cmp(&a.1 .0));
let (mut never, mut total) = (0, 0);
for (_, (bytes, _, hit)) in &rows {
total += bytes;
if *hit == 0 {
never += bytes;
}
}
println!("code {total} bytes; in units the run never entered: {never}");
for (unit, (bytes, fns, hit)) in rows.iter().filter(|r| std::env::var("NEVER").is_err() || r.1 .2 == 0).take(60) {
println!("{bytes:>9} {fns:>5} fns {hit:>5} entered {unit}");
}
}
_ => eprintln!("wasm_units instrument|coverage|strip ... (see the source)"),
}
}