Skip to content

Port WeakRef and FinalizationRegistry record/replay from Chromium - #162

Merged
Andarist merged 7 commits into
masterfrom
claude/weakrefs-finalization-registry-port-193ca2
Oct 7, 2026
Merged

Andarist merged 7 commits into
masterfrom
claude/weakrefs-finalization-registry-port-193ca2

Conversation

@Andarist

@Andarist Andarist commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

ports:
replayio/chromium-v8#279
replayio/chromium-v8#334
replayio/chromium-v8#335

This is needed in preparation for #161 . Without this landing #161 would start crashing for a bunch of recorded journeys etc because FinalizationRegistry is much more prevalent in node code than it is in browser code. For example, undici is using it

Node-specific behaviour worth knowing (no changes made):

  • Node also calls ClearKeptObjects on every top-level callback exit (src/api/callback.cc:129), so the poll runs more often than in Chromium. Those points happen identically when recording and replaying, so it only adds recorded values.
  • Node core creates FinalizationRegistries and WeakRefs itself (event_target.js, abort_controller.js, iterable_weak_map.js). Once one of them registers a cell or creates a WeakRef, every recording records a poll value at each checkpoint from then on. The fork's comments already acknowledge these flags are never reset.

@Andarist
Andarist added this pull request to stack #165 October 2, 2026 11:56
@Andarist Andarist changed the title Port WeakRef and FinalizationRegistry record/replay from the Chromium… Port WeakRef and FinalizationRegistry record/replay from Chromium Oct 2, 2026
@Andarist
Andarist force-pushed the claude/weakrefs-finalization-registry-port-193ca2 branch 5 times, most recently from 67a090c to e911ed0 Compare October 6, 2026 10:51
@Andarist
Andarist requested a review from Domiii October 7, 2026 09:11
@Andarist
Andarist force-pushed the claude/weakrefs-finalization-registry-port-193ca2 branch from e911ed0 to 2f60df2 Compare October 7, 2026 09:52
Andarist and others added 7 commits October 7, 2026 10:21
… fork

Port replayio/chromium-v8#279, nodejs#334 and nodejs#335, ending at the state of nodejs#335:

- WeakRef.prototype.deref() records/replays whether the target is alive.
  When replaying, marking treats JSWeakRef::target as strong and a deref()
  the recording saw as dead clears the field.
- FinalizationRegistry cleanup of registries constructed at a point that
  replays ("tracked", behind the "finalization-registry" feature) is
  driven by the recording: the GC no longer schedules the cleanup task for
  them, ReplayFinalizationRegistries::Poll records the decision at every
  microtask checkpoint, and the cleanup loop records which cell each
  callback is called for. When replaying, the targets of tracked cells are
  marked strongly, and the recorded cells are cleared right before their
  callbacks run. Registries the recording's GC collected are released.
- unregister() and register() of a tracked cell while events are
  disallowed crash, since they would change the registry on one side only.
- Per-isolate replay state lives in ReplayIsolateData, reset in
  Isolate::Deinit before the global handles go away.

V8 9.4 adaptations: JSFinalizationRegistry accessors are hand-written, the
unregister bookkeeping hooks into Unregister's match callback, and
recordreplay::AreEventsPassedThrough is added (chromium-v8 f6f61489066).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bring the port in line with the latest replayio/chromium-v8#334 and nodejs#335
(up to 609b71dddca):

- Rename the added fields and runtime functions with the record_replay /
  RecordReplay prefix, and pass Heap::RecordReplayTracking to the dirty
  registry helpers and the cleanup task. Heap::DequeueDirtyJSFinalizationRegistry
  loses its last caller and is removed.
- Record what the GC did to tracked registries and WeakRefs in a single
  ReplayGCPoll::Poll at every microtask checkpoint.
- Track WeakRefs constructed at a deterministic point ("weak-ref-collection"
  feature): the recording notes which ones its GC cleared, and the replay
  clears the same targets instead of keeping them alive until the WeakRef
  goes away. The list of tracked WeakRefs lives in old space.
- Give a WeakCell its id before it is linked into the registry, cache
  IsReplaying() in the marking visitor, and crash when a replayed deref()
  or a delivered cell disagrees with the recording.

V8 9.4 adaptations: src/replay/replayio.{h,cc} only carry
AreEventsAvailable(), global-handles.h replaces global-handles-inl.h, and
the Torque constructor uses assert where the fork uses dcheck.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A FinalizationRegistry gets its record/replay id when it is
constructed. One deserialized from the startup snapshot, like the
registry AbortSignal.timeout() uses, was constructed when the snapshot
was built and so has none, and its cleanup task was posted from inside
the GC and run at a point which depends on when the GC ran.

Give a registry without an id one on its first register() call, when
it has no cells yet, so that it is tracked like any other from then
on.

When the GC still finds cleared cells of a registry without an id
(the feature is off, or the registry was populated before it could be
adopted), leave them uncollected instead of running the cleanup at a
GC-dependent point, and report it as a diagnostic once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit cf7518182391241161b1981647add940da5c7e9b)
Isolate::Deinit reset it before debug()->Unload() and the wasm compile
job cleanup, which can still allocate and so run a GC. A mark-compact
in that window which cleared a tracked WeakRef's target called
ReplayWeakRefs::OnTargetCleared on the reset data.

Reset it after Heap::StartTearDown, after which no GC runs and before
the global handles its v8::Globals need are destroyed, and have
OnTargetCleared tolerate missing data like ReplayGCPoll::Poll does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ba9caeffae56707d94b22e2b1b174845e5da4bca)
…rding or replaying

Port of replayio/chromium-v8#343, first commit.

WeakRef.prototype.deref(), the WeakRef constructor, the
FinalizationRegistry constructor and register() each called a
RecordReplay* runtime function unconditionally. Outside record/replay
those functions do nothing, but the builtin-to-runtime transition costs
every caller: deref() went from an inline field load to a runtime call.

The process-wide recording flag is now readable by generated code through
ExternalReference::record_replay_is_recording_or_replaying, and the
builtins only call the runtime when it is set. When it is clear, deref()
loads the target inline as upstream does and the record/replay ids stay 0,
which the runtime side already treats as untracked. The flag is set by
recordreplay::SetRecordingOrReplaying before any isolate exists and never
changes, and it is per process like recordreplay::IsRecordingOrReplaying().

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Port of replayio/chromium-v8#343, second commit.

Runtime_RecordReplayWeakRefDeref recorded the target's liveness even
where the driver cannot record or replay a value, e.g. while a pause
evaluates an expression which calls deref(). The driver passed the
value through, so the result was the current target either way, but it
added a recording warning on every call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
What the record/replay bookkeeping needs is a point whose behaviour is
semantically deterministic between recording and replay, where the same
state is reached and the same decisions are made, so that a value recorded
or cleanup run there reproduces. "A point which replays" named it badly:
GC points replay too, at other places. Comments and diagnostics only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@Andarist
Andarist force-pushed the claude/weakrefs-finalization-registry-port-193ca2 branch from 2f60df2 to 72bddbe Compare October 7, 2026 10:43
@Andarist
Andarist merged commit 0ea7a6e into master Oct 7, 2026
2 of 3 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.

2 participants