netplay: 8 MiB test-stack budget for Device-constructing unit tests (issue #23) #24
No reviewers
Labels
No Label
blocked
bounced
in-progress
in-review
manual-only
needs-guidance
netplay
p1
p2
p3
rust
No Milestone
No project
No Assignees
1 Participants
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: pbwraith/gopher64-netplay#24
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "issue-23"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #23
What
Fixes the step-3 gate abort: every
Device-constructing unit test overflowedthe 2 MiB libtest thread stack in a debug build, aborting the whole test
binary (
fatal runtime error: stack overflow/ SIGABRT) and taking downevery other test in the run. One cause, five PR-shaped symptoms
(!15/!13/!9/!18 + any future netplay/savestate test).
Root cause recap (measured in the issue)
Deviceis the entire N64 machine state;size_of::<Device>()on thisrun = 307968 bytes (~301 KiB) (criterion 5; well under the ~700 KiB
revisit threshold).
Device::new()returnsBox::new(Device { ... }); in a debug build thestruct literal and nested field literals are materialised on the stack
before being moved into the box, so peak stack use is a multiple of the
struct size.
not the 8 MiB main-thread stack the project already standardises on
(
.cargo/config.tomlships/STACK:8388608on Windows).RUST_MIN_STACK=4MiBoverflows,8MiBpasses — large-frame pressure, not recursion.Fix taken: (b) durable helper + (a) gate immunisation — both
The issue says pick (a) or (b). I took both because they cover different
failure surfaces and together satisfy every acceptance criterion:
src/test_util.rs—with_big_stack(the durable fix): runs atest body on an explicitly sized 8 MiB thread. Correct under any runner,
no env var required. All Device-constructing tests (this one plus the
netplay cluster's as they land) route their bodies through it, so a bare
cargo test --libpasses — acceptance criterion 3, which fix (a) alonefails.
scripts/gate.sh:RUST_MIN_STACK=8388608exported around step3. Cheap insurance so the documented gate also passes for
Device-constructing tests that are not yet wrapped — the netplay
cluster's !15/!18 tests land after this fix on contended files and cannot
be assumed rewritten yet.
Regression test (criterion 4)
test_util::tests::constructing_two_devices_fits_under_the_8_mib_test_stack_budgetconstructs two
Devices (plusclone_state) via the helper. If thewrapping is ever removed the test binary aborts with a stack overflow
instead of failing normally — the mitigation cannot silently regress once
step 3 has real content. It also asserts
size_of::<Device>() < 700 KiB(criterion 5 tripwire: if
Devicegrows past ~700 KiB the 8 MiB budgetmust be revisited).
Evidence (own runs, this box)
Device::new(false)+clone_state) on theambient libtest thread,
cargo test --lib, no env:fatal runtime error: stack overflow— SIGABRT (exit 134). This is thefailure mode the issue documents; it also proves the regression test
would fail if the mitigation were removed.
RUST_MIN_STACK=8388608: passes — stackbudget, not recursion.
cargo test --lib, no wrapper, no env:test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out./scripts/gate.sh→GATE_OK, step 3test result: ok. 1 passed; 0 failed; ...(non-zero count; step 3 onmainalone used to run 0 tests).git diff --check: clean.Out of scope (per issue)
Devicelayout /Device::newsignature changes (option (c)) — invasivechange to a hot upstream-shared file, own issue.
vendor/ggrs/, the mutual netplay-PR merge conflicts,-C target-cpuportability.
Evidence for gopher64-netplay#23 (implementer lane 2, commit
d400b538, PR #24):Fix taken: (b) durable helper + (a) gate immunisation — both. (b) is the durable fix satisfying criterion 3; (a) additionally immunises the documented gate path so the netplay cluster's unwrapped Device-constructing tests (!15/!18, landing after this fix) pass step 3 without depending on rebase timing.
Measured on this box (own runs):
size_of::<Device>()= 307968 bytes (~301 KiB) — criterion 5, well under the ~700 KiB revisit threshold (also asserted in the test as a tripwire).cargo test --lib, no env:thread 'stack_probe...' has overflowed its stack->fatal runtime error: stack overflow, aborting(SIGABRT, exit 134). Reproduces the issue's documented failure mode with main-tree APIs only (Device::new + postcard serialise/restore + clone_state — the same profile as the !15/!18 netplay-savestate round trips).RUST_MIN_STACK=8388608: passes (proves large-frame stack pressure, not runaway recursion).cargo test --lib, no wrapper, no env var:test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s— criterion 3 (fix (a) alone cannot satisfy this)../scripts/gate.sh: steps 1-3 all green, GATE_OK; step 3 now reports a non-zero count (1 passed) where main's step 3 ran0 passed— criteria 1/2 step-3 requirement (the main+!15 / main+!18 combinations get exercised by the reviewer once those branches merge; gate.sh'sRUST_MIN_STACK=8388608export covers their not-yet-wrapped tests).git diff --check: clean.Files:
scripts/gate.sh(RUST_MIN_STACK export around step 3),src/test_util.rs(new:with_big_stack+ regression test),src/lib.rs(#[cfg(test)] mod test_util;). No changes to device.rs/netplay.rs/savestates.rs, no vendor/ggrs changes. Out-of-scope items (Device layout, merge conflicts, target-cpu) untouched.Merged. This is the one that unwedges the gate, so I gated it twice -- the
first run was void and I want that on the record rather than quietly redone.
My first gate was invalid (TRAP 2), disregard any earlier green
The warm clone
~/gate64has the WINDOWS CHECKOUT as itsorigin, and Ifetched
+refs/heads/*, which pulls only that checkout's LOCAL heads -- andits only local head is
main. Soorigin/issue-23did not exist,git checkout -B verify origin/issue-23failed withand because my script lacked
set -eit carried on and gated plainmain,printing
GATE_OK. The tell was exactly the one this repo has been bitten bybefore: the step-3 test count did not move (4 tests, all
netplay::*), andgrep -c device_round_trip_fits_under_the_8_mib_test_stack_budgeton the logreturned 0. A green gate that never compiled the PR's code proves nothing.
Corrected by fetching the checkout's remote-tracking refs
(
+refs/remotes/origin/*:refs/remotes/origin/*) and asserting everyprecondition loudly instead of letting a failure fall through.
Real gate (merge result)
4 -> 5 with the new test named in the output: the PR's code was exercised.
The check that decided it
Taking both (a) the gate env var and (b) the
with_big_stackhelper is theright call, but it raises an obvious question: is (b) actually doing anything,
or is
RUST_MIN_STACK=8388608ingate.shsilently carrying the test? If itwere the env var alone, the helper would be decorative and every future test
would still depend on being run through
gate.sh.So I re-ran the lib tests with the stack budget pinned back DOWN to the
libtest default, using
gate.sh's own provisioning env:At 2 MiB -- the exact budget that SIGABRTs the whole binary today -- the
Device round trip passes.
with_big_stackis load-bearing on its own, so atest that routes through it is safe under a bare
cargo test --libon anyrunner, not just under
gate.sh. That is the durable half, and it is real.Notes for whoever picks up the blocked stack
!9,!10,!11,!13,!15and!18were all blocked behind this oneabort. Landing this does NOT auto-fix them:
RUST_MIN_STACKingate.shimmunises the gate, but each Device-constructing test should still route its
body through
test_util::with_big_stackso it survives a barecargo test --lib. Five of those six also currently reportmergeable=false, so they need a rebase onto this main regardless.The
MAX_DEVICE_SIZE = 700 * 1024tripwire is a good touch -- it turns the8 MiB choice into something that fails loudly if
Devicegrows, instead of amagic number that silently stops being enough.