Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Memory leak in plugin tree views #10404

Closed
xcariba opened this issue Nov 10, 2021 · 6 comments · Fixed by #12353
Closed

Memory leak in plugin tree views #10404

xcariba opened this issue Nov 10, 2021 · 6 comments · Fixed by #12353
Labels
help wanted issues meant to be picked up, require help vscode issues related to VSCode compatibility

Comments

@xcariba
Copy link
Contributor

xcariba commented Nov 10, 2021

Bug Description:

Theia always fetches whole tree in plugin tree views when change event is fired. (It calls tree refresh without argument and it causes complete refresh).
In tree-views.ts Theia calls clearChildren to dispose previous commands and remove children before new child elements are fetched.
When full refresh is called it begins with root node (with empty id) and old nodes are not properly disposed/removed. If tree is huge and is constantly updating it consumes several gb of memory and sometimes even crashes. I found out that old commands are not disposed and still kept in command registry (and maybe somewhere else). Same tree works fine in VSCode.

Steps to Reproduce:

  1. Install vscode extension that has huge tree view and is constanlty updating.
  2. Open it
  3. Open chrome inspector (memory tab).
  4. Start tree updates and wait for some time.
  5. In chrome inspector memory consumption will increase (from 35mb to 600-900mb in my case)

Additional Information

  • Operating System: Fedora
  • Theia Version: 1.16, master.
@vince-fugnitto vince-fugnitto added help wanted issues meant to be picked up, require help vscode issues related to VSCode compatibility labels Nov 10, 2021
@tsmaeder
Copy link
Contributor

When full refresh is called it begins with root node (with empty id) and old nodes are not properly disposed/removed

@xcariba What makes you say that old nodes are not properly disposed? How would nodes escape being disposed when refreshing the root with id ''?

@xcariba
Copy link
Contributor Author

xcariba commented Jan 30, 2023

@tsmaeder I have a custom (proprietary) extension that uses tree view and first implementation was constantly updating. In chrome developer tools I found out that when frequent updates occur memory consumption can go (from ~35mb up to ~1Gb). So I optimized my code so that it sends updates a lot less frequent. It still consumes a lot of memory after some time, but now it can work for a ~day before it reaches high numbers.
I made several memory dumps and it had a lot of old versions of tree view item objects and its parts (like commands). Maybe some references does not allow garbage collector to dispose of these objects.

@tsmaeder
Copy link
Contributor

@xcariba do the "new" elements have the same id's as the "old" elements or do they always change?

@xcariba
Copy link
Contributor Author

xcariba commented Jan 30, 2023

@tsmaeder they have same ids

@pisv
Copy link
Contributor

pisv commented Mar 23, 2023

Looking at the current implementation of the TreeViewExtImpl<T>.getChildren(parentId: string) method, every time this method is called with parentId === '', a new root node is created and placed into this.nodes map, replacing the old root node (if any) in the map:

if (parentId === '') {
const rootNodeDisposables = new DisposableCollection();
this.nodes.set(parentId, { id: '', disposables: rootNodeDisposables, dispose: () => { rootNodeDisposables.dispose(); } });
}

The newly created top-level children are pushed to parentNode.children:

if (parentNode) {
const children = parentNode.children || [];
children.push(node);
parentNode.children = children;
}

However, parentNode is the old root rather than the new one:

async getChildren(parentId: string): Promise<TreeViewItem[] | undefined> {
const parentNode = this.nodes.get(parentId);

This causes two issues with regard to memory leaks:

  1. The root node that is currently in this.nodes map has no children and therefore nothing is ever done in the clearChildren call for the root node:

this.clearChildren(parentNode);

As a result of the child nodes not being disposed, the associated commands placed into the CommandsConverter.commandsMap will not be deleted and the map will grow up each time the tree view is refreshed:

this.commandsMap.set(id, command);
disposables.push(new Disposable(() => this.commandsMap.delete(id)));

  1. Each root node implicitly holds a reference to the 'previous' root node (where its children are incorrectly added) due to the parentNode variable being in a closure of the arrow function of the node's dispose property:

this.nodes.set(parentId, { id: '', disposables: rootNodeDisposables, dispose: () => { rootNodeDisposables.dispose(); } });

As a result of this 'chaining' of the trees, the nodes accumulate on each refresh. This can cause a massive memory leak.

The following screenshots illustrate the issue when opening Theia on the vscode-extension-samples/helloworld-sample workspace and using the Node Dependencies view contributed by the vscode-extension-samples/tree-view-sample:

Screenshot 2023-03-23 at 12 17 01

Screenshot 2023-03-23 at 12 12 21

Screenshot 2023-03-23 at 16 38 26

Screenshot 2023-03-23 at 16 38 53

As can be seen, the nodes accumulate each time the view is refreshed.


It looks like it can be fixed by reusing the single root node instead of creating a new one each time the getChildren method is called with parentId === ''. I have tried the fix locally and it seems to work fine:

Screenshot 2023-03-23 at 12 59 16

Screenshot 2023-03-23 at 13 01 20

I can try to provide the patch, if needed.

pisv added a commit to pisv/theia that referenced this issue Mar 27, 2023
Fixes a bug in `TreeViewExtImpl<T>.getChildren(parentId: string)`,
where newly created top-level children were added to the old root node
instead of the new one. This caused a memory leak by retaining an implicit
reference to the old root node from the new one and failing to properly track
and, hence, dispose the top-level children.

The fix ensures that old nodes get disposed and become subject to garbage
collection by reusing the single root node instead of creating a new one
on full refresh.

See eclipse-theia#10404 (comment)
for more information.

Fixes eclipse-theia#10404.
@pisv
Copy link
Contributor

pisv commented Mar 27, 2023

Since this is a help wanted issue, I have submitted a PR proactively. Although the fix is not complicated, hopefully it can still help save a keystroke or two.

tsmaeder added a commit to pisv/theia that referenced this issue Apr 12, 2023
tsmaeder pushed a commit that referenced this issue Apr 12, 2023
Fixes a bug in `TreeViewExtImpl<T>.getChildren(parentId: string)`,
where newly created top-level children were added to the old root node
instead of the new one. This caused a memory leak by retaining an implicit
reference to the old root node from the new one and failing to properly track
and, hence, dispose the top-level children.

The fix ensures that old nodes get disposed and become subject to garbage
collection by reusing the single root node instead of creating a new one
on full refresh.

See #10404 (comment)
for more information.

Fixes #10404.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted issues meant to be picked up, require help vscode issues related to VSCode compatibility
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants