Skip to content

perf(menu bar): cache rasterized custom layouts - #3110

Open
thatlev wants to merge 1 commit into
steipete:mainfrom
thatlev:perf/rasterized-menu-layout
Open

perf(menu bar): cache rasterized custom layouts#3110
thatlev wants to merge 1 commit into
steipete:mainfrom
thatlev:perf/rasterized-menu-layout

Conversation

@thatlev

@thatlev thatlev commented Aug 20, 2026

Copy link
Copy Markdown

Summary

This draft pre-renders a custom menu-bar layout into a cached NSImage and leaves the
NSStatusBarButton attributed title empty. The rendered text, icon, accessibility
label, item width, provider overrides, spacing, and layout settings are preserved.

The cache is scoped per status-bar button and invalidates when rendered content, the
leading icon, or the configured gap changes. Recreating the status items clears it.

Why

On a three-display M4 MacBook Pro, AppKit repeatedly replicated and snapshotted a
custom attributed status-item title across displays and Spaces. Samples were dominated
by NSStatusItem._updateReplicants, _redrawReplicantSnapshot, and Core Animation
transaction commits even while the displayed value had not changed.

Environment: M4 MacBook Pro, macOS 27.0 (26A5406e), built-in display plus 144 Hz and
180 Hz external displays. Layout used for both measurements: percentage + pace +
compact runs-out (82% +11% 2d 2h).

Measurement Before With this draft
Process CPU time 1.39 s / 20 s (~6.95% of one core) 0.29 s / 30 s (~0.97% of one core)
6-8 s sample trace ~1,300 CA/status-item replicant stack samples 0 CA commit or replicant-redraw stack samples
RSS during steady 30 s window 105.8 to 106.4 MB

During the original system-wide incident, the correlated processes reached roughly
78% CPU for MenuBarAgent and 108% for WindowServer; those figures include the wider
menu-bar/display workload and are not claimed as CodexBar-only CPU.

Verification

  • swift test --filter 'MenuBarLayoutRendererTests|StatusItemLayoutPaceSignatureTests|StatusItemIconObservationSignatureTests'
  • 74 tests passed on current upstream main
  • Full formatting and lint checks passed in the optimized mirror
  • Packaged, code-signature verified, launch-smoke tested, and run with the existing
    provider/account configuration
  • Visual output remained 82% +11% 2d 2h

Review note

This is deliberately a draft. Existing tests retain the attributed-title path so their
renderer characterization remains unchanged; the production bitmap path would benefit
from a dedicated injectable policy or direct bitmap/cache tests before merge. I would
appreciate maintainer guidance on the preferred shape for that coverage.

@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@thatlev
thatlev marked this pull request as ready for review August 20, 2026 15:40

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4a46d6d730

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

rendered.attributedTitle.draw(
with: titleRect,
options: [.usesLineFragmentOrigin, .usesFontLeading])
image.isTemplate = false

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve inactive-display tinting for rasterized layouts

When inactive-display contrast is disabled and a custom layout contains a normal template icon, forcing the combined bitmap to be non-template prevents AppKit from dimming it on inactive displays. MenuBarLayoutRenderedTitle explicitly surfaces the leading icon separately because only template status images receive this tinting; after this change the entire custom layout remains at full contrast on every monitor, making the inactive-display contrast preference ineffective for these layouts. Preserve the native/template path when inactive tinting is requested, or otherwise account for each replicant's active state.

Useful? React with 👍 / 👎.

Comment on lines +330 to +331
self.rasterizedMenuBarLayoutCache.signatures[buttonID] = signature
self.rasterizedMenuBarLayoutCache.images[buttonID] = image

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Remove raster cache entries with destroyed status items

When a provider is disabled or icons are switched into merged mode, removeProviderStatusItem destroys its status item but leaves these ObjectIdentifier entries behind; switching back creates a new button and inserts another image. Repeated configuration changes therefore grow both dictionaries for the lifetime of the controller, and the existing memory-pressure trim also does not clear this new image cache. Remove the button's entries during status-item teardown and include this cache in rebuildable-cache trimming.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Aug 20, 2026
@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codex review: needs changes before merge. Reviewed August 20, 2026, 12:27 PM ET / 16:27 UTC.

ClawSweeper review

What this changes

The PR caches each custom menu-bar layout as a raster image per status-bar button to avoid repeated AppKit title layout and redraw work.

Merge readiness

⚠️ Needs maintainer review before merge - 4 items remain

Keep this PR open for a focused repair: the raster path changes the established inactive-display contrast behavior and retains images for destroyed status items.

