Skip to content

perf: optimize Java Language Server startup time - #4514

Open
angelozerr wants to merge 1 commit into
mainfrom
perf/optimize-startup
Open

angelozerr wants to merge 1 commit into
mainfrom
perf/optimize-startup

Conversation

@angelozerr

@angelozerr angelozerr commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Optimize Java Language Server startup time by eliminating expensive subprocess calls, unnecessary system scans, and redundant disk I/O on the critical startup path.

Before: ~72s | After: ~49ms (99.93% reduction)

Changes

  • getMajorVersion() read release file instead of spawning java -version: Replace jdk-utils subprocess call with a synchronous read of the JDK's release file to extract JAVA_VERSION. Saves ~33s (2 calls at startup).
  • listJdks() skip system scan when unnecessary: Only call listJdks() if no valid embedded JRE is found. When toolingJre is available, defer JDK auto-detection to after server startup via didChangeConfiguration.
  • getJavaConfig() defer JDK auto-detection after startup: Add skipAutoDetection parameter to avoid calling listJdks() before server start. Send full configuration (with auto-detected JDKs) after standardClient.start().
  • getVersion() use context.extension.packageJSON instead of reading package.json from disk: Avoid fs.readFileSync + JSON.parse on every call (3 calls at startup).

Measurements (embedded JRE present, no java.home configured)

Step Before After Gain
getMajorVersion(toolingJre) 33,632ms 26ms -33,606ms
listJdks 16,181ms skipped -16,181ms
resolveRequirements total 49,818ms 31ms -49,787ms
Total before client options 72,771ms 49ms -72,722ms

Test plan

  • Verify extension activates and connects to JDTLS with embedded JRE (no java.home configured)
  • Verify extension activates with explicit java.home setting
  • Verify extension activates without embedded JRE (falls back to system JDK scan)
  • Verify JDK auto-detection still works (runtimes appear in settings after startup)
  • Verify java -version fallback works if release file is missing/malformed

@angelozerr
angelozerr force-pushed the perf/optimize-startup branch 3 times, most recently from c29af18 to 0a07746 Compare September 28, 2026 08:38
@datho7561

Copy link
Copy Markdown
Contributor

Seems like a good idea and a really helpful improvement. Reading through your description and the code, wouldn't this make more sense for these changes to be made in the jdk-utils package?

I'm going to try it out on Windows to see if I notice the performance improvement there.

@angelozerr

angelozerr commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor Author

Seems like a good idea and a really helpful improvement.

Tanks!

Reading through your description and the code, wouldn't this make more sense for these changes to be made in the jdk-utils package?

Your idea is to add new API in jdk-utils like getMajorVersion ? I am not sure it is really relevant?

I'm going to try it out on Windows to see if I notice the performance improvement there.

You don't see performance with your OS? You should almost immediately see Java: Activating...:

image

@datho7561

Copy link
Copy Markdown
Contributor

I don't seem to be running into the slowdown like you mention. The activating step takes around 4 seconds on my Windows computer, before and after this change. I have 7 JDKs installed.

@datho7561

Copy link
Copy Markdown
Contributor

I misunderstood your fix at first, though, I think this is a good idea.

@datho7561

Copy link
Copy Markdown
Contributor

OH I don't have an embedded JRE, that's probably why

@datho7561

Copy link
Copy Markdown
Contributor

It seems like jdk-utils should already be doing this? https://github.com/Eskibear/node-jdk-utils/blob/main/src/index.ts#L394

@datho7561 datho7561 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the other fixes seem like good ideas, but from my understanding, the release file optimization should already be performed by jdk-utils so it's redundant to include it here.

I don't notice a significant improvement in startup time, both before and after this change takes around 4 seconds for me.

@angelozerr

Copy link
Copy Markdown
Contributor Author

,The activating step takes around 4 seconds on my Windows

The timing which is improved is when vscode starts and Java: Activating appears. Do you see some improvment?

@angelozerr

Copy link
Copy Markdown
Contributor Author

I think the other fixes seem like good ideas, but from my understanding, the release file optimization should already be performed by jdk-utils so it's redundant to include it here.

I don't notice a significant improvement in startup time, both before and after this change takes around 4 seconds for me.

I have tried to use 0.7.0 (we use 0.6.0) of jdk-utils whic have this fix and for some reason it is slower.

This PR uses:
const content = fse.readFileSync(releaseFile, 'utf8');

and is fast.

jdk-utils uses:

const content = await fse.readFile(releaseFile, { encoding: "utf-8" });

and it is slow.

Perhaps this problem exists only when vscode-java is started in debug mode.

Is it possible to keep this code in vscode-java and I will create a PR in jdk-utils.

@datho7561

Copy link
Copy Markdown
Contributor

Just to confirm, you were seeing a significant difference in the performance? When I added timing and timed the two solutions, both this PR and the main branch took around 300 ms.

@fbricon

fbricon commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

@datho7561 can you check on windows?

@datho7561

Copy link
Copy Markdown
Contributor

can you check on windows?

I've been testing on Windows. I have 7 JDKs installed (I installed a few to check if that's the issue). The startup time before and after this change is ~300 ms.

Is it possible to keep this code in vscode-java and I will create a PR in jdk-utils.

In the case that the release file is corrupted, both vscode-java and jdk-utils will attempt to read it, which will waste time.

It's generally considered bad practice to use the synchronous version of the file operations, since NodeJS can switch to a different task while the file is being read if the asynchronous readFile is being used, but not if readFileSync is being used. I don't know if that really matters in our case, since I think the vscode-java extension has its own process and we won't be blocking anything else.

Reduce startup time from ~72s to ~49ms by eliminating unnecessary
subprocess calls, system scans, and disk I/O on the critical path:

- Read JDK release file instead of spawning java -version subprocess
  in getMajorVersion() (was ~33s per call)
- Skip listJdks() system scan in resolveRequirements when toolingJre
  is valid - defer JDK discovery after server startup via
  didChangeConfiguration notification (was ~16s)
- Defer addAutoDetectedJdks in getJavaConfig until after server start
  by adding skipAutoDetection parameter
- Use context.extension.packageJSON.version instead of reading and
  parsing package.json from disk in getVersion() (3 calls at startup)

Signed-off-by: azerr <azerr@redhat.com>
@angelozerr
angelozerr force-pushed the perf/optimize-startup branch from 0a07746 to 39f218f Compare September 29, 2026 14:43
@angelozerr

Copy link
Copy Markdown
Contributor Author

In the case that the release file is corrupted, both vscode-java and jdk-utils will attempt to read it, which will waste time.

I have removed my code and use jdk-utils.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants