Skip to content

perf: idle CPU, dock icon memory, and closed-workspace retention - #335

Merged
arzafran merged 2 commits into
mainfrom
perf/pass-20260912
Sep 12, 2026
Merged

arzafran merged 2 commits into
mainfrom
perf/pass-20260912

Conversation

@arzafran

@arzafran arzafran commented Sep 12, 2026

Copy link
Copy Markdown
Member

What this does

Programa with one idle terminal burned 12 to 22% of a core doing nothing, and every launch parked a 32 MB dock icon bitmap in memory. Both are gone, and closing a workspace now frees more of it.

Measured on a Debug build of a0dbcc882 at a 1700x1050 window (same script before and after):

Metric Before After
Idle CPU, one terminal (top, 10 s windows) 12 to 22% 1.2%
CG image footprint 32 MB 8 MB
Live Workspace objects with 1 open, after opening and closing 6 7 3

The production process shows the same idle cost: main thread at 17.5% CPU and 214 CPU-minutes over 42 hours of uptime.

Why

  1. TmuxWorkspacePaneOverlayView wrapped its canvas in TimelineView(.animation) unconditionally. Every window installs this overlay at init, and a hidden hosting view still ticks, so each frame reset the root view, dirtied constraints (about 2,500 times per second), and ran an AppKit display cycle whose NSHostingView.updateConstraints acquires an App Nap assertion over a synchronous XPC call. The timeline now mounts only while a flash is within FocusFlashPattern.duration, and the controller re-renders once when the flash ends so the timeline is torn down.
  2. AppIconDark and AppIconLight are 1024 px PNGs declared without a scale, so applicationIconImage rasterized a 2048 px copy on retina. They are now 512pt @2x.
  3. The sidebar row's .onHover closure captured the whole row value including its Workspace, and SwiftUI's context-menu responder tree kept those closures alive after the row was removed. Hover tracking moved to a leaf view that captures only the state binding.

Full measurement log, commands, and the remaining unmeasured candidates are in docs/audits/performance-audit-2026-09-12.md on the maintainer's machine (audit reports are not committed).

Test plan

  • Unit: TmuxWorkspacePaneOverlayTests and WorkspaceContentViewVisibilityTests, 14 passed locally, including the two new tests (timeline gating window; icon decodes at 1024 px / 512pt).
  • Debug app idle CPU re-measured at 1.2% after the change with the same top command.
  • Flash teardown verified live (Codex review question): an lldb counter on the overlay hosting view saw 7 layout invalidations during a notification flash and 0 in the 5 s after it settled, against 341 per 4 s at idle before this PR. CPU during and after two flashes: 1.9 to 2.8%.
  • CI: full unit and UI lanes (sidebar hover reveal is covered by BonsplitTabDragUITests.testMinimalModeSidebarControlsRevealOnlyFromSidebarHover).
  • Manual: hover a sidebar row and confirm the close button still appears; trigger a notification flash on a hidden pane and confirm the ring still pulses once.

…lease closed workspaces

Measured on a Debug build at a 1700x1050 window with one idle terminal
(footprint, top, sample, heap; commands in docs/audits/performance-audit-2026-09-12.md):

- Idle CPU 12-22% -> 1.2%. TmuxWorkspacePaneOverlayView ran TimelineView(.animation)
  permanently, which made every window's overlay NSHostingView re-run
  updateConstraints (and an App Nap XPC round trip) on every display frame.
  The timeline is now mounted only while a flash is within FocusFlashPattern.duration,
  and the overlay controller re-renders once when the flash ends.
- CG image footprint 32 MB -> 8 MB. AppIconDark/AppIconLight were 1024px assets with no
  scale, so applicationIconImage rasterized a 2048px copy; they are now 512pt @2x.
- Live Workspace objects after opening and closing six workspaces: 7 -> 3 with one open.
  The sidebar row's .onHover closure captured the whole row (including the Workspace)
  and SwiftUI's context-menu responder tree kept it alive; hover tracking moved to a
  leaf view that captures only the state binding.
Codex review flagged that reassigning an otherwise identical root view may
not re-run the body, which would leave TimelineView mounted after a flash.
The controller now passes isFlashActive as a stored input: true when a
flash starts, false from the settle callback (which no longer re-reads the
wall clock, so a backwards clock step cannot pin it on).

Verified live with an lldb counter on -[NSView setNeedsUpdateConstraints:]
for the overlay hosting view: 7 hits during a notification flash, 0 hits in
the 5 s after it settled (341 hits per 4 s before this PR).

@darkestdarky-bot darkestdarky-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff addresses idle CPU, dock icon memory, and workspace retention with targeted changes. No code review issues found per the provided standards.

Review coverage: 324/324 diff lines supplied. Partial input (truncated: standards). Inline comments are limited to fully visible, valid right-side hunks. Reviewed commit: 44d7448225bec0a8757e31b87e8b9ccc1df9cb27.

@arzafran
arzafran merged commit 1636122 into main Sep 12, 2026
10 checks passed
@arzafran
arzafran deleted the perf/pass-20260912 branch September 12, 2026 12:49
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