Priority: P2
Reviewed head: 4a46d6d7305938fcce4ef2e702ac615f6912b8d1

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) The PR has useful real-system performance evidence, but two direct correctness issues block merge.
Proof confidence 🐚 platinum hermit (4/6) Sufficient (live_output): The PR body provides after-fix production measurements, launch smoke evidence, and the observed rendered output; refresh that evidence after correcting the two findings and redact private details.
Patch quality 🦐 gold shrimp (3/6) 2 actionable review findings remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (live_output): The PR body provides after-fix production measurements, launch smoke evidence, and the observed rendered output; refresh that evidence after correcting the two findings and redact private details.
Evidence reviewed 6 items Production raster path: The PR routes all non-test custom-layout updates through the raster cache.
Inactive-display rendering contract: Current main documents that only template button images receive inactive-display tinting; attributed-title attachments do not.
Cache lifetime gap: The new cache stores images by button identity, while the existing status-item teardown removes the item without clearing a corresponding raster entry; memory-pressure trimming also does not clear this cache.
Findings 2 actionable findings [P2] Preserve inactive-display tinting for raster layouts
[P2] Evict cached images when status items are removed
Security None None.

Live Verification

Command: swift test --filter 'MenuBarLayoutRendererTests|StatusItemLayoutPaceSignatureTests|StatusItemIconObservationSignatureTests'

Result: FAIL (failed) — execution before step 1 run: sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

Assertions:

  • FAIL expect_output: Test Suite

How this fits together

CodexBar renders provider usage and menu-bar settings into macOS status-bar buttons. The proposed path replaces AppKit’s attributed-title rendering with a cached bitmap image and explicit status-item width.

flowchart LR
A[Provider usage and settings] --> B[Menu-bar layout renderer]
B --> C[Rendered title and icon]
C --> D{Test or production}
D -->|Tests| E[Native attributed title]
D -->|Production| F[Cached raster image]
F --> G[Status-bar button on displays]
Loading

Before merge

  • Preserve inactive-display tinting for raster layouts (P2) - The current high-contrast layout deliberately remains an attributed title so AppKit dims the entire layout on inactive displays. This always flattens it into a non-template bitmap, so that setting's custom-layout path can no longer receive active-state tinting; retain the native path for that mode or render with the appropriate display state.
  • Evict cached images when status items are removed (P2) - Each cache miss retains an image under a button identity, but provider status-item teardown only removes the item and its existing render signature. Switching providers or merged mode repeatedly therefore accumulates stale images; remove that button's cache entries during teardown and clear this cache with the other rebuildable caches.
  • Resolve merge risk (P1) - Rasterizing the high-contrast title as a non-template bitmap prevents AppKit from applying the established inactive-display tint.
  • Resolve merge risk (P1) - Repeated provider enablement or merged-mode changes can retain stale raster images after their status items are destroyed.

Findings

  • [P2] Preserve inactive-display tinting for raster layouts — Sources/CodexBar/StatusItemController+MenuBarLayout.swift:377
  • [P2] Evict cached images when status items are removed — Sources/CodexBar/StatusItemController+MenuBarLayout.swift:329-331
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Production versus test delta production +96, tests +0 The new production-only bitmap path bypasses the existing test execution path and needs focused coverage.

Merge-risk options

Maintainer options:

  1. Repair the raster path before merge (recommended)
    Preserve inactive-display contrast behavior and bound cache lifetime during status-item removal and memory pressure, then refresh focused proof.

Technical review

Best possible solution:

Keep raster caching only where it preserves display-state semantics, evict entries during item teardown and memory-pressure relief, and add direct coverage for the bitmap/cache path.

Do we have a high-confidence way to reproduce the issue?

Not applicable as a bug report; this is a performance optimization, and the body provides detailed after-fix live measurements for the submitted path.

Is this the best way to solve the issue?

No: raster caching is plausible, but the current implementation must retain the existing per-display contrast semantics and bounded cache ownership.

Full review comments:

  • [P2] Preserve inactive-display tinting for raster layouts — Sources/CodexBar/StatusItemController+MenuBarLayout.swift:377
    The current high-contrast layout deliberately remains an attributed title so AppKit dims the entire layout on inactive displays. This always flattens it into a non-template bitmap, so that setting's custom-layout path can no longer receive active-state tinting; retain the native path for that mode or render with the appropriate display state.
    Confidence: 0.97
  • [P2] Evict cached images when status items are removed — Sources/CodexBar/StatusItemController+MenuBarLayout.swift:329-331
    Each cache miss retains an image under a button identity, but provider status-item teardown only removes the item and its existing render signature. Switching providers or merged mode repeatedly therefore accumulates stale images; remove that button's cache entries during teardown and clear this cache with the other rebuildable caches.
    Confidence: 0.96

