Skip to content

File: keep the FileReader listener snapshot rooted while dispatching - #248

Open
matthargett wants to merge 3 commits into
BabylonJS:mainfrom
rebeckerspecialties:filereader-dispatch-snapshot
Open

matthargett wants to merge 3 commits into
BabylonJS:mainfrom
rebeckerspecialties:filereader-dispatch-snapshot

Conversation

@matthargett

@matthargett matthargett commented Sep 16, 2026

Copy link
Copy Markdown

Problem

FileReader::Dispatch originally copied callbacks into a heap-allocated vector of bare Napi::Function values. On the JavaScriptCore backend those values are not strong roots. Removing a listener during dispatch could release its registered FunctionReference while leaving a bare value in the snapshot.

There is also a semantic error in that example: a listener removed before its turn must not be invoked. The DOM invoke algorithm snapshots listener registrations while still observing their removal state. Keeping removed functions alive and calling them is not the correct contract.

Change

Each registration now has a shared record containing a strong FunctionReference and a removal flag. Dispatch copies shared records, removal marks the record before erasing it, and dispatch skips removed records before retrieving their callbacks. Re-adding the same callback creates a new registration, so it cannot revive an entry in an older snapshot. Nested dispatch observes the same removal state.

This supersedes the initial FunctionReference-only snapshot and corrects the initial test expectation. It belongs in this PR because rooting and removal semantics are part of the same dispatch snapshot mechanism. The diff stays within FileReader and its tests; no general EventTarget refactor or production GC hook is introduced.

Regression Coverage

  • Remove a middle listener, clear the test's reference, allocate, and verify it is skipped while the final listener runs.
  • Remove/re-add the same callback: defer the new registration until the next dispatch.
  • Self-removal and additions during dispatch.
  • Removal during nested dispatch, observed by the outer dispatch too.

The WPT listener-mutation and reentrancy cases were reviewed for contract guidance, not run unchanged in this non-DOM host.

Validation

Fresh isolated Debug JavaScriptCore build on macOS 27.0, commit bead2d631fbd239f43cfd75b6851c91d3274f981:

  • Existing FunctionReference-only snapshot: 15 FileReader cases pass, three new semantic assertions fail.
  • Original bare-function snapshot control: same three assertion failures, both normally and with JSC_collectContinuously=1.
  • Fixed: all 18 FileReader cases pass for 25 iterations normally and another 25 with JSC_collectContinuously=1.
  • Full suite passes normally and with continuous GC: all 23 native tests pass, including JavaScript.All with 231 passing and three existing V8-only skips.
  • npm run build and git diff --check pass.

These are deterministic semantic regression oracles, not a deterministic GC-crash reproducer. The allocation-only test did not establish reliable pre/post collection failure; the bare-function control also did not crash in this validation. The durable rooting guarantee is provided by the shared records owning strong references. Other engines/platforms and sanitizer builds were not run in this pass.

#221 mirrors the dispatch snapshot pattern into XMLHttpRequest and should preserve both rooting and removal semantics. Fork twin: rebeckerspecialties#32.

FileReader::Dispatch copied the listeners into a std::vector of bare
Napi::Function values before calling them. A listener that removes a
later listener drops the only strong reference to it, and on
JavaScriptCore nothing else roots a napi_value that lives on the C++
heap (its handle scopes are stubs; only the C stack is scanned), so the
later function could be collected before the loop reached it. Snapshot
FunctionReferences instead, released when dispatch returns.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The garbage-collection scenario lacks a focused automated regression test.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Comment thread Polyfills/File/Source/FileReader.cpp Outdated
Share listener records that own strong callback references so snapshots retain both roots and registration identity. Mark records removed before erasing them, and skip them even in nested dispatches; re-adding the same callback creates a new registration.

Correct the earlier test's removed-listener expectation and cover removal, remove/re-add deferral, self-removal, additions, and nested dispatch. These assert DOM dispatch semantics independently of GC timing, not a deterministic collection crash.
matthargett added a commit to rebeckerspecialties/JsRuntimeHost that referenced this pull request Sep 17, 2026
Carry the reviewed PR BabylonJS#248 shared listener records and regression coverage into the polyfill integration stack. Removed listeners are skipped, new registrations are deferred, and snapshots retain their callbacks during nested dispatch.

Source: BabylonJS#248, bead2d6.
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.

2 participants