Skip to content

[Bug]: An extension's render tree is retained after the command exits #1075

Description

@FezVrasta

What happened?

An extension's render tree survives the command exiting, for the rest of the app's life.

Open an extension command, Escape back to the root search, and the whole tree is still on the heap. Measured on a fresh 0.11.3 launched with MallocStackLogging, after running Iconify's View Icons and escaping out:

14,625   6.5 MB   Swift._DictionaryStorage<String, Tinycast.RenderValue>
 2,403   1.1 MB   Swift._ContiguousArrayStorage<Tinycast.RenderNode>
 1,600   0.2 MB   Swift._ContiguousArrayStorage<Tinycast.RenderValue>

It's deterministic: the same 14,625 across three separate runs including a fresh process, and it never drops afterwards.

It isn't ExtensionManager.state. After that, I opened several more icon extensions and the count stayed at exactly 14,625. Each render assigns state = .rendered(newTree), which would have released the previous one, so something outside the manager owns it.

It isn't every extension. SF Symbols Search and Tailwind's Search Classes both go to zero on the same exit path, and so does an exit via F4 plus the 90-second Pop to Root. So it depends on what you do inside the command, not on extensions in general.

The JS side is fine. The JSC heap is fully reclaimed on exit, 161,414 allocations / 455.6 MB down to 949 / 106 KB, so runtime.shutdown() is doing its job. This is Swift-side only.

Where I think it is, unconfirmed. MenuPanelController reuses one NSHostingView and never clears rootView or clipPath on hide: hide() drops the key handlers and detach only does removeChildWindow + orderOut, so the last menu shown is retained until a different menu replaces it. And ExtensionCommandScreen.menuContent does let screen = screen, capturing the entire ExtensionScreen into the menu content's closures. Pressing ⌘K inside an extension would then hand its whole tree to a hosting view that outlives the session, the runtime shutdown and state = .idle.

A test that would confirm or kill that: open a big extension, press ⌘K, Escape out, and check the retained count. Then open a small extension, press ⌘K there, Escape out. If the retained count collapses to the small one, the menu panel is the owner. If it stays put, it's somewhere else and the above is wrong.

Worth saying separately: the menu panel holding its last content is a retention regardless of extensions. Extensions just make it expensive, because what it captures is a whole render tree.

Steps to reproduce

  1. Install Iconify and run its View Icons command, letting the grid render.
  2. Escape back to the root search, so no extension view is open.
  3. heap <tinycast-pid> | grep RenderValue

Expected: nothing. Actual: the full tree, permanently.

Tinycast version + channel

0.11.3 stable

macOS version

27.0 (26A428)

Contribution

  • I'm willing to implement this myself once this issue is labelled approved

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions