test(ail-embed): RED — build.rs links a stale libailang_rt.a (missing rerun-if-changed on runtime sources)
bugfix-swarm-rc-alloc-undercount, RED stage (audit trail; GREEN follows separately via implement mini-mode). Root cause of the m5.2-resume-attempt residual (the M5 swarm leak-proof still non-deterministic after the atomic-counter fix7bfa11e): ail-embed/build.rs declares cargo:rerun-if-changed only for the kernel .ail + build.rs itself — NOT runtime/rc.c. So Cargo never re-ran build.rs after 7bfa11e; the swarm keeps linking a stale pre-7bfa11e libailang_rt.a whose g_rc_*count++ is the old non-atomic racing increment.7bfa11ewas correct and is unchanged; it just never reached the ail-embed binary. This explains why the isolated crates/ail RED (rebuilds the staticlib fresh per run) is green while the ail-embed swarm is not, and the frees-stable/allocs-jitter asymmetry (few TLS-NULL frees = no contention; millions of TLS-NULL host allocs = race on the stale non-atomic counter). Framing: a genuine defect (build-dependency completeness gap), NOT test-methodology unsoundness — with a fresh archive the swarm Σ-over-stat-lines balances deterministically (Σallocs==Σfrees== 12000003, 5/5 per the debugger). runtime/rc.c at HEAD is correct; the GREEN fix is in the build script, not the runtime. RED pins the CAUSE deterministically (the symptom is non-deterministic by construction): objdump the libailang_rt.a that build.rs emitted for this crate; assert >=2 lock-prefixed instructions (the atomic lock incq for g_rc_alloc_count + g_rc_free_count from current rc.c) and that it matches a freshly-built archive. Boss-verified RED: linked archive has 0 lock insns ("STALE pre-7bfa11e archive linked"). Does NOT touch the committed #[ignore] of symbol_fan_swarm_leak_free nor the isolated embed_rc_global_stats_race RED (both correct, unchanged).
This commit is contained in:
@@ -0,0 +1,186 @@
|
|||||||
|
//! RED for `bugfix-swarm-rc-alloc-undercount`.
|
||||||
|
//!
|
||||||
|
//! Named property protected: the `libailang_rt.a` archive that
|
||||||
|
//! `ail-embed` links (emitted by `build.rs` via
|
||||||
|
//! `ail build --emit=staticlib`) MUST be regenerated whenever
|
||||||
|
//! `runtime/rc.c` changes, so the global RC stats counter increment
|
||||||
|
//! in the linked binary is the atomic (`lock`-prefixed) form that
|
||||||
|
//! commit `7bfa11e` introduced. If the build does not track
|
||||||
|
//! `runtime/rc.c` as an input, a stale pre-`7bfa11e` archive is
|
||||||
|
//! linked whose `g_rc_alloc_count++` is the original non-atomic
|
||||||
|
//! racing increment — the exact swarm alloc-undercount
|
||||||
|
//! (`Σfrees` stable-exact, `Σallocs` short by a jittering
|
||||||
|
//! ~1600–57000 across runs) re-surfaces, frozen into a stale
|
||||||
|
//! build artefact even though `runtime/rc.c` itself is correct.
|
||||||
|
//!
|
||||||
|
//! Why this shape and not a swarm re-run: the symptom (jittering
|
||||||
|
//! `Σallocs`) is non-deterministic by construction (it depends on
|
||||||
|
//! thread interleaving on the racing `incq`). The *cause* is fully
|
||||||
|
//! deterministic: the linked archive's `ailang_rc_alloc` either has
|
||||||
|
//! the `lock`-prefixed atomic global increment (correct, from the
|
||||||
|
//! current `runtime/rc.c`) or a plain non-atomic `incq` (stale
|
||||||
|
//! pre-`7bfa11e`). This RED pins the cause directly: it asserts the
|
||||||
|
//! archive `build.rs` produced for THIS crate is built from a
|
||||||
|
//! `runtime/rc.c` whose `g_rc_alloc_count` increment is atomic.
|
||||||
|
//!
|
||||||
|
//! Not a methodology unsoundness: with the correct (current)
|
||||||
|
//! `runtime/rc.c` linked, the swarm's Σ-over-stat-lines balances
|
||||||
|
//! deterministically (`Σallocs == Σfrees == 12000003`, every run).
|
||||||
|
//! The Σ invariant is sound; only the *stale-artefact build gap*
|
||||||
|
//! breaks it. The GREEN fix is a build-dependency completeness fix
|
||||||
|
//! (`ail-embed/build.rs` must declare `runtime/rc.c` — and the rest
|
||||||
|
//! of the staticlib's runtime inputs — as `rerun-if-changed`), NOT
|
||||||
|
//! a runtime change: `runtime/rc.c` at HEAD is already correct.
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
/// `<repo>` — the AILang workspace root, parent of this crate.
|
||||||
|
fn repo_root() -> PathBuf {
|
||||||
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.parent()
|
||||||
|
.expect("ail-embed has a parent (the AILang repo root)")
|
||||||
|
.to_path_buf()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `libailang_rt.a` that `build.rs` emitted into this crate's
|
||||||
|
/// `OUT_DIR` and that `swarm_runner` / `lib.rs` link. There is exactly
|
||||||
|
/// one under `ail-embed/target/**/build/ail-embed-*/out/`.
|
||||||
|
fn linked_runtime_archive() -> PathBuf {
|
||||||
|
let target = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target");
|
||||||
|
let mut found: Vec<PathBuf> = Vec::new();
|
||||||
|
fn walk(dir: &Path, acc: &mut Vec<PathBuf>) {
|
||||||
|
let Ok(rd) = std::fs::read_dir(dir) else { return };
|
||||||
|
for e in rd.flatten() {
|
||||||
|
let p = e.path();
|
||||||
|
if p.is_dir() {
|
||||||
|
walk(&p, acc);
|
||||||
|
} else if p.file_name().and_then(|s| s.to_str())
|
||||||
|
== Some("libailang_rt.a")
|
||||||
|
{
|
||||||
|
acc.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(&target, &mut found);
|
||||||
|
assert!(
|
||||||
|
!found.is_empty(),
|
||||||
|
"no libailang_rt.a under {} — build.rs must have emitted one \
|
||||||
|
(build the crate first: it links this archive)",
|
||||||
|
target.display()
|
||||||
|
);
|
||||||
|
// Newest by mtime — the one the current build links.
|
||||||
|
found.sort_by_key(|p| {
|
||||||
|
std::fs::metadata(p)
|
||||||
|
.and_then(|m| m.modified())
|
||||||
|
.unwrap_or(std::time::SystemTime::UNIX_EPOCH)
|
||||||
|
});
|
||||||
|
found.pop().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The linked runtime archive must be built from the current
|
||||||
|
/// `runtime/rc.c`, in which the global `g_rc_alloc_count` increment is
|
||||||
|
/// atomic. On x86-64 a relaxed `atomic_fetch_add_explicit` lowers to a
|
||||||
|
/// `lock`-prefixed RMW; the pre-`7bfa11e` non-atomic `g_rc_*count++`
|
||||||
|
/// emits none. A stale archive (build did not track `runtime/rc.c`)
|
||||||
|
/// therefore links zero `lock` instructions in `ailang_rc_alloc` and
|
||||||
|
/// re-introduces the swarm alloc-undercount race.
|
||||||
|
#[test]
|
||||||
|
fn linked_runtime_archive_has_atomic_global_rc_counters() {
|
||||||
|
let archive = linked_runtime_archive();
|
||||||
|
|
||||||
|
// Disassemble; count `lock`-prefixed instructions. The current
|
||||||
|
// runtime emits exactly two (`lock incq` for g_rc_alloc_count in
|
||||||
|
// ailang_rc_alloc and for g_rc_free_count in ailang_rc_dec). The
|
||||||
|
// stale pre-7bfa11e archive emits zero (plain `incq`, or a
|
||||||
|
// cmov-fused plain `incq`).
|
||||||
|
let out = Command::new("objdump")
|
||||||
|
.args(["-d", "--no-show-raw-insn"])
|
||||||
|
.arg(&archive)
|
||||||
|
.output()
|
||||||
|
.expect("run objdump on libailang_rt.a");
|
||||||
|
assert!(
|
||||||
|
out.status.success(),
|
||||||
|
"objdump failed on {}:\n{}",
|
||||||
|
archive.display(),
|
||||||
|
String::from_utf8_lossy(&out.stderr)
|
||||||
|
);
|
||||||
|
let disasm = String::from_utf8_lossy(&out.stdout);
|
||||||
|
let lock_insns = disasm
|
||||||
|
.lines()
|
||||||
|
.filter(|l| {
|
||||||
|
// Match the mnemonic column, not the symbol name / path.
|
||||||
|
l.split_once('\t')
|
||||||
|
.map(|(_, ins)| ins.trim_start().starts_with("lock "))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
lock_insns >= 2,
|
||||||
|
"linked runtime archive `{}` has {lock_insns} lock-prefixed \
|
||||||
|
instruction(s); the current runtime/rc.c emits an atomic \
|
||||||
|
(lock-prefixed) global RC counter increment in both \
|
||||||
|
ailang_rc_alloc and ailang_rc_dec (>=2 expected). Zero/one \
|
||||||
|
means a STALE pre-7bfa11e archive is linked: build.rs did not \
|
||||||
|
track runtime/rc.c as an input, so the swarm links the old \
|
||||||
|
non-atomic g_rc_alloc_count++ and the alloc-undercount race \
|
||||||
|
re-surfaces (Sfrees stable-exact, Sallocs short & jittering).",
|
||||||
|
archive.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cross-check: a freshly-built archive from the SAME current
|
||||||
|
// runtime/rc.c, via the same `ail build --emit=staticlib` path
|
||||||
|
// build.rs uses, is atomic. If this differs from the linked one,
|
||||||
|
// the linked one is provably stale.
|
||||||
|
let outdir = std::env::temp_dir().join(format!(
|
||||||
|
"ail-embed-rt-fresh-{}",
|
||||||
|
std::process::id()
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_dir_all(&outdir);
|
||||||
|
std::fs::create_dir_all(&outdir).unwrap();
|
||||||
|
let repo = repo_root();
|
||||||
|
let ail_bin = repo.join("target/debug/ail");
|
||||||
|
assert!(
|
||||||
|
ail_bin.exists(),
|
||||||
|
"expected the `ail` CLI at {} (build.rs builds it; run \
|
||||||
|
`cargo build -p ail` in the workspace first)",
|
||||||
|
ail_bin.display()
|
||||||
|
);
|
||||||
|
let st = Command::new(&ail_bin)
|
||||||
|
.arg("build")
|
||||||
|
.arg(repo.join("examples/embed_backtest_step_tick.ail"))
|
||||||
|
.args(["--emit=staticlib", "-o"])
|
||||||
|
.arg(&outdir)
|
||||||
|
.output()
|
||||||
|
.expect("ail build --emit=staticlib (fresh runtime archive)");
|
||||||
|
assert!(
|
||||||
|
st.status.success(),
|
||||||
|
"fresh `ail build --emit=staticlib` failed:\n{}",
|
||||||
|
String::from_utf8_lossy(&st.stderr)
|
||||||
|
);
|
||||||
|
let fresh = outdir.join("libailang_rt.a");
|
||||||
|
let fresh_disasm = Command::new("objdump")
|
||||||
|
.args(["-d", "--no-show-raw-insn"])
|
||||||
|
.arg(&fresh)
|
||||||
|
.output()
|
||||||
|
.expect("objdump fresh archive");
|
||||||
|
let fresh_lock = String::from_utf8_lossy(&fresh_disasm.stdout)
|
||||||
|
.lines()
|
||||||
|
.filter(|l| {
|
||||||
|
l.split_once('\t')
|
||||||
|
.map(|(_, ins)| ins.trim_start().starts_with("lock "))
|
||||||
|
.unwrap_or(false)
|
||||||
|
})
|
||||||
|
.count();
|
||||||
|
let _ = std::fs::remove_dir_all(&outdir);
|
||||||
|
assert_eq!(
|
||||||
|
lock_insns, fresh_lock,
|
||||||
|
"linked archive ({lock_insns} lock insns) disagrees with a \
|
||||||
|
freshly-built archive from the current runtime/rc.c \
|
||||||
|
({fresh_lock} lock insns) — the linked archive is STALE: \
|
||||||
|
ail-embed/build.rs did not regenerate libailang_rt.a after \
|
||||||
|
runtime/rc.c changed (missing rerun-if-changed on the \
|
||||||
|
staticlib runtime inputs)."
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user