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

Re-use ExecutorService to avoid creating extra threads and resource leak #2041

Merged

Conversation

mfussenegger
Copy link
Contributor

In both the signature help and in the completion resolve path ad-hoc
cached ThreadPool instances were created and not shutdown properly.

Using the same ExecutorService instance can avoid spawning new threads,
which is cheaper than re-creating new threads all time. A quick
benchmark:

	ExecutorsBenchmark.createAdhocThreadPool          avgt   10  124.947 ± 1.527  us/op
	ExecutorsBenchmark.reuseThreadPool                avgt   10   14.979 ± 1.234  us/op

But more importantly the instances weren't shutdown after use, leaking
resources.

@mfussenegger mfussenegger marked this pull request as draft March 30, 2022 16:03
@mfussenegger mfussenegger marked this pull request as ready for review March 30, 2022 17:30

public CompletionResolveHandler(PreferenceManager manager) {
public CompletionResolveHandler(ExecutorService executorService, PreferenceManager manager) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I like the idea of reusing the thread pool. One thing with this PR is to keep it backward compatible. Currently the new constructor will break VS Code IntelliCode extension, which is calling the old constructor to instantiate a CompletionResolveHandler. (cc: @Eskibear )

Another idea is to move ExecutorService instance to BaseJDTLanguageServer, and other places can access it via getter like JavaLanguageServerPlugin.getProtocol().getCachedThreadPool(). In this way, both syntax server and standard server share the same logic, and no API changes are required in Handlers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure I can change it. I wasn't aware that API compatiblity is a concern.
That it would also increase coupling between the CompletionResolveHandler and the BaseJDTLanguageServer is no problem?

Copy link
Contributor

@Eskibear Eskibear Apr 7, 2022

Choose a reason for hiding this comment

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

How about keeping both two implementation, so downstream extensions like IntelliCode won't break immediately. Later we can update them to call the new constructor if necessary

public CompletionResolveHandler(ExecutorService executorService, PreferenceManager manager);

public CompletionResolveHandler(PreferenceManager manager);

Copy link
Contributor

Choose a reason for hiding this comment

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

How about keeping both two implementation, so downstream extensions like IntelliCode won't break immediately. Later we can update them to call the new constructor if necessary

public CompletionResolveHandler(ExecutorService executorService, PreferenceManager manager);

public CompletionResolveHandler(PreferenceManager manager);

This may be an option, but it would require the downstream extensions to make the same changes again. If we could make this benefit available to downstream extensions for free, I would vote for it.

Copy link
Contributor

Choose a reason for hiding this comment

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

If we could make this benefit available to downstream extensions for free

Downstream extension E.g. IntelliCode is using CompletionResolveHandler(PreferenceManager manager). Can we just change the implementation of it to use a shared ExecutorService?

Copy link
Contributor

Choose a reason for hiding this comment

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

yes, that's what I wanted in my first comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had a quick go at this but unit tests fail because getProtocol returns null.

Are there already tests in place that set that up so I could have a look at how it's done?

Copy link
Contributor

Choose a reason for hiding this comment

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

@mfussenegger mfussenegger marked this pull request as draft April 14, 2022 10:22
@mfussenegger mfussenegger force-pushed the reuse-executor-service branch 2 times, most recently from 93a4c41 to 43698f3 Compare April 15, 2022 14:55
@jdneo
Copy link
Contributor

jdneo commented Sep 23, 2022

@mfussenegger Will you keep working on this PR? The idea is really good and I hope we can include it.

If you do not have time to work on it recently, would you mind me to append commits to this PR?

@mfussenegger
Copy link
Contributor Author

If you do not have time to work on it recently, would you mind me to append commits to this PR?

It might be a while until I'd get back to it, so feel free to pick it up and continue

In both the signature help and in the completion resolve path ad-hoc
cached ThreadPool instances were created and not shutdown properly.

Using the same ExecutorService instance can avoid spawning new threads,
which is cheaper than re-creating new threads all time. A quick
benchmark:

	ExecutorsBenchmark.createAdhocThreadPool          avgt   10  124.947 ± 1.527  us/op
	ExecutorsBenchmark.reuseThreadPool                avgt   10   14.979 ± 1.234  us/op

But more importantly the instances weren't shutdown after use, leaking
resources.
@jdneo jdneo force-pushed the reuse-executor-service branch 3 times, most recently from a65a163 to cb4a78f Compare October 9, 2022 01:26
Signed-off-by: sheche <sheche@microsoft.com>
@jdneo jdneo marked this pull request as ready for review October 9, 2022 02:24
@jdneo
Copy link
Contributor

jdneo commented Oct 9, 2022

Did some test on Windows with this change.

I kept triggering the completion resolving request by iterating the completion list for 30 times, with following code:

String s;
s.|

Time cost comparation

Avg. time cost for each completion resolving request:

Before After
53.3ms 49.8ms

The time difference is not significant, with this change, the time cost for the resolving completion is slightly slower than before.

Memory comparation

I also captured the JVM memory status during the experiment.

Before:
origin

After:
after

Both case the heap size continuously increases, no significant difference can be observed about the memory cost.

But the thread number is different. Without the change, the thread number keeps growing (since each request will create a new thread pool). While with this change, we can observe that the thread number is very stable, since all the request reuses the existing thread pool.

@jdneo
Copy link
Contributor

jdneo commented Oct 10, 2022

@Eskibear @testforstephen I've updated the PR, please review when you have time :)

Copy link
Contributor

@testforstephen testforstephen left a comment

Choose a reason for hiding this comment

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

LGTM

@testforstephen testforstephen added this to the End October milestone Oct 10, 2022
Signed-off-by: sheche <sheche@microsoft.com>
@testforstephen testforstephen merged commit 4e751a8 into eclipse-jdtls:master Oct 10, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants