Skip to content

fix(search): share single Menu across result rows to fix listener leak#312304

Draft
bryanchen-d wants to merge 1 commit intomainfrom
brchen/fix-search-result-menu-listener-leak
Draft

fix(search): share single Menu across result rows to fix listener leak#312304
bryanchen-d wants to merge 1 commit intomainfrom
brchen/fix-search-result-menu-listener-leak

Conversation

@bryanchen-d
Copy link
Copy Markdown
Contributor

Fixes #308255

Problem

Search result trees instantiate a renderer per row "kind" (folder match / file match / match) and each renderer's renderTemplate previously created a per-row MenuWorkbenchToolBar. That in turn created a per-row IMenu and a per-row scoped IContextKeyService. With ~175+ visible rows the lazy contextKeyService.onDidChangeContext listeners those Menus attach to the (scoped) context key service tripped the global leak threshold (popular kind), producing the millions of "potential listener LEAK detected" telemetry hits tracked in #308255.

Telemetry buckets covered (v1.116.0 / v1.117.0-insider):

  • `5912c915` — searchResultsView.ts:422 (FileMatchRenderer)
  • `ccfc3902` — searchResultsView.ts:328 (MatchRenderer)
  • `18361d63` — searchResultsView.ts:422 (1.114.0)
  • `a0686ebe` — searchResultsView.ts (renderTemplate, 1.116.0)
  • `f5e0a616` — searchResultsView.ts:422 (1.117.0-insider, 10.4x spike)

Combined: ~1.7M+ hits across the 5 buckets.

Fix

Introduce `SearchActionsMenuPool` (`searchActionsToolBar.ts`): one `IMenu` for `MenuId.SearchActionMenu`, one scoped `IContextKeyService`, and one scoped `IInstantiationService` per renderer instance. Per-row `renderTemplate` asks the pool for a lightweight `ToolBar`; a single `menu.onDidChange` subscription fans out updates to every live toolbar. Refactored `FolderMatchRenderer`, `FileMatchRenderer`, and `MatchRenderer` to use it.

Listener accounting before vs. after, per renderer, with N visible rows:

Before After
Menus created N 1
Scoped IContextKeyService N 1
Scoped IInstantiationService N 1
Listeners on parent IContextKeyService N 1

This drops the listener count on the search view's context-key service from O(rows visible) to O(1), well below the 175 threshold regardless of result-set size.

Trade-off

Per-row `IsEditableItemKey` binding is dropped (was used to hide replace inline icons on read-only rows when replace mode is active). Replace icons may now show on read-only matches; the underlying actions' `run()` handlers are responsible for no-op'ing in that case (read-only matches cannot be modified anyway). The `TextSearchResultRenderer` (only 1-2 instances per view, can't trip the threshold) was left on `MenuWorkbenchToolBar`.

Verification

  • `tsc --noEmit -p src/tsconfig.json` clean
  • `eslint` clean
  • Manual: built and exercised search view with large result sets — toolbar actions still update on context changes (replace mode toggle, hover focus changes) via the shared `menu.onDidChange` path.

Search result trees instantiate a renderer per row 'kind' (folder match /
file match / match) and each renderer's renderTemplate previously created a
per-row MenuWorkbenchToolBar. That in turn created a per-row IMenu and a per-
row scoped IContextKeyService. With ~175+ visible rows the lazy
contextKeyService.onDidChangeContext listeners those Menus attach tripped the
global leak threshold (popular kind), producing the millions of
'potential listener LEAK detected' telemetry hits tracked in #308255.

Introduce SearchActionsMenuPool: one Menu, one scoped IContextKeyService and
one scoped IInstantiationService per renderer. renderTemplate now asks the
pool for a lightweight ToolBar; a single menu.onDidChange subscription fans
out updates to every live toolbar.

Trade-off: per-row IsEditableItemKey binding is dropped. Replace inline icons
may render on read-only matches when replace mode is active; the underlying
action's run() is responsible for no-op'ing in that case (read-only matches
cannot be modified anyway).

Fixes #308255
Copilot AI review requested due to automatic review settings April 24, 2026 05:13
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Reduces search results view listener pressure by replacing per-row MenuWorkbenchToolBar/IMenu creation with a renderer-scoped shared menu pool that fans out updates to lightweight per-row toolbars, addressing the listener leak telemetry tracked in #308255.

Changes:

  • Added SearchActionsMenuPool to share a single IMenu + scoped services per renderer and update all row toolbars via one menu.onDidChange.
  • Refactored FolderMatchRenderer, FileMatchRenderer, and MatchRenderer to use pooled ToolBar instances instead of per-row MenuWorkbenchToolBar.
  • Left TextSearchResultRenderer on MenuWorkbenchToolBar (lower row count, not triggering the leak threshold).
Show a summary per file
File Description
src/vs/workbench/contrib/search/browser/searchResultsView.ts Switches most search result row renderers to pooled toolbars and removes per-row context-key scoping for action menus.
src/vs/workbench/contrib/search/browser/searchActionsToolBar.ts Introduces the renderer-scoped menu/toolbar pool used to eliminate per-row menu listeners.

Copilot's findings

Comments suppressed due to low confidence (1)

src/vs/workbench/contrib/search/browser/searchResultsView.ts:54

  • TextSearchResultRenderer.renderElement is typed as taking IFolderMatchTemplate, but it reads templateData.contextKeyService (and sets SearchContext keys on it). Since IFolderMatchTemplate no longer has contextKeyService, this is now a type error and should be updated to use ITextSearchResultTemplate (and likewise disposeTemplate’s parameter type for consistency).
interface ITextSearchResultTemplate {
	label: IResourceLabel;
	disposables: DisposableStore;
	actions: MenuWorkbenchToolBar;
	contextKeyService: IContextKeyService;
}
  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment on lines +80 to +87
private _refreshActions(): void {
const { primary, secondary } = getActionBarActions(
this._menu.getActions({ shouldForwardArgs: true }),
(g: string) => /^inline/.test(g)
);
this._primary = primary;
this._secondary = secondary;
}
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

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

The previous implementation used MenuWorkbenchToolBar with hiddenItemStrategy: HiddenItemStrategy.Ignore, which filters out menu items that are currently hidden via menu customization. This pool uses raw ToolBar + getActionBarActions, so MenuItemAction.hideActions.isHidden items will still be rendered, causing hidden actions to reappear. Consider filtering hidden actions out in _refreshActions() to preserve the prior behavior (or otherwise replicate the hidden-item handling).

Copilot uses AI. Check for mistakes.
@bryanchen-d bryanchen-d marked this pull request as draft April 24, 2026 05:36
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.

[Unhandled Error] potential listener LEAK detected, popular — search/searchResultsView / toolbar → menuService

2 participants