Repository navigation
Keep active MessagePort listeners reachable across garbage collection #288
Description
Activity
- addedbugSomething isn't workingSomething isn't workingvscode-oss/plannedPlanned for the AppScene/WebScene VS Code OSS integrationPlanned for the AppScene/WebScene VS Code OSS integration
on Sep 17, 2026 Read-only audit of PR #245 current head
d95831ccis a no-go for #281 rebase. The MessagePort implementation and portable lifetime test are unchanged after94171a32. That commit retains local same-isolate peers, but transfer removes the edge and there is no started/listener-active root, so it cannot cover #281's direct Promise-resolver listener after the peer transfers to a Service Worker.Current acceptance disposition: direct resolver/SW forced-GC fail; active queued/future cross-realm delivery fail; transfer/teardown coverage partial; browser oracle partial; 1,000 forced-GC bound missing; original #281 100-cycle current-main gate fail; target cross-platform runners missing. The smallest owner handoff remains a focused active-listener reachability commit or extracted prerequisite PR from current main with exact release boundaries and repeated #281/#285 gates.
The #81 packaged trace does not reproduce a transferred-port delivery loss. The unchanged local extension-host path reaches transferred port → first message in 2 ms, first message → Ready in 1 ms, and Ready → Initialized in 708 ms, in order.
The distinct latency begins on the remote extension-host protocol path and is tracked by native subissue #289. This does not qualify or close #288: the forced-GC active-listener regression and PR #245 overlap remain unchanged. It only prevents the #280/#252 timing symptom from being misassigned to this port-lifetime ticket.
PR #245 has merged its active-MessagePort reachability fix to
mainat4040058e. The rebased ServiceWorker Clients top #281 at62af6ad8now passes the focusedmessageport-gcregression together with its 100-cycle ServiceWorker Clients gate and native WPT 3/3. #288 remains open until #281’s related runner confirms the exact rebased head and the issue’s full acceptance/closure audit is recorded.Post-#245 acceptance audit:
Merged commit
94171a32adds a focused native GC regression foronmessage,addEventListener(...)+start(), and transferred same-realm endpoints. Rebased #281 at62af6ad8additionally proves 100 ServiceWorker Client/MessagePort cycles without the former cycle-69/70 stall.#288 remains open because the issue's complete acceptance is broader than those two gates. The next focused hardening slice must still add:
- the exact direct Promise-resolver listener form from the original failure;
- a 1,000 forced-GC round-trip gate with queue/heap/RSS/retained-binding metrics;
- listener removal/inactive collection plus close, transfer, navigation, Worker termination, and engine-teardown release assertions;
- Window, dedicated Worker, ServiceWorker, iframe, and same-realm identity/ordering/exactly-once coverage;
- Chromium/WPT-derived active-port GC/queued-delivery evidence.
Schedule: merge #281 after its directly related native Linux runner, then create this as a separate #288 acceptance PR from the new main. It must preserve the four-source Worker/ServiceWorker/MessagePort/WebSocket arbitration introduced by the #281 rebase and avoid the independent #287/#289 timer-arbitration work until that branch is rebased.
Focused same-realm GC acceptance slice is now open as #296.
It covers the exact direct-Promise-resolver failure, transferred-sender parity, read-only binding/queue metrics, a 1,000-cycle forced-GC stress gate, and a dedicated native/Chromium WPT contract. Native 5/5 and Chromium 5/5 pass; no bindings, messages, or bytes remain queued after settle.
The issue remains open for the cross-realm Worker, ServiceWorker, iframe, navigation, termination, listener-removal, inactive-port, and engine-teardown acceptance matrix.
Focused PR #296 merged to
mainat716aaf829ebe1ea14da35249bad189367e2e6ca2after the exact-head native Linux document contracts and portable V8 contract passed. Its broad package/platform matrix was canceled after merge under the changed-path policy.The WebScene consolidation PR #76 is refreshed at
32059fb4, and vscode-demo now pins merged WebScene716aaf82locally.This issue remains open. The next dependency-ordered acceptance slice covers dedicated Worker, ServiceWorker, and iframe identity/reachability, followed by navigation, worker termination, listener removal, inactive-port collection, and engine teardown.
Coordination from exact main
e523daa1: the next focused slice owns dedicated-Worker cross-isolate MessagePort reachability and its exact release boundaries. The reduced gap is that merged local-peer retention cannot cross the Worker isolate boundary, so an explicitly startedmessagelistener may be collected after transfer; active roots also need explicit release on listener removal,close(), transfer, Worker termination, and engine teardown. Scope is limited to generic MessagePort/EventTarget/Worker lifecycle code, one dedicated native forced-GC contract, and one Chrome/WPT-style cross-realm contract/profile. It will not touch the active #237 relative-sibling:has()lane, #425/#239 nested CSS rule files, consolidation #76, AppScene, or vscode-demo. ServiceWorker and iframe-navigation acceptance remain later #288 slices.Focused prerequisite #430 now owns the browser-shaped MessagePort.prototype onmessage/onmessageerror EventHandler accessor surface. It is a native child of #81 and will merge before this cross-isolate lifetime slice so the active-root implementation can observe the final handler assignment/clearing boundaries. #288 remains authoritative for reachability and release semantics.
Exact package dependency update — 18 September 2026
The
fc71e92fpackage proves transfer now occurs and exposes the next prerequisite as #430: browser-shapedMessagePort.prototype.onmessageandonmessageerroraccessors. The focused #430 fix should merge first. Then this issue resumes the dedicated Worker/ServiceWorker/iframe active-listener reachability, inactive-port collection, transfer/release, navigation, termination, and teardown matrix on top of it.This keeps API shape separate from GC lifetime and gives each hot path its own direct correctness, performance, memory, and lifecycle gates.\n
Focused dedicated-Worker cross-isolate lifetime slice is open as #436 at
689fda8e. The implementation roots only started, remotely entangled ports with activemessage/messageerrorlisteners and releases at handler/listener removal, abort/once dispatch, transfer, explicit or collected peer close, Worker termination, navigation, and teardown. Forced low-memory collection now runs synchronously on live Worker isolate threads. Exact-head local gates pass: Chrome 153 3/3, native contract 3/3, 100 Worker forced-GC cycles in 1.03 s with 2 binding slots and zero retained/queued state, existing 1,000-cycle GC stress, 10,000-message throughput, and the complete Worker/MessagePort aggregate. #288 remains open after this slice for ServiceWorker/iframe-specific cross-realm acceptance.PR #436 merged to
mainasac235def2417569b949af14ff18fd99153c88dbdfrom exact tested head689fda8e. Dedicated-Worker cross-isolate active-port reachability now follows the started + remotely entangled + activemessage/messageerrorlistener condition and releases on removal, abort/once dispatch, transfer, explicit or collected peer close, Worker termination, navigation, and teardown. Final evidence: Chrome 153 3/3; native contract 3/3; 100 child+parent forced-GC cycles in 1.03 s with 2 binding slots, zero retained bindings and zero queued messages/bytes; existing 1,000-cycle stress, 10,000-message throughput, and full Worker/MessagePort aggregate pass. Hosted portable V8 passed at the exact PR head; unrelated broad/package tails were canceled after merge under the fast policy. #288 remains open for ServiceWorker and iframe-specific cross-realm acceptance.Implementation-first re-audit at WebScene
488b9790found no remaining reduced ownership/teardown defect. Same-realm forced-GC, browser-shaped handlers, dedicated-Worker cross-isolate reachability/release, ServiceWorker transfer/generation retirement, and queue caps are present. The remaining iframe/ServiceWorker matrix is acceptance coverage, and the developer-owned #81 worktree was left untouched. No code or runtime validation was run;git diff --checkpassed.
Current MessagePort lifetime checkpoint — 21 September 2026
Exact merged heads are WebScene
d3c030fa, AppScene080ee9c, and unchanged Code OSS645f29cc. Focused child #825 and PR #856 are closed/merged at5db88657(20 September); the prior local-only/blocked compiler-stack status is superseded.The merged change forwards low-memory notifications through the serialized ServiceWorker runtime and retains active iframe ports across V8 contexts. Its focused iframe forced-GC gate and 100-cycle ServiceWorker gate passed, including queue, binding, V8 heap, and RSS bounds. Earlier same-realm 1,000-round-trip direct-Promise-resolver/addEventListener forced-GC gate passed with p95 0.823 ms, zero settled bindings/messages/bytes, and unchanged bounded heap/RSS; this evidence is recorded in
docs/validation/messageport-active-gc-20260917.md.#288 stays open for a cumulative 1,000-round-trip same-realm/dedicated-worker/ServiceWorker/iframe cross-platform installed-package matrix and the final inactive/listener-removal/close/transfer/navigation/termination/engine-teardown queue, binding, heap, and RSS proof. No new runtime fix is attributed without a reduced current-head failure.
Parent worker/MessagePort issue: #81.
Related integration: #265 and PR #281.
Overlapping owner: PR #245 (
webscene_v8_runtime_clone.inc).Proven gap
A started
MessagePortwith an installedonmessagehandler can be reclaimed while JavaScript is awaiting the next message. This drops the endpoint and leaves the pending Promise unresolved. The failure becomes deterministic when the current merged CSS/runtime head is combined with the #281 Service Worker Clients tranche.The unchanged focused loop creates a channel, assigns the Promise resolver directly, starts
port1, transfersport2into the Service Worker, and awaits the Service Worker message plus port reply. The old #281 build completes 100/100 cycles. Against currentmainplus #281, it repeatedly stalls near cycle 69–70 withcomplete:false,error:null, and no queue-capacity phase reached.Applying PR #245 commit
94171a32locally preserves local entangled peers but does not fix this case. Replacingchannel.port1.onmessage = resolvewith a closure that records state and callsresolve(event)makes five consecutive 100-cycle runs pass (p95 0.065–0.147 ms). That timing-sensitive difference indicates that the weak native wrapper is not retained by the started-port/listener reachability contract; the closure happens to keep additional JavaScript state alive.This is a generic MessagePort lifetime defect. Do not special-case Service Workers or Promise resolver functions. The implementation path overlaps #245 and must be coordinated there instead of duplicated.
Acceptance
port.onmessage = promiseResolveandaddEventListener('message', listener)afterstart().close(), transfer, listener removal plus inactive state, navigation, worker termination, and engine teardown release native endpoints, weak handles, listeners, queue bytes, and wake registrations.mainwithout adding timing sleeps or artificial JavaScript captures.Proposed fix
Extend the MessagePort reachability model in the existing #245-owned clone/runtime path: retain active ports through the browser-spec active-port condition while they are started and have message listeners, and release that retention at the exact close/transfer/listener/lifecycle boundaries. Preserve weak collection for inactive unreachable pairs.