Overall correctness: patch is incorrect
Overall confidence: 0.97

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 8709988272b8.

Labels

Label changes:

  • add P2: The patch introduces bounded but user-visible menu-bar rendering and cache-lifecycle defects.
  • add merge-risk: 🚨 compatibility: The new non-template bitmap path can change the existing inactive-display contrast behavior.
  • add proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides after-fix production measurements, launch smoke evidence, and the observed rendered output; refresh that evidence after correcting the two findings and redact private details.
  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🐚 platinum hermit and patch quality is 🦐 gold shrimp.
  • add status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (live_output): The PR body provides after-fix production measurements, launch smoke evidence, and the observed rendered output; refresh that evidence after correcting the two findings and redact private details.

Label justifications:

  • P2: The patch introduces bounded but user-visible menu-bar rendering and cache-lifecycle defects.
  • merge-risk: 🚨 compatibility: The new non-template bitmap path can change the existing inactive-display contrast behavior.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🐚 platinum hermit and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (live_output): The PR body provides after-fix production measurements, launch smoke evidence, and the observed rendered output; refresh that evidence after correcting the two findings and redact private details.
  • proof: sufficient: Contributor real behavior proof is sufficient. The PR body provides after-fix production measurements, launch smoke evidence, and the observed rendered output; refresh that evidence after correcting the two findings and redact private details.

Evidence

Acceptance criteria:

  • [P1] swift test --filter 'MenuBarLayoutRendererTests|StatusItemLayoutPaceSignatureTests|StatusItemIconObservationSignatureTests'.
  • [P1] make check.

What I checked:

Likely related people:

  • Peter Steinberger: The locally available provenance for both the renderer contract and status-item teardown traces to the v0.54.0 source snapshot; earlier history is unavailable locally. (role: recent current-main contributor; confidence: medium; commits: 22a2168842a9, 050c121c5eb3; files: Sources/CodexBar/MenuBarLayoutRenderer.swift, Sources/CodexBar/StatusItemController+MenuBarLayout.swift, Sources/CodexBar/StatusItemController.swift)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Correct the inactive-display tint and stale-cache paths.
  • Add focused coverage for the production bitmap/cache behavior and refresh the live measurement after the repair.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@steipete

Copy link
Copy Markdown
Owner

Thanks — the measurements are compelling and NSStatusItem._updateReplicants churn on multi-display setups is a real problem worth killing. A few correctness issues in the current approach before this can land:

  1. lockFocus rasterizes at one backing scale. NSImage.lockFocus() draws into a rep at the main screen's scale factor; on mixed-DPI setups (your exact three-display scenario) the status item text will be blurry on whichever display doesn't match. Use NSImage(size:flipped:drawingHandler:) instead — AppKit re-invokes the handler per backing store and caches the result per-representation, so you keep the "no attributed title → no replicant relayout" win without baking a single DPI.

  2. Template/highlight behavior is lost. image.isTemplate = false plus rasterized labelColor means the content no longer inverts when the item is highlighted (menu open) and no longer follows the menu bar's vibrancy/appearance the way a template image or attributed title does. Today's path adapts because the button re-resolves colors against its effectiveAppearance. With the drawingHandler approach you can resolve colors inside the handler (it runs with the destination context's appearance), or mark the whole image isTemplate = true when the layout is monochrome.

  3. Cache signature includes ObjectIdentifier(rendered.attributedTitle).hashValue. If the renderer allocates a fresh attributed string per pass, that component changes every time and the cache never hits; if it reuses instances, the component is redundant with attributedTitle.hash. Drop the identity components and build the signature from content: the render signature the controller already computes (it feeds lastAppliedProviderIconRenderSignatures) plus appearanceName, gap, and icon content identity — not pointer identity.

  4. Appearance switches. The signature doesn't include the resolved appearance, so a light↔dark switch only invalidates if the attributed string's hash happens to change with its colors. Make appearanceName an explicit signature component.

  5. SettingsStore.isRunningTests gating the new path means zero test coverage of the rasterization. Fine for the AppKit draw itself, but please extract the signature computation and the width math into a testable helper and cover those.

Happy to land this once it draws via drawingHandler, keeps highlight/appearance adaptation, and the cache key is content-based. The cache-clearing hook in recreateStatusItems looks right.

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

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants