Skip to content

Getter-aware iterator reads, IteratorClose, and timeout-safe iteration (interpreter)#982

Merged
nickna merged 1 commit into
mainfrom
wrk/iterator-getter-close-and-timeout
Jun 27, 2026
Merged

Getter-aware iterator reads, IteratorClose, and timeout-safe iteration (interpreter)#982
nickna merged 1 commit into
mainfrom
wrk/iterator-getter-close-and-timeout

Conversation

@nickna

@nickna nickna commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Background

Started as an investigation into the long-"unexplained" interpreted Test262 baseline flake. Finding: the baseline is fully deterministic in isolation — 7 consecutive runs (default 6 workers + oversubscribed 24 workers on 12 cores) are byte-identical to each other and to the committed baseline. The per-realm work (#981) already removed the cross-worker state race; that was never this flake.

The flake is load-induced timeout flips: workers are long-lived subprocesses pulling from a shared queue (non-deterministic test→worker assignment), and under heavy concurrent machine load tests near the 15s timeout flip Pass↔Timeout depending on scheduling. The amplifier was a real interpreter bug (below): timed-out infinite-loop tests leaked CPU-pegged orphan threads that accumulated and starved later tests.

Changes (interpreter-only — compiled mode emits its own iterator IL)

Interpreter.EnumerateWithIteratorProtocol had three gaps, all fixed here:

  1. Getter-aware done/value reads. It read the iterator result's done/value via a raw field accessor that bypassed accessor getters (_getters). A result with a throwing value/done accessor (Test262 poisoned-iterator tests) never fired the getter, so done stayed falsy and the loop spun forever. Now reads via Get() (getter-aware, prototype-walking), reading value only when not done (ECMA-262 §7.4.4/§7.4.5).

  2. Timeout-safe iteration. The while(true) never checked the VM timeout token, so a non-terminating iterator under the Test262 harness left a background (orphan) thread spinning at 100% CPU after the runner returned Timeout. The accumulating orphans were the flake amplifier. Now checks _vmTimeoutToken each step and unwinds the enumerator so the thread exits. (No-op in the CLI, where the token is non-cancellable.)

  3. IteratorClose (§7.4.6). The loop is wrapped in try/finally so an abandoned iteration — for-of break/throw, or a spread / Array.from element callback throwing — invokes the iterator's return(). An iteratorDone flag suppresses the close on normal exhaustion. Generalizes to all iterator consumers (for-of, spread, yield*, destructuring).

Array.from(items, mapFn) (ArrayStaticBuiltIns) now applies mapFn during iteration (lazy foreach) instead of materializing the whole source via .ToList() first, so a throwing mapFn surfaces immediately (no infinite-iterator hang), triggers IteratorClose, and observes mid-iteration mutations (ECMA-262 §23.1.2.1 step 6.g).

Results

Test262 interpreted baseline: +6, 0 regressions

  • Array/from/iter-get-iter-val-err.js: Timeout → Pass
  • Array/from/iter-map-fn-err.js: Timeout → Pass
  • call/spread-err-mult-err-itr-value.js: Timeout → Pass
  • call/spread-err-sngl-err-itr-value.js: Timeout → Pass
  • Object/fromEntries/iterator-not-closed-for-throwing-done-accessor.js: Fail → Pass
  • Array/from/elements-updated-after.js: Fail → Pass

Full xUnit suite 14410 / 0. Added 3 interpreter-only iterator regression tests (all use finite/breakable iterators so they terminate regardless of the fix — no hang risk; the throwing/non-terminating shapes are guarded by the bounded Test262 baseline). Compiled baseline untouched.

Notes / follow-ups

  • The flake itself is load-sensitive and not fully eliminated by code (it's inherent to timeout + non-deterministic scheduling), but this removes its main self-inflicted amplifier (orphan threads).
  • The large line count in Interpreter.Statements.cs is mostly re-indentation for the try/finally; the logic change is ~40 lines.

… iteration

The interpreter's iterator protocol (EnumerateWithIteratorProtocol) read the
iterator result's `done`/`value` via a raw field accessor that bypassed accessor
getters, never checked the VM timeout token, and never performed IteratorClose.
Three ECMA-262 fixes (interpreter-only; compiled mode emits its own iterator IL):

1. Read `done`/`value` through Get() (getter-aware, prototype-walking), reading
   `value` only when not done (7.4.4/7.4.5). A result with a throwing `value`/`done`
   accessor (Test262 poisoned-iterator tests) previously never fired the getter, so
   `done` stayed falsy and the loop spun forever.

2. Honor `_vmTimeoutToken` in the loop. A non-terminating iterator under the Test262
   harness left a CPU-pegged background (orphan) thread after the runner returned
   Timeout; the accumulating orphans starved later tests and made the interpreted
   baseline flaky under load. The check unwinds the enumerator so the thread exits.

3. IteratorClose (7.4.6): wrap the loop in try/finally so an abandoned iteration
   (for-of break/throw, a spread or Array.from element callback throwing) invokes the
   iterator's return(). `iteratorDone` suppresses the close on normal exhaustion.

Array.from(items, mapFn) now applies mapfn DURING iteration (lazy foreach) instead
of materializing the whole source first, so a throwing mapfn surfaces immediately
(no infinite-iterator hang), triggers IteratorClose, and observes mid-iteration
mutations (23.1.2.1 step 6.g).

Test262 interpreted baseline: +6 (4 Timeout->Pass, 2 Fail->Pass), 0 regressions.
Full xUnit suite 14410/0; +3 iterator regression tests.
@nickna nickna merged commit cb26d61 into main Jun 27, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant