Skip to content

RDEV-10097 - Dispose the plugins of a destroyed view - #218

Open
helderjgoncalves wants to merge 1 commit into
masterfrom
rdev-10097/dispose-view-plugins
Open

RDEV-10097 - Dispose the plugins of a destroyed view#218
helderjgoncalves wants to merge 1 commit into
masterfrom
rdev-10097/dispose-view-plugins

Conversation

@helderjgoncalves

Copy link
Copy Markdown
Collaborator

What

Inner views leak their whole shadow dom when they are destroyed.

Plugins are not part of the react tree, so unmounting a view never reaches them. A plugin that registered itself in document level state stays registered, and its registration closes over the view's root, so the shadow root and everything under it is kept alive by a module level singleton in the host document. ViewFramework is the clearest case: its constructor adds handlers to the shared keyboard, mouse and text selection handlers, and those queues live for as long as the document does.

A heap snapshot from a screen that swaps inner views shows exactly that shape: detached shadow roots and their react fibers, retained by TextSelectionChangeEventHandler.endHandlersQueue.

The C# side does call Dispose on some plugins when the adapter goes, but nothing does it when the view itself is destroyed and the host keeps running, which is what happens on every inner view swap.

What changed

onChildViewRemoved now releases the view before notifying the host:

  • disposes the view's plugins, individually guarded so that one failure does not abandon the rest of the teardown
  • clears modules and lets go of root, head and the render handler
  • marks the view as released, so that a component or a plugin that is still loading does not attach itself to a view that is already detached

Two load paths were guarded with that flag, since both used to finish against a dead view: loadPlugins no longer builds a plugin after its view is gone, and loadComponent no longer renders into it. loadScriptPerView returns instead of waiting on a load that a detached head can never complete.

Feature toggle

ReactViewFactory.EnsureViewPluginsAreDisposed, defaulting to true, gates the release. It is a separate flag from LoadScriptsOncePerDocument on purpose: reusing that one would mean a host that hits a problem here has to give up the script deduplication too, and the point of a kill switch is to be able to tell which change caused what.

Tests

InnerViewPluginsTests.PluginIsDisposedWhenInnerViewIsDestroyed loads an inner view with a plugin, unmounts the frame and asserts the plugin was disposed. The count also pins that only the inner view's plugin goes, not the main view's.

The test suite is not discoverable on my machine (dotnet test reports no tests for any fixture, including the existing ones), so this one has only been verified to compile — worth a look at the CI run.

Test plan

  • Inner view tests pass on CI
  • The host that consumes this sees the detached shadow roots go from the heap snapshot after swapping views

Made with Cursor

Plugins are not part of the react tree, so unmounting an inner view never
reached them. One that registered itself in document level state - a listener
on the document, an entry in a handlers queue - stayed registered, holding its
view's root and the whole shadow dom under it alive. A heap snapshot of a
screen that swaps inner views shows exactly that: detached shadow roots and
their fibers, kept by the text selection handlers queue.

Destroying a view now releases it: its plugins are disposed, its modules are
dropped, and it is marked as released so that a component or a plugin that was
still loading does not attach itself to it.

Guarded by EnsureViewPluginsAreDisposed on the factory.

Co-authored-by: Cursor <cursoragent@cursor.com>
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