tests: make weak-table weakness tests robust under conservative GC - #1815
tests: make weak-table weakness tests robust under conservative GC#1815dg1sbg wants to merge 1 commit into
Conversation
WEAK-KEY-AND-VALUE-WEAKNESS fails intermittently on
clasp/ubuntu-latest/bytecode. The identical commit failed the regression
step on one run and passed it on another, while macos/bytecode and
ubuntu/native passed both times, so it is non-deterministic rather than a
real regression.
The four weakness tests populate a table with throwaway (list nil)
values, call garbage-collect once, and assert an exact hash-table-count.
Two things make that fragile under Boehm's conservative stack scanning:
- the temporaries are created in the same frame that then calls
garbage-collect, so their addresses are still in live stack slots and
registers when the collector scans;
- a single collection is assumed sufficient.
Either one leaves a dead entry alive and the count comes out one too
high, which can be perturbed by entirely unrelated codegen changes.
Build the table in a function that returns before collecting, so the
frame holding the temporaries is popped and below the stack pointer when
the scan happens, and collect up to TRIES times instead of once. A
genuinely retained entry still fails the test -- verified by holding a
key in a global, where the count stays at 2 for all attempts -- so this
does not make the tests vacuous.
Factored into one WEAK-COUNT helper shared by all four tests rather than
repeating the retry in each.
Fixes clasp-developers#1814.
|
Tried to validate this on Linux, where the flake actually occurs. The result is inconclusive, and I would rather say so than imply the fix is proven. Setup: x86-64 Ubuntu 24.04, LLVM 18,
The control passed too. So this box never reproduced the flake, and the six green runs on the fixed side establish nothing about the fix. Reproduction likely needs CI's environment — 2-4 core runners under memory pressure have a very different stack and heap profile than the 16-core / 157 GB box I used, and conservative-GC retention is exactly the sort of thing that depends on that. What is established, independent of reproduction:
So this rests on the reasoning plus the non-vacuity check, not on an observed before/after. If the flake reappears after this merges, that is the reason. |
|
For the moment I'm just going to mark the tests as known failures under Boehm. Boehm is not reliable enough for us to be able to mandate that objects are collectable. |
The four weakness tests in hash-tables0.lisp allocate throwaway (list nil) temporaries in the same frame that then calls garbage-collect, and assert an exact hash-table-count afterwards. Boehm scans the stack conservatively, so a stale word left in that frame or in a register can still reference one of the temporaries and keep its weak entry alive; the count comes out one too high and the test fails. This is not a collector defect. A conservative collector promises never to collect live data, not to always collect dead data, and the comment above these tests already acknowledges they are stricter than that. The tests create the stale reference themselves by building and collecting in the same frame. Move construction into a builder that RETURNS first, so the frame holding the temporaries is popped before the collection. The assertion is unchanged: one collection, exact count. Measured on x86-64 Linux (LLVM 18, boehmprecise, bytecode) by calling the test body directly under 500 frames of recursion, 1000 trials per arm. The rate depends on code layout and varies between builds: across three builds the current form failed at 16.5%, 5.4% and 0.1%. The builder form failed 0 times in 2000 trials spanning the 5.4% and 0.1% builds, where about 55 failures would otherwise be expected. Non-vacuity checked: an entry whose key and value are both held in globals still yields a count of 2 and still fails, so a genuine leak is not masked. Previously submitted as clasp-developers#1815, which also retried the collection. The retry proves unnecessary and is omitted, so the test asserts exactly what it does today. Refs clasp-developers#1814.
Fixes #1814.
WEAK-KEY-AND-VALUE-WEAKNESSfails intermittently onclasp/ubuntu-latest/bytecode. The identical commit failed the regression step on one run and passed it on another, whilemacos/bytecodeandubuntu/nativepassed both times — so it is non-deterministic, not a real regression. Details and run links in #1814.Why the tests are fragile
All four weakness tests share this shape:
Two things make that unreliable under Boehm's conservative stack scanning:
(list nil)values are created in the same frame that then callsgarbage-collect, so their addresses are still sitting in live stack slots and registers when the collector scans the stack. The collector cannot tell those words from real references and keeps the entries alive.Either one leaves a dead entry alive and the count comes out one too high. Because it depends on register allocation and stack residue, it can be perturbed by entirely unrelated codegen changes elsewhere in the tree — which is how it surfaced.
The existing comment already acknowledged these are "pretty strict"; this makes them strict about the property being tested rather than about incidental stack layout.
Change
TRIEStimes rather than exactly once.WEAK-COUNThelper shared by all four tests instead of repeating the retry logic four times.Not vacuous
A retry loop risks turning the test into a tautology, so I checked that it still fails on genuine retention. Holding one key in a global:
An entry that is really retained never reaches the expected count, so the tests keep their teeth.
Verification
macOS arm64, boehmprecise: full suite 1979 successes, zero unexpected failures; the weakness tests pass 3/3 across repeated
TEST_SUITES=hash-tables0runs. (weak-key-or-value-weaknessis#-use-boehmand does not run on these variants, but is updated for consistency.)