fix(search): share single Menu across result rows to fix listener leak#312304
fix(search): share single Menu across result rows to fix listener leak#312304bryanchen-d wants to merge 1 commit intomainfrom
Conversation
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
There was a problem hiding this comment.
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
SearchActionsMenuPoolto share a singleIMenu+ scoped services per renderer and update all row toolbars via onemenu.onDidChange. - Refactored
FolderMatchRenderer,FileMatchRenderer, andMatchRendererto use pooledToolBarinstances instead of per-rowMenuWorkbenchToolBar. - Left
TextSearchResultRendereronMenuWorkbenchToolBar(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.renderElementis typed as takingIFolderMatchTemplate, but it readstemplateData.contextKeyService(and sets SearchContext keys on it). SinceIFolderMatchTemplateno longer hascontextKeyService, this is now a type error and should be updated to useITextSearchResultTemplate(and likewisedisposeTemplate’s parameter type for consistency).
interface ITextSearchResultTemplate {
label: IResourceLabel;
disposables: DisposableStore;
actions: MenuWorkbenchToolBar;
contextKeyService: IContextKeyService;
}
- Files reviewed: 2/2 changed files
- Comments generated: 1
| private _refreshActions(): void { | ||
| const { primary, secondary } = getActionBarActions( | ||
| this._menu.getActions({ shouldForwardArgs: true }), | ||
| (g: string) => /^inline/.test(g) | ||
| ); | ||
| this._primary = primary; | ||
| this._secondary = secondary; | ||
| } |
There was a problem hiding this comment.
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).
Fixes #308255
Problem
Search result trees instantiate a renderer per row "kind" (folder match / file match / match) and each renderer's
renderTemplatepreviously created a per-rowMenuWorkbenchToolBar. That in turn created a per-rowIMenuand a per-row scopedIContextKeyService. With ~175+ visible rows the lazycontextKeyService.onDidChangeContextlisteners 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):
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:
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