TreeView performance degradation caused by MenuService with heavy context menus
- Dominant language
- TypeScript
- Stars
- 193k
- Forks
- 42.4k
- PR merge metrics
- PR metrics pending
Description
I have reached the point where extension TreeView scrolling FPS degrades significantly because of heavy context menus.
I deliberately avoided using `=~` in `when` expressions, anticipating possible performance issues with large menu counts, but still eventually reached the point where animation frame computation during scrolling rises to 100–200 ms. However, this precaution may not actually affect scrolling performance.
For reference, here is the approximate size of the extension `view/item/context` menu definition, not including submenus (submenus increase the total roughly x3, though `when` are less complex there):
view/item/context
```json
"view/item/context": [
{
"command": "focus.explore.newFile",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (focus.esHidCmds || focus.mExpNewPath) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)",
"group": "11@1"
},
{
"command": "focus.explore.newFolder",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (focus.esHidCmds || focus.mExpNewPath) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)",
"group": "11@2"
},
{
"submenu": "focus.grpOpen",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && ((focus.esHidCmds && focus.wssGrpOpenHC) || (!focus.esHidCmds && focus.wssGrpOpen))",
"group": "11@3"
},
{
"command": "focus.path.open",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && ((focus.esHidCmds && !focus.wssGrpOpenHC) || (!focus.esHidCmds && !focus.wssGrpOpen)) && (((focus.esHidCmds || focus.mExpOpen) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpen) && (viewItem == tFile || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContOpen) && (viewItem == tGroup || viewItem == tWS)))",
"group": "11@3"
},
{
"command": "focus.path.openToTheSide",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && ((focus.esHidCmds && !focus.wssGrpOpenHC) || (!focus.esHidCmds && !focus.wssGrpOpen)) && (((focus.esHidCmds || focus.mExpOpenTTS) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpenTTS) && (viewItem == tFile || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContOpenTTS) && (viewItem == tGroup || viewItem == tWS)))",
"group": "11@4"
},
{
"command": "focus.path.openAbove",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && ((focus.esHidCmds && !focus.wssGrpOpenHC) || (!focus.esHidCmds && !focus.wssGrpOpen)) && (((focus.esHidCmds || focus.mExpOpenAbove) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpenAbove) && (viewItem == tFile || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContOpenAbove) && (viewItem == tGroup || viewItem == tWS)))",
"group": "11@5"
},
{
"command": "focus.path.openBelow",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && ((focus.esHidCmds && !focus.wssGrpOpenHC) || (!focus.esHidCmds && !focus.wssGrpOpen)) && (((focus.esHidCmds || focus.mExpOpenBelow) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpenBelow) && (viewItem == tFile || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContOpenBelow) && (viewItem == tGroup || viewItem == tWS)))",
"group": "11@6"
},
{
"command": "focus.path.openLeft",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && ((focus.esHidCmds && !focus.wssGrpOpenHC) || (!focus.esHidCmds && !focus.wssGrpOpen)) && (((focus.esHidCmds || focus.mExpOpenLeft) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpenLeft) && (viewItem == tFile || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContOpenLeft) && (viewItem == tGroup || viewItem == tWS)))",
"group": "11@7"
},
{
"command": "focus.path.openRight",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && ((focus.esHidCmds && !focus.wssGrpOpenHC) || (!focus.esHidCmds && !focus.wssGrpOpen)) && (((focus.esHidCmds || focus.mExpOpenRight) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpenRight) && (viewItem == tFile || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContOpenRight) && (viewItem == tGroup || viewItem == tWS)))",
"group": "11@8"
},
{
"command": "focus.path.openWith",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && ((focus.esHidCmds && !focus.wssGrpOpenHC) || (!focus.esHidCmds && !focus.wssGrpOpen)) && (((focus.esHidCmds || focus.mExpOpenWith) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpenWith) && (viewItem == tFile || viewItem == tFolderEM)))",
"group": "11@9"
},
{
"command": "focus.path.revealInFileManager",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpRvlFM) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusRvlFM) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContRvlFM) && (viewItem == tGroup || viewItem == tWS)))",
"group": "11@10"
},
{
"command": "focus.path.revealInExplorer",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpRvlEV) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusRvlEV) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContRvlEV) && (viewItem == tGroup || viewItem == tWS)))",
"group": "11@11"
},
{
"command": "focus.path.openInTerminal",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpOpenInT) && (viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpenInT) && (viewItem == tFolder || viewItem == tFolderEM)))",
"group": "11@12"
},
{
"command": "focus.path.find",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpFind) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusFind) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContFind) && (viewItem == tGroup || viewItem == tWS)))",
"group": "12@1"
},
{
"command": "focus.path.compareSelected",
"when": "((view == focus.activity && focus.activity.sel2F) || (view == focus.explorer && focus.explorer.sel2F) || (view == focus.scm && focus.scm.sel2F) || (view == focus.debug && focus.debug.sel2F) || (view == focus.test && focus.test.sel2F)) && (((focus.esHidCmds || focus.mExpCompare) && (viewItem == tFileE)) || ((focus.esHidCmds || focus.mFocusCompare) && (viewItem == tFile)))",
"group": "13@1"
},
{
"command": "focus.path.compareWithSelected",
"when": "((view == focus.activity && !focus.activity.sel2F) || (view == focus.explorer && !focus.explorer.sel2F) || (view == focus.scm && !focus.scm.sel2F) || (view == focus.debug && !focus.debug.sel2F) || (view == focus.test && !focus.test.sel2F)) && focus.esSelForCmpr && (((focus.esHidCmds || focus.mExpCompare) && (viewItem == tFileE)) || ((focus.esHidCmds || focus.mFocusCompare) && (viewItem == tFile)))",
"group": "13@2"
},
{
"command": "focus.path.selectForCompare",
"when": "((view == focus.activity && !focus.activity.sel2F) || (view == focus.explorer && !focus.explorer.sel2F) || (view == focus.scm && !focus.scm.sel2F) || (view == focus.debug && !focus.debug.sel2F) || (view == focus.test && !focus.test.sel2F)) && (((focus.esHidCmds || focus.mExpCompare) && (viewItem == tFileE)) || ((focus.esHidCmds || focus.mFocusCompare) && (viewItem == tFile)))",
"group": "13@3"
},
{
"command": "focus.path.openTimeline",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpOpenTL) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusOpenTL) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)))",
"group": "14@1"
},
{
"command": "focus.path.cut",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpCB) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusCB) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContCB) && (viewItem == tGroup || viewItem == tWS)))",
"group": "15@1"
},
{
"command": "focus.path.copy",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpCB) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusCB) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContCB) && (viewItem == tGroup || viewItem == tWS)))",
"group": "15@2"
},
{
"command": "focus.path.paste",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpCB) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusCB) && (viewItem == tFolder || viewItem == tFolderEM))) && focus.esCBItems",
"group": "15@3"
},
{
"command": "focus.path.copyPath",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpCopyP) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusCopyP) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContCopyP) && (viewItem == tGroup || viewItem == tWS)))",
"group": "16@1"
},
{
"command": "focus.path.copyRelativePath",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpCopyRP) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusCopyRP) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContCopyRP) && (viewItem == tGroup || viewItem == tWS)))",
"group": "16@2"
},
{
"command": "focus.tools.typescript.findFileReferences",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && tsSupportsFileReferences && (((focus.esHidCmds || focus.mExpTSFFR) && viewItem == tFileE) || ((focus.esHidCmds || focus.mFocusTSFFR) && viewItem == tFile))",
"group": "17@1"
},
{
"command": "focus.pick",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpPick) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusPick) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContPick) && (viewItem == tGroup || viewItem == tWS)))",
"group": "18@1"
},
{
"command": "focus.scope",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpScope) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusScope) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mContScope) && (viewItem == tGroup || viewItem == tWS)))",
"group": "18@2"
},
{
"submenu": "focus.arrange",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (focus.esHidCmds || focus.mFocusArrange) && (viewItem != tFileE && viewItem != tFolderE)",
"group": "19@1"
},
{
"submenu": "focus.customize",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpCust) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusCust) && (viewItem != tFileE && viewItem != tFolderE)))",
"group": "19@2"
},
{
"command": "focus.unfocus",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (focus.esHidCmds || focus.mFocusUnfocus) && (viewItem != tFileE && viewItem != tFolderE)",
"group": "20@1"
},
{
"command": "focus.explore.focus",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (focus.esHidCmds || focus.mExpFocus) && (viewItem == tFileE || viewItem == tFolderE)",
"group": "20@2"
},
{
"command": "focus.path.rename",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpRename) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusRename) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)))",
"group": "21@1"
},
{
"command": "focus.path.delete",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && (((focus.esHidCmds || focus.mExpDelete) && (viewItem == tFileE || viewItem == tFolderE || viewItem == tFolderEM)) || ((focus.esHidCmds || focus.mFocusDelete) && (viewItem == tFile || viewItem == tFolder || viewItem == tFolderEM)))",
"group": "21@2"
},
{
"command": "focus.unfocus",
"when": "(view == focus.activity || view == focus.explorer || view == focus.scm || view == focus.debug || view == focus.test) && focus.unfocusButton == true && (viewItem != tFileE && viewItem != tFolderE && viewItem != tWS)",
"group": "inline@1"
}
]
```
When `view/item/context` is empty, scrolling FPS returns to normal, which strongly suggests that menu processing is the dominant contributor.
## Performance analysis
The following is profiling data from an average heavy animation frame during scrolling. Sorry if the formatting is not ideal — this is mostly intended for analysis and AI-assisted parsing.
```
Animation frame (130 ms)
> (anonymous)
> o
> (anonymous)
> _performSmoothScrolling
> _setState
> fire
> _deliver
> (anonymous)
> fire
> _deliverQueue
> _deliver
> onScroll
> render
> transact
> (anonymous)
> insertItemInDOM (127.5 ms)
```
`insertItemInDOM` is from `src/vs/base/browser/ui/list/listView.ts`
and is the main contributor to frame computation.
```
insertItemInDOM (127.5 ms)
- renderElement (122.6 ms)
-- renderElement 1 (66.3 ms)
-- renderElement 2 (55.4 ms)
-- renderElement 3 (0.9 ms)
- insertBefore (4.8 ms)
- updateItemInDOM (0.2 ms)
```
Assuming this is the expected virtualized DOM work caused by rows entering the viewport, I inspected the nested renderElement calls.
#### renderElement 1
```
insertItemInDOM (127.5 ms)
> renderElement
> renderElement 1 (66.3 ms)
> renderElement
> renderElement
> getResourceActions
> getActions
> getMenuActions (59.1 ms)
```
`getActions` is from `src/vs/workbench/browser/parts/views/treeView.ts`
`getMenuActions` is from `src/vs/platform/actions/common/menuService.ts`
This path alone takes `59.1 ms / 66.3 ms`
#### renderElement 2
```
insertItemInDOM (127.5 ms)
> renderElement
> renderElement 2 (55.4 ms)
> getAriaLabel
> getAriaLabel
> getAriaLabel
> getActions
> getMenuActions (54.8 ms)
```
This reaches the same `getMenuActions` path again and takes `54.8 ms / 55.4 ms`
#### renderElement 3
```
insertItemInDOM (127.5 ms)
> renderElement
> renderElement 3 (0.9 ms)
> unrender
> remove
```
`unrender` is from `src/vs/base/browser/ui/list/listWidget.ts`
So it appears that the heaviest menu processing paths are executed multiple times for the same element during a single frame.
Another observation is that additional `_performSmoothScrolling` calls sometimes appear within the same animation frame execution. This may be intended batching or the result of performance choking, but it additionally delays frame finalization.
### Deeper inspection of getMenuActions
```
getMenuActions (59.1 ms)
- constructor (46 ms)
-- constructor (46 ms)
--- refresh (23.5 ms)
---- _collectContextKeysAndSubmenuIds (11.5 ms)
---- _sort (10.5 ms)
---- getMenuItems (0.7 ms)
--- constructor (19.9 ms)
---- refresh (18 ms)
----- _sort (10.3 ms)
----- _collectContextKeysAndSubmenuIds (5 ms)
----- getMenuItems (1.4 ms)
- getActions (13.1 ms)
```
The following methods dominate execution time:
`_sort`
`_collectContextKeysAndSubmenuIds`
both from `src/vs/platform/actions/common/menuService.ts`
In this trace `_sort + _collectContextKeysAndSubmenuIds` = `37.3 ms / 59.1 ms`
In total, there are 4 `refresh()` calls for the same element and `MenuId` during a single `insertItemInDOM`, resulting approximately in:
(_sort + _collectContextKeysAndSubmenuIds) × 4
even though menu expressions and menu item lists remain unchanged during that frame.
The ongoing PR specifically addresses `_sort` and `_collectContextKeysAndSubmenuIds` by caching their results, which effectively removes them from the performance trace.
However, there may still be `getActions`, `setResource` (not investigated), other, or higher-level optimizations possible around menu recomputation and duplicate processing paths.
Contributor guide
Assessment
This issue has not been assessed yet.