The bug report, as filed by my own hands: hold the down arrow in a file list and the cursor stutters its way down instead of gliding. A file manager that can't scroll smoothly is a car that can't idle, so this jumped the queue.
I assumed the table view. It's the obvious suspect — thousands of rows, custom drawing, selection updates on every step. So I profiled a key-repeat session, opened the heaviest main-thread stack, and found… menu code. With everything included, the main thread was 61% busy during key repeat, and the hot stacks weren't under the table view at all. They were under the menu bar.
Nobody was looking at the menu bar. No menu was open. Why is the menu bar doing work at all while I hold an arrow key?
Here's the mechanism. Every keyDown that reaches a window also has to answer the
question "is this a menu shortcut?" AppKit answers it by walking the menu tree — and before it
checks a menu's key equivalents, it gives that menu's delegate a chance to bring the items up to
date. Which means:
menuNeedsUpdate(_:) is not "the user opened a menu".
It also fires for every menu in the bar, on every key press, during key-equivalent
resolution. Hold a key at ~30 repeats per second with nine top-level menus, and your delegate
runs a few hundred times per second — while the user believes the menu bar is idle.
My delegate was a decorator: it walks menu items and annotates the ones that need a Pro badge, using localized titles to match items. The implementation was written with "the user just opened a menu" in mind, so being called meant: re-decorate all nine menus, and rebuild the localized title tables it matches against — from scratch, every call. Per keypress. The work was correct; the assumption about how often it runs was fiction.
NSMenu; touching the other eight is volunteering.Result: main-thread occupancy during key repeat went from 61% to 2%, and the cursor glides. No table-view changes at all — the obvious suspect was innocent the whole time.
NSMenuDelegate, profile before you blame your views. Search your stacks for
menuNeedsUpdate and key-equivalent resolution.menuNeedsUpdate as a hot path with a per-call budget of roughly nothing.
Do per-menu work only, cache everything derivable.@MainActor with a @preconcurrency NSMenuDelegate conformance, and
then it can touch NSMenu synchronously — no hopping, which matters at this call
frequency.