Gate step 3 aborts: every Device-constructing unit test overflows the 2 MiB libtest thread stack (blocks the whole netplay PR cluster) #23
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#23
Loading…
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
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?
Summary
cargo test --lib-- step 3 of the documented gate (scripts/gate.sh) --dies with
fatal runtime error: stack overflow, aborting/ SIGABRT on everybranch that adds a unit test constructing a
Device. Because a stack overflowaborts the whole test binary, one such test takes down every other test in
the run, so the gate is not merely red, it is uninformative.
This is one cause with five PR-shaped symptoms. Filing it once rather than
bouncing each PR for a defect none of them can reasonably fix alone.
Why nobody hit this before
cargo test --libonmainruns zero tests:Step 3 is currently vacuous. It was green when #16 established it because
there was nothing to run. The first PR to add a real lib test discovers that
the step cannot execute one.
Measurement
Gated in a warm clone on the review box (WSL),
origin/main@8a46ac5:main(control)main+!21+!22+!14Devicein its tests)main+!21+!22+!14+!15savestates::tests::netplay_in_memory_savestate_round_trip_restores_devicemain+!21+!22+!18netplay::tests::resume_host_loads_state_from_disk_and_resets_frameTwo different tests on two different branches, same failure mode. !18 subsumes
!14/!15/!13/!9 (they are ancestors of
issue-4), so this single cause blocks!15, !13, !9, !18 and any future netplay/savestate test.
Root cause, with numbers
Deviceis the entire N64 machine state (cpu,pif,cart,memory,rsp,rdp,rdram,mi,pi,vi,ai,si,ri,vru, 4 xTransferPak,cheats). Measured directly:Device::new()returnsBox<Device>(src/device.rs:337) but builds it asBox::new(Device { ... })(src/device.rs:345). In a debug build thestruct literal and its nested field literals are materialized on the
stack and then moved into the box -- the temporaries are not elided at
-C opt-level=0. Peak stack use is therefore a multiple of 301 KiB, andtests that build two devices (or one plus a savestate clone, e.g.
savestates.rs:324) multiply it again.libtestruns each test on a spawned thread, whose Rust default stackis 2 MiB -- not the 8 MiB main-thread stack.
Bracketed by measurement on the failing test:
The repo already knows the number.
.cargo/config.tomlships:That 8 MiB link-arg sizes the built executable's main thread. Nothing gives
libtest's test threads the same budget, on any platform. The measured
requirement (>4, <=8 MiB) matches the figure the project already chose.
Confirms it is a large-frame problem and not runaway recursion: raising the
stack alone makes the identical test pass unchanged.
Fix
Pick one; (a) is the minimum that unblocks the cluster, (b) is the durable one.
State which you took and why.
(a) Give test threads the stack the project already standardises on. Export
RUST_MIN_STACK=8388608inscripts/gate.sharound step 3 (and the same inthe CI test job, if one runs
cargo test). Cheap and honest, but it only fixesrunners that go through those two paths -- a bare
cargo test --libstillaborts.
(b) Make the tests independent of the ambient stack. A shared test helper
that runs the body on an explicitly sized thread:
Correct under any runner, no env var required.
(c) Out of scope for this issue, but note it: removing the stack temporary
in
Device::new(field-by-field init intoBox::new_uninit, or boxing thelarge sub-structs) would fix the class rather than the symptom. That is an
invasive change to a hot upstream-shared file and should be its own issue if
anyone wants it -- do not attempt it here.
Acceptance criteria
./scripts/gate.shfrom a clean checkout ofmainwith !15 merged inexits 0, and step 3 reports a non-zero passing test count -- quote the
test result:line. A run reporting0 passeddoes not satisfy this.main+ !18.cargo test --libexecuted directly, with no wrapper and no env varset by the caller, does not abort. (Fix (a) alone fails this; say so
explicitly if you choose (a) anyway.)
constructing two
Devices -- so this cannot silently regress once step 3has real content.
size_of::<Device>()from your own run; if it has grown past ~700 KiBthe 8 MiB figure needs revisiting.
Explicitly OUT of scope
Device's layout orDevice::new's signature (see (c)).vendor/ggrs/-- the vendored fork is byte-identical across!9/!10/!11/!18 (tree
816fce1d) and is not implicated.sequencing problem and does not belong in this fix.
-C target-cpu/ portability (#16, #20) -- unrelated.Coordination
This should land before !15/!13/!9/!18, on a branch off current
maintouching only
scripts/gate.shand/or a test helper, so it does not collidewith the netplay cluster's contended files (
src/netplay.rs,src/savestates.rs,src/device.rs). !14 and !21/!22 are unaffected and canland independently.