Skip to content

Commit 87834b0

Browse files
committed
perf: optimize Java Language Server startup time
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>
1 parent a0b2508 commit 87834b0

4 files changed

Lines changed: 46 additions & 51 deletions

File tree

‎src/extension.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ export async function activate(context: ExtensionContext): Promise<ExtensionAPI>
224224
const requireStandardServer = (serverMode !== ServerMode.lightWeight) && (!isDebugModeByClientPort || !!process.env['JDTLS_CLIENT_PORT']);
225225
let initFailureReported: boolean = false;
226226

227-
const javaConfig = await getJavaConfig(requirementsData.java_home);
227+
const javaConfig = await getJavaConfig(requirementsData.java_home, true);
228228
javaConfigDeferred.resolve(javaConfig);
229229

230230
// Options to control the language client
@@ -790,6 +790,12 @@ async function doStartStandardServer(context: ExtensionContext, requirements: re
790790
await standardClient.initialize(context, requirements, clientOptions, workspacePath, jdtEventEmitter);
791791
standardClient.start().then(async () => {
792792
standardClient.registerLanguageClientActions(context, await fse.pathExists(path.join(workspacePath, ".metadata", ".plugins")), jdtEventEmitter);
793+
// Send auto-detected JDKs after server startup to avoid blocking initialization
794+
standardClient.getClient().sendNotification(DidChangeConfigurationNotification.type, {
795+
settings: {
796+
java: await getJavaConfig(requirements.java_home),
797+
}
798+
});
793799
});
794800
serverStatusBarProvider.setBusy("Activating...");
795801
return standardClient.getClient();
@@ -1269,7 +1275,7 @@ async function cleanJavaWorkspaceStorage() {
12691275
}
12701276

12711277
async function cleanOldGlobalStorage(context: ExtensionContext) {
1272-
const currentVersion = getVersion(context.extensionPath);
1278+
const currentVersion = getVersion(context);
12731279
const globalStoragePath = context.globalStorageUri?.fsPath; // .../Code/User/globalStorage/redhat.java
12741280

12751281
ensureExists(globalStoragePath);

‎src/javaServerStarter.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,8 @@ function prepareParams(requirements: RequirementsData, workspacePath, context: E
149149
const params: string[] = [];
150150
if (DEBUG) {
151151
const port = isSyntaxServer ? 1045 : 1044;
152-
params.push(`-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=${port},quiet=y`);
152+
const suspend = 'n'; //isSyntaxServer ? 'y' : 'n';
153+
params.push(`-agentlib:jdwp=transport=dt_socket,server=y,suspend=${suspend},address=${port},quiet=y`);
153154
// suspend=y is the default. Use this form if you need to debug the server startup code:
154155
// params.push('-agentlib:jdwp=transport=dt_socket,server=y,address=1044');
155156
}
@@ -297,7 +298,7 @@ function prepareParams(requirements: RequirementsData, workspacePath, context: E
297298
}
298299

299300
const hasJDWP = params.find((param: string) => param.includes('jdwp')) !== undefined;
300-
const extVersion = getVersion(context.extensionPath);
301+
const extVersion = getVersion(context);
301302
const globalStoragePath = path.resolve(context.globalStorageUri?.fsPath, extVersion); // .../Code/User/globalStorage/redhat.java/1.42.0/
302303
const appCDSMode = workspace.getConfiguration().get('java.jdt.ls.appcds.enabled');
303304
const useAppCDS = (appCDSMode === 'on') || (appCDSMode === 'auto' && (isPrereleaseOrInsiderVersion(context)));
@@ -396,7 +397,7 @@ export function getSharedIndexCache(context: ExtensionContext): string {
396397

397398
function resolveConfiguration(context, configDir) {
398399
ensureExists(context.globalStoragePath);
399-
const version = getVersion(context.extensionPath);
400+
const version = getVersion(context);
400401
let configuration = path.resolve(context.globalStoragePath, version);
401402
ensureExists(configuration);
402403
configuration = path.resolve(configuration, configDir);

‎src/requirements.ts‎

Lines changed: 26 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ interface ErrorData {
3535
*/
3636
export async function resolveRequirements(context: ExtensionContext): Promise<RequirementsData> {
3737
let toolingJre: string = await findEmbeddedJRE(context);
38-
let toolingJreVersion: number = await getMajorVersion(toolingJre);
38+
let toolingJreVersion: number = getMajorVersion(toolingJre);
3939
return new Promise(async (resolve, reject) => {
4040
const javaPreferences = await checkJavaPreferences(context);
4141
const preferenceName = javaPreferences.preference;
@@ -60,7 +60,7 @@ export async function resolveRequirements(context: ExtensionContext): Promise<Re
6060
}
6161
invalidJavaHome(reject, msg);
6262
}
63-
javaVersion = await getMajorVersion(javaHome);
63+
javaVersion = getMajorVersion(javaHome);
6464
if (preferenceName === "java.jdt.ls.java.home" || !toolingJre) {
6565
if (javaVersion >= REQUIRED_JDK_VERSION) {
6666
toolingJre = javaHome;
@@ -75,42 +75,28 @@ export async function resolveRequirements(context: ExtensionContext): Promise<Re
7575
}
7676
}
7777

78-
// search valid JDKs from env.JAVA_HOME, env.PATH, SDKMAN, jEnv, jabba, Common directories
79-
const javaRuntimes = await listJdks();
80-
if (!toolingJre) { // universal version
81-
// as latest version as possible.
78+
if (!toolingJre) {
79+
// No tooling JRE found, scan system to find a valid JDK
80+
const javaRuntimes = await listJdks();
8281
sortJdksByVersion(javaRuntimes);
8382
const validJdks = javaRuntimes.filter(r => r.version.major >= REQUIRED_JDK_VERSION);
8483
if (validJdks.length > 0) {
8584
sortJdksBySource(validJdks);
8685
toolingJre = validJdks[0].homedir;
8786
toolingJreVersion = validJdks[0].version.major;
88-
if (!javaHome) { // keep javaHome if set even if it is an older JDK version
87+
if (!javaHome) {
8988
javaHome = toolingJre;
9089
javaVersion = toolingJreVersion;
9190
}
9291
}
93-
} else { // pick a default project JDK/JRE
94-
/**
95-
* For legacy users, we implicitly following the order below to
96-
* set a default project JDK during initialization:
97-
* java.jdt.ls.java.home > java.home > env.JDK_HOME > env.JAVA_HOME > env.PATH
98-
*
99-
* We'll keep it for compatibility.
100-
*/
101-
if (javaHome) {
102-
logger.info(`Use the JDK from '${preferenceName}' setting as the initial default project JDK.`);
103-
} else if (javaRuntimes.length) {
104-
sortJdksBySource(javaRuntimes);
105-
javaHome = javaRuntimes[0].homedir;
106-
javaVersion = javaRuntimes[0].version?.major;
107-
logger.info(`Use the JDK from '${getSources(javaRuntimes[0])}' as the initial default project JDK.`);
108-
} else if (javaHome = await findDefaultRuntimeFromSettings()) {
109-
javaVersion = await getMajorVersion(javaHome);
110-
logger.info("Use the JDK from 'java.configuration.runtimes' as the initial default project JDK.");
111-
} else {
112-
openJDKDownload(reject, "Please download and install a JDK to compile your project. You can configure your projects with different JDKs by the setting ['java.configuration.runtimes'](https://github.com/redhat-developer/vscode-java/wiki/JDK-Requirements#java.configuration.runtimes)");
113-
}
92+
} else if (!javaHome) {
93+
// toolingJre is valid, use it as default project JDK for now.
94+
// The actual project JDK will be resolved later via didChangeConfiguration.
95+
javaHome = toolingJre;
96+
javaVersion = toolingJreVersion;
97+
logger.info(`Use the tooling JRE as the initial default project JDK (will be updated after startup).`);
98+
} else {
99+
logger.info(`Use the JDK from '${preferenceName}' setting as the initial default project JDK.`);
114100
}
115101

116102
if (!toolingJre || toolingJreVersion < REQUIRED_JDK_VERSION) {
@@ -217,10 +203,19 @@ function invalidJavaHome(reject, cause: string) {
217203
}
218204
}
219205

220-
async function getMajorVersion(javaHome: string): Promise<number> {
206+
function getMajorVersion(javaHome: string): number {
221207
if (!javaHome) {
222208
return 0;
223209
}
224-
const runtime = await getRuntime(javaHome, { withVersion: true });
225-
return runtime?.version?.major || 0;
210+
const releaseFile = path.join(javaHome, 'release');
211+
try {
212+
const content = fse.readFileSync(releaseFile, 'utf8');
213+
const match = content.match(/JAVA_VERSION="(\d+)/);
214+
if (match) {
215+
return parseInt(match[1]);
216+
}
217+
} catch {
218+
// release file not found or unreadable
219+
}
220+
return 0;
226221
}

‎src/utils.ts‎

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ function getDirectoriesByBuildFile(inclusions: string[], exclusions: string[], f
220220

221221
const detectJdksAtStart: boolean = getJavaConfiguration().get<boolean>('configuration.detectJdksAtStart');
222222

223-
export async function getJavaConfig(javaHome: string) {
223+
export async function getJavaConfig(javaHome: string, skipAutoDetection?: boolean) {
224224
const origConfig = getJavaConfiguration();
225225
const javaConfig = JSON.parse(JSON.stringify(origConfig));
226226
javaConfig.home = javaHome;
@@ -274,7 +274,7 @@ export async function getJavaConfig(javaHome: string) {
274274
}
275275

276276
javaConfig.telemetry = { enabled: workspace.getConfiguration('redhat.telemetry').get('enabled', false) };
277-
if (detectJdksAtStart) {
277+
if (detectJdksAtStart && !skipAutoDetection) {
278278
const userConfiguredJREs: any[] = javaConfig.configuration.runtimes;
279279
javaConfig.configuration.runtimes = await addAutoDetectedJdks(userConfiguredJREs);
280280
}
@@ -344,14 +344,8 @@ export function resolveActualCause(callstack: any): any {
344344
return callstack;
345345
}
346346

347-
export function getVersion(extensionPath: string): string {
348-
const packagePath = path.resolve(extensionPath, "package.json");
349-
const packageFile = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
350-
if (packageFile) {
351-
return packageFile.version;
352-
}
353-
354-
return '0.0.0';
347+
export function getVersion(context: ExtensionContext): string {
348+
return context.extension.packageJSON?.version || '0.0.0';
355349
}
356350

357351
export function getVSCodeVariablesMap(): any {
@@ -368,19 +362,18 @@ export function getVSCodeVariablesMap(): any {
368362
* @param context The extension context or extension path
369363
* @returns true if the version is a pre-release version or running an insider editor
370364
*/
371-
export function isPrereleaseOrInsiderVersion(context: ExtensionContext | string): boolean {
365+
export function isPrereleaseOrInsiderVersion(context: ExtensionContext): boolean {
372366
return isInsiderEditor() || isPreReleaseVersion(context);
373367
}
374368

375369
/**
376370
* Check if the extension version is a pre-release version.
377371
* Pre-release versions follow the pattern: major.minor.timestamp (e.g., 1.47.1234567890)
378-
* @param context The extension context or extension path
372+
* @param context The extension context
379373
* @returns true if the version is a pre-release version
380374
*/
381-
export function isPreReleaseVersion(context: ExtensionContext | string): boolean {
382-
const extensionPath = typeof context === 'string' ? context : context.extensionPath;
383-
const extVersion = getVersion(extensionPath);
375+
export function isPreReleaseVersion(context: ExtensionContext): boolean {
376+
const extVersion = getVersion(context);
384377
return /^\d+\.\d+\.\d{10}/.test(extVersion);
385378
}
386379

0 commit comments

Comments
 (0)