Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions test/js/node/http2/h2-conformance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1297,21 +1297,25 @@ describe("request pseudo-header requirements (RFC 9113 §8.3.1)", () => {
const GC_STRAGGLERS = 3;

/**
* Collects until every one of `refs` is gone, giving up after 50 passes or once the count has not
* moved for 10 passes, and resolves to how many survived.
* Collects until no more than GC_STRAGGLERS of `refs` are left, giving up after 50 passes, or after
* 10 that collected nothing at all (the leak itself), and resolves to how many survived.
*
* A count that holds steady above GC_STRAGGLERS is no reason to stop. A concurrent JIT compile roots
* the arguments of the call that started it (a stream, as `this`) until the main thread installs the
* result, which it only does at its next tier-up check. On a debug build, where a compile takes
* seconds, 4 to 9 streams stay rooted that way for a dozen passes before they go.
*/
async function liveCount(refs: WeakRef<object>[]): Promise<number> {
const live = () => refs.filter(ref => ref.deref() !== undefined).length;
// A completed stream's JS teardown is spread over a few immediates (rstNextTick, deferred
// destroy), and a WeakRef target survives the job that dereferenced it, so every pass gets a
// fresh turn before collecting.
let last = live();
for (let pass = 0, stuck = 0; pass < 50 && last > 0 && stuck < 10; pass++) {
for (let pass = 0; pass < 50 && last > GC_STRAGGLERS; pass++) {
if (pass === 10 && last === refs.length) break;
await new Promise(resolve => setImmediate(resolve));
await gcTick();
const now = live();
stuck = now === last ? stuck + 1 : 0;
last = now;
last = live();
}
return last;
}
Expand Down