perf(icons): chrome glyphs come from one shared bitmap per glyph, size and scale (REOPEN-F7 part A2)
Summary
REOPEN-F7 part A2. After a reopen, AppKit's first display of the restored window spends 37-43 ms on the main thread before the first frame. Whole stall stacks (10 warm realistic reopens, fleet job cea96828b32f4e6d7f183e8d on cmux18) show chrome glyphs rasterized one view at a time there: -[_NSSimpleImageView updateLayer] -> NSImage CGImageForProposedRect -> NSCGImageSnapshotRep / NSImageSymbolRepProvider -> CGBitmapAllocateData.
Cause: NSImage.icon(_:size:style:) / icon(symbol:size:style:) (about 105 callers) returned a new drawing-handler NSImage per call, and AppKit keeps one raster per image instance. N views showing the same glyph rasterized it N times.
Chrome's model is one shared image cache. IconRasters (CmuxNextIcons) draws each glyph once per key (icon name, style, SF Symbol, pack family overlay, side, scale 1x/2x), from any thread, in black on clear (template). NSImage.icon returns a new template NSImage per call whose representations wrap the shared bitmaps, so a caller that sets its own image's size or description changes no other. The drawing code is the same as before (pack layers y-down, or the SF Symbol fitted and centered).
- Bounded (lead, STATUS 7066): at most 512 bitmaps and 8 MB, least recently used dropped first; every bitmap dropped on a memory pressure warning (its own kernel dispatch source on a utility queue). One unfair lock held only to look up or store; drawing runs outside it; no main-thread hop.
- Three fallback glyphs were drawn larger than they were asked for; a bitmap would be stretched up, so each now asks for its drawn size: the media row's artwork (40 pt), the import row's browser icon (30 pt), an extension button with no icon (the button's side).
- Architecture:
IconRasters.sharedisfileprivate(onlyNSImage.iconreaches it) and lands under a reviewed, dated exception,plans/cmux-next/arch-exceptions/icon-rasters.json(stateful_singletons+1, expiry 2026-11-16). Follow-up #32444 moves callers to an injected icon provider and removes it.
Not in this PR: TitlebarBandButton.renderSymbol and SidebarToggleIcon make their own symbol images per instance (not through NSImage.icon).
Testing
Regression tests (committed first):
IconRasterSharingTests: 8 identical glyphs (a pack icon at 1x and 2x, an SF Symbol) give one bitmap per key, and the images' own representations are that bitmap; another size is another key; each caller owns its image (size, description).IconRastersBoundTests: the count limit drops the least recently used; the byte limit holds; a drop keeps nothing.
Fleet runs (package-test-lane.sh suite, through proof-step.py):
- Red, 37d98175 (the test on feat 82eddd7c): job 3c8c89abf8e3279852e971c9:
IconRasterSharingTestsFAILS: 8 bitmaps for 8 identical glyphs, at 1x, at 2x and for the SF Symbol. - Green, head afa4fd35: job 94e766941b50e2cc8a6e53ba:
IconRasterSharingTests,IconRastersBoundTests,BrowserChromeIconsTests,BrowserToolbarExtensionsButtonTests,SidebarItemGlyphStaysTestsall passed. - Compile checks passed at the head (job 6053116c) and at the red ref.
Review: an independent review subagent gave SHIP at dd6e954f (3 P2 fixed since) and SHIP at 0a428af2; the exception file carries that receipt.
Launch A/B (A1 + A2 together, lead order): AB_LINE
Observed / Not run
- Observed: the stall stacks; the red and green steps.
- Not run: a cold first-launch measurement (the first miss of each key draws both scales; review P3).
Changelog
none
Proof
No capture: the glyphs draw the same pixels; the tests pin the sharing.
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Chrome glyphs now come from one shared bitmap per glyph, size, and scale, so N views showing the same icon rasterize it once instead of N times. That removes the 37–43 ms AppKit stall in the first display pass after reopening a window.
NSImage.iconstill returns a new templateNSImageper call; the image's representations wrap the shared bitmaps, so callers setting size or description change no other image.- The cache holds at most 512 bitmaps and 8 MB, drops the least recently used first, and clears on memory pressure.
- Three fallback glyphs (media row artwork, import row browser icon, extension button without an icon) now ask for their drawn size so a bitmap is not stretched up.
- Tests:
IconRasterSharingTestspins the sharing;IconRastersBoundTestspins the limits and drops.