use std::collections::BTreeMap;
use std::process::Command;
use object::{Object, ObjectSection, ObjectSymbol, SymbolKind};
fn synth() -> &'static str {
env!("CARGO_BIN_EXE_synth")
}
fn fixture() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("scripts/repro/gust_kernel.wasm")
}
fn per_function_sizes() -> BTreeMap<String, u64> {
let elf = "/tmp/size_attribution_390_gate.o";
let out = Command::new(synth())
.args([
"compile",
fixture().to_str().unwrap(),
"-o",
elf,
"-b",
"arm",
"--target",
"cortex-m4",
"--all-exports",
])
.output()
.expect("run synth");
assert!(
out.status.success(),
"synth compile failed: {}",
String::from_utf8_lossy(&out.stderr)
);
let bytes = std::fs::read(elf).expect("read elf");
let obj = object::File::parse(&*bytes).expect("parse elf");
let text = obj.section_by_name(".text").expect(".text");
let end = text.address() + text.size();
let mut starts: Vec<(u64, String)> = obj
.symbols()
.filter(|s| {
!s.name().unwrap_or("").is_empty()
&& matches!(
s.kind(),
SymbolKind::Text | SymbolKind::Label | SymbolKind::Unknown
)
&& s.address() >= text.address()
&& s.address() < end
})
.map(|s| (s.address(), s.name().unwrap().to_string()))
.collect();
starts.sort();
starts.dedup_by(|a, b| a.0 == b.0);
let mut sizes = BTreeMap::new();
for (i, (addr, name)) in starts.iter().enumerate() {
let next = starts.get(i + 1).map(|(a, _)| *a).unwrap_or(end);
sizes.insert(name.clone(), next - addr);
}
sizes
}
const LOCKED: &[(&str, u64)] = &[
("gust_poll", 716),
("gust_mix", 32),
("func_0", 408),
("func_1", 76),
];
#[test]
fn gust_hot_path_text_size_is_pinned_390() {
let sizes = per_function_sizes();
let mut repin = String::from("\nREPIN (per-function .text bytes):\n");
for (name, _) in LOCKED {
let got = sizes.get(*name).copied().unwrap_or(0);
repin.push_str(&format!(" (\"{name}\", {got}),\n"));
}
println!("{repin}");
let mut mismatches = Vec::new();
for (name, locked) in LOCKED {
let got = sizes
.get(*name)
.copied()
.unwrap_or_else(|| panic!("function {name} not found in .text (renamed/dropped?)"));
if got != *locked {
mismatches.push(format!(
" {name}: {got} B (locked {locked} B, delta {:+})",
got as i64 - *locked as i64
));
}
}
assert!(
mismatches.is_empty(),
"gust hot-path .text size changed — this is the #390 size gate.\n{}\n\
If a perf lane INTENTIONALLY shrank these (a win!), or a change grew them, \
update LOCKED (copy the REPIN block above) and regenerate the attribution \
report:\n SYNTH=$CARGO_TARGET_DIR/debug/synth \
python3 scripts/repro/size_attribution_390.py --report",
mismatches.join("\n")
);
}