microsoft / microsoft/vscode

TreeView performance degradation caused by MenuService with heavy context menus

Open
#324,355 2 comments 1 reaction 1 assignee Claimed by @alexr00 View on GitHub
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.