Skip to content

Commit 5b5f310

Browse files
committed
test(oom): cover async generator recovery guard
1 parent 97c511c commit 5b5f310

2 files changed

Lines changed: 85 additions & 4 deletions

File tree

docs/threads/testing.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,10 +135,11 @@ heap-cap witnesses also cover no-GIL `ArrayBuffer` byte-slab recovery while a
135135
real peer thread is running and publishing roots to the abort-safe parallel
136136
collector, plus the trace-sensitive async-generator request lock guard that
137137
keeps allocation-failure recovery out of mutable generator side-store critical
138-
sections. Separate deferred-generator and deferred-iterator-helper witnesses
139-
root suspended/mid-helper state and check that no-GIL allocation-failure
140-
recovery aborts instead of sweeping while parallel tracing has deferred mutable
141-
execution/helper buffers to a world-stopped finish.
138+
sections. Separate deferred-generator, deferred-async-generator-request, and
139+
deferred-iterator-helper witnesses root suspended, pending-request, and
140+
mid-helper state, then check that no-GIL allocation-failure recovery aborts
141+
instead of sweeping while parallel tracing has deferred mutable execution/helper
142+
buffers to a world-stopped finish.
142143

143144
`zig build threads-test` runs the green WebKit PR-249 allowlist from
144145
`reference/webkit-249/threads-tests`. CI shards the serialized/GIL leg with

src/context.zig

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12888,6 +12888,86 @@ test "parallel_js heap_limit_bytes fails closed with deferred iterator helper tr
1288812888
try std.testing.expect(ctx.gc_par_deferred_rounds.load(.monotonic) > before_deferred_rounds);
1288912889
}
1289012890

12891+
test "parallel_js heap_limit_bytes fails closed with deferred async-generator requests" {
12892+
// #36 request-buffer witness: an async generator suspended on an unresolved
12893+
// await keeps later next/return requests in its GC-backed request side
12894+
// store. That cell is still a Generator cell and is deferred by the parallel
12895+
// tracer, so no-GIL allocation-failure recovery must fail closed rather than
12896+
// sweep while a peer is live.
12897+
if (builtin.single_threaded) return error.SkipZigTest;
12898+
const ctx = try Context.createWithTestingOptions(std.testing.allocator, .{
12899+
.enable_threads = true,
12900+
.enable_gc = true,
12901+
.parallel_gc = true,
12902+
.parallel_js = true,
12903+
.heap_limit_bytes = 8 * 1024 * 1024,
12904+
});
12905+
defer ctx.destroy();
12906+
12907+
const baseline = ctx.gc_generator_backing_stores_live;
12908+
const prepared = try ctx.evaluate(
12909+
\\(() => {
12910+
\\ async function* ag() {
12911+
\\ await new Promise(() => {});
12912+
\\ yield 1;
12913+
\\ }
12914+
\\ const it = ag();
12915+
\\ const p1 = it.next();
12916+
\\ const p2 = it.next();
12917+
\\ const p3 = it.return(7);
12918+
\\ globalThis.__deferredRecoveryAsyncGenerator = { it, p1, p2, p3 };
12919+
\\ return typeof p1.then === 'function' &&
12920+
\\ typeof p2.then === 'function' &&
12921+
\\ typeof p3.then === 'function';
12922+
\\})();
12923+
);
12924+
try std.testing.expect(prepared.asBool());
12925+
try std.testing.expect(ctx.gc_generator_backing_stores_live > baseline);
12926+
12927+
var sibling: jsthread.ThreadRecord = .{ .id = 999, .gil = ctx.gil.?, .ctx = ctx };
12928+
{
12929+
ctx.gil.?.lockApi();
12930+
defer ctx.gil.?.unlockApi();
12931+
try ctx.reserveJsThreadsLocked(1);
12932+
ctx.js_threads.appendAssumeCapacity(&sibling);
12933+
}
12934+
defer {
12935+
ctx.gil.?.lockApi();
12936+
var i: usize = ctx.js_threads.items.len;
12937+
while (i > 0) {
12938+
i -= 1;
12939+
if (ctx.js_threads.items[i] == &sibling) {
12940+
_ = ctx.js_threads.swapRemove(i);
12941+
break;
12942+
}
12943+
}
12944+
ctx.gil.?.unlockApi();
12945+
}
12946+
12947+
const before_attempts = ctx.gc_par_attempts.load(.monotonic);
12948+
const before_collections = ctx.gc_par_collections.load(.monotonic);
12949+
const before_aborts = ctx.gc_par_aborts.load(.monotonic);
12950+
const before_round_aborts = ctx.gc_par_round_limit_aborts.load(.monotonic);
12951+
const before_deferred_rounds = ctx.gc_par_deferred_rounds.load(.monotonic);
12952+
12953+
const gc_saved = gc_mod.setActiveHeap(ctx.gc);
12954+
defer _ = gc_mod.setActiveHeap(gc_saved);
12955+
const ss_saved = stack_scan.enter(@frameAddress());
12956+
defer stack_scan.leave(ss_saved);
12957+
var machine = ctx.interpreter();
12958+
try ctx.pushActiveInterpreter(&machine);
12959+
defer ctx.popActiveInterpreter(&machine);
12960+
const ai_saved = gc_mod.setActiveInterpreter(&machine);
12961+
defer _ = gc_mod.setActiveInterpreter(ai_saved);
12962+
12963+
try std.testing.expect(!ctx.collectForAllocationFailure(&machine));
12964+
try std.testing.expect(ctx.gc_par_attempts.load(.monotonic) > before_attempts);
12965+
try std.testing.expectEqual(before_collections, ctx.gc_par_collections.load(.monotonic));
12966+
try std.testing.expect(ctx.gc_par_aborts.load(.monotonic) > before_aborts);
12967+
try std.testing.expect(ctx.gc_par_round_limit_aborts.load(.monotonic) > before_round_aborts);
12968+
try std.testing.expect(ctx.gc_par_deferred_rounds.load(.monotonic) > before_deferred_rounds);
12969+
}
12970+
1289112971
test "parallel_js (M3): sync wait peers publish roots for mid-script parallel GC" {
1289212972
// Sync property waits, Condition waits, and contended Lock acquisition are
1289312973
// not frozen `gc_parked` peers: they periodically pump tasks between short

0 commit comments

Comments
 (0)