menus: Optimize one-shot menu action resolution #336733

Merged
benibenj opened 2:54am on September 18, 2026 wanted to merge 443 ฮ” into microsoft/vscode main from
benibenj/agents/perf-fix-menu-actions-caching

<!-- โš ๏ธโš ๏ธ Do Not Delete This! bug_report_template โš ๏ธโš ๏ธ --> <!-- Please read our Rules of Conduct: https://opensource.microsoft.com/codeofconduct/ --> <!-- ๐Ÿ•ฎ Read our guide about submitting issues: https://github.com/microsoft/vscode/wiki/Submitting-Bugs-and-Suggestions --> <!-- ๐Ÿ”Ž Search existing issues to avoid creating duplicates. --> <!-- ๐Ÿงช Test using the latest Insiders build to see if your issue has already been fixed: https://code.visualstudio.com/insiders/ --> <!-- ๐Ÿ’ก Instead of creating your report here, use 'Report Issue' from the 'Help' menu in VS Code to pre-fill useful information. --> <!-- ๐Ÿ”ง Launch with code --disable-extensions to check. --> Does this issue occur when all extensions are disabled?: Yes

  • VS Code Version: 1.139.0-insider (4dbe1643e6189ba7b1bbe542cc0e56a94d9ff132)
  • OS Version: macOS 14.2 (23C64)

Steps to Reproduce:

  1. Open anything that resolves a menu through IMenuService โ€” a tree view row's inline actions, or any context menu.
  2. Profile the renderer. Both MenuInfoSnapshot's constructor and MenuInfo's constructor appear, each performing a full MenuRegistry.getMenuItems copy + sort + context-key collection for the same MenuId.

The problem

MenuInfoSnapshot's constructor calls this.refresh() (src/vs/platform/actions/common/menuService.ts:175). MenuInfo extends it and calls this.refresh() again immediately after super() (menuService.ts:266):

class MenuInfoSnapshot {
    constructor(_id, _collectContextKeysForSubmenus) { this.refresh(); }
}

class MenuInfo extends MenuInfoSnapshot {
    constructor(...) { super(_id, _collectContextKeysForSubmenus); this.refresh(); }
}

Because _sort is virtually dispatched, the base constructor's refresh() resolves this._sort to MenuInfo.prototype._sort (menuService.ts:314) rather than the no-op base implementation at menuService.ts:225 โ€” so both passes perform the full sort, not just the cheaper collection walk. The first pass's _menuGroups is then discarded by the second.

This looks like a regression from 603b0ee03b8 (#219964, "Don't listen on menu changed, method 2"), which extracted MenuInfoSnapshot, moved refresh() into the base constructor, and left the derived call in place.

Impact

In a CPU profile of a tree-view interaction on a workspace with many view/item/context contributions, MenuInfoSnapshot's constructor accounts for 7,381ms (35.3%) of a single 20,904ms renderer task โ€” all of it thrown away. MenuInfo's constructor totals 14,268ms (68.3%).

Full profile and surrounding context in #336496.

Suggested fix

Remove the redundant this.refresh() at menuService.ts:266.

Diff Delta:
443
About 239 Diff Delta/hour
Classified as:  General

benibenj's Description of Work #336495 Menus: MenuInfo runs the full collect+sort twice on every construction

Summary

Optimize the menu-construction part of #336496 without adding a product-wide cache or changing tree-find behavior.


  • Resolve one-shot menu actions directly instead of constructing and disposing an event-tracking MenuImpl.

  • Filter contributions by the current context before sorting, and skip collection of context-key dependencies that only persistent menus need. Apply the same path to submenus.

  • Share action construction with persistent menus, preserving action arguments, alternate commands, enablement, toggled state, hidden state, and submenu notifications.

  • Remove the redundant second MenuInfo.refresh() (fixes #336495).

  • Normalize empty and omitted group names to the same default group in the comparator. This makes sorting consistent with grouping and avoids order differences between filtering before and after sorting.

There is deliberately no cross-call cache or registry-version API: each one-shot request rereads the registry, so in-place descriptor changes and implicit Command Palette exclusions remain current. No per-menu or per-row retained state is added.

Validation



  • npm run gulp compile-client โ€” passed, zero errors.

  • Hygiene/ESLint on all three changed files โ€” passed.

  • Electron MenuService suite โ€” 19 tests passed, including 13 new tests.

  • Three targeted performance/ordering regressions fail against the original implementation and pass with this change.

  • 200 deterministic mixed-group comparisons matched one-shot and persistent-menu output.

Local Windows microbenchmark: 875 registered items, 100 rows, two lookups per row; median of nine alternating runs, with output parity checked and no concurrent build.

| Scenario | Before | After | Speedup |
| --- | ---: | ---: | ---: |
| Row-specific context (16 context values) | 939.3 ms | 74.5 ms | 12.6x |
| All contributions applicable | 1944.1 ms | 1515.1 ms | 1.28x |

These are menu-resolution measurements, not an end-to-end reproduction of the reported 20-second freeze. The tree find widget's full-list refilter remains unchanged, so #336496 should remain open for that separate part.

3 total changed files
Loading changes...
Loading changes...
Youโ€™ve reached the end
You have reached the final โ€œwhy is this here?โ€