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
- Install Iconify and run its View Icons command, letting the grid render.
- Escape back to the root search, so no extension view is open.
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
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: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 assignsstate = .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.
MenuPanelControllerreuses oneNSHostingViewand never clearsrootVieworclipPathon hide:hide()drops the key handlers anddetachonly doesremoveChildWindow+orderOut, so the last menu shown is retained until a different menu replaces it. AndExtensionCommandScreen.menuContentdoeslet screen = screen, capturing the entireExtensionScreeninto 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 andstate = .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
heap <tinycast-pid> | grep RenderValueExpected: nothing. Actual: the full tree, permanently.
Tinycast version + channel
0.11.3 stable
macOS version
27.0 (26A428)
Contribution
approved