Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 26 additions & 9 deletions scripts/test-appcds.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ const os = require('os');
const path = require('path');
const { createTsLoader } = require('../e2e/harness/tsLoader');

const { appCdsJvmOptions } = createTsLoader()('src/install/fsUtils.ts');
const { appCdsJvmOptions, hasCompactObjectHeaders, COMPACT_OBJECT_HEADERS } = createTsLoader()('src/install/fsUtils.ts');

// the server runs with compact object headers unless javaOpts say otherwise (the last option wins)
const COMPACT = [COMPACT_OBJECT_HEADERS];

function install(root, release = 'JAVA_VERSION="25.0.1"\n') {
const runtime = path.join(root, 'wurst-runtime');
Expand All @@ -32,42 +35,56 @@ try {
const { java, jar, compiler } = install(tmp);

// the options: a JDK managed archive next to the jar, and no JVM log lines on the protocol stream
const options = appCdsJvmOptions(java, jar);
assert.strictEqual(COMPACT_OBJECT_HEADERS, '-XX:+UseCompactObjectHeaders');
assert.strictEqual(hasCompactObjectHeaders([]), false);
assert.strictEqual(hasCompactObjectHeaders(COMPACT), true);
assert.strictEqual(hasCompactObjectHeaders([...COMPACT, '-Xmx2g']), true);
assert.strictEqual(hasCompactObjectHeaders([...COMPACT, '-XX:-UseCompactObjectHeaders']), false);
assert.strictEqual(hasCompactObjectHeaders([...COMPACT, '-XX:-UseCompactObjectHeaders', '-XX:+UseCompactObjectHeaders']), true);

const options = appCdsJvmOptions(java, jar, COMPACT);
assert.deepStrictEqual(options.filter((option) => !option.startsWith('-XX:SharedArchiveFile=')),
['-XX:+AutoCreateSharedArchive', '-Xlog:disable']);
const archive = archiveOf(options);
assert.strictEqual(path.dirname(archive), compiler);
assert.match(path.basename(archive), /^wurstscript-[0-9a-f]{12}\.jsa$/);
assert.deepStrictEqual(appCdsJvmOptions(java, jar), options, 'the same install gets the same archive');
assert.deepStrictEqual(appCdsJvmOptions(java, jar, COMPACT), options, 'the same install gets the same archive');
assert.deepStrictEqual(appCdsJvmOptions(java, jar, [...COMPACT, '-Xmx2g']), options, 'other options keep the archive');
// people already have the option in wurst.javaOpts: given twice it is the same mode, so the same archive
assert.deepStrictEqual(appCdsJvmOptions(java, jar, [...COMPACT, ...COMPACT]), options, 'the option twice keeps the archive');
// a JVM uses an archive only for its object header mode, so switching it off gets an archive of its own
const withoutCompact = appCdsJvmOptions(java, jar, [...COMPACT, '-XX:-UseCompactObjectHeaders']);
assert.notStrictEqual(archiveOf(withoutCompact), archive);
assert.deepStrictEqual(appCdsJvmOptions(java, jar, COMPACT), options, 'and the archive for compact headers is kept');
assert.deepStrictEqual(fs.readdirSync(compiler), ['wurstscript.jar'], 'the write probe is removed');

// an update: the archive of the old jar does not fit and is not replaced by the JVM, so it is removed here
fs.writeFileSync(archive, 'archive of compiler 1');
fs.writeFileSync(path.join(compiler, 'notes.jsa'), 'not ours');
fs.writeFileSync(path.join(compiler, 'wurstscript-latest.jsa'), 'not ours');
fs.writeFileSync(jar, 'compiler 2 is longer');
const updated = archiveOf(appCdsJvmOptions(java, jar));
const updated = archiveOf(appCdsJvmOptions(java, jar, COMPACT));
assert.notStrictEqual(updated, archive);
assert.ok(!fs.existsSync(archive), 'the archive of the old jar is removed');
assert.deepStrictEqual(fs.readdirSync(compiler).sort(), ['notes.jsa', 'wurstscript-latest.jsa', 'wurstscript.jar']);

// a jar with the same size, rewritten later, is another jar
const stat = fs.statSync(jar);
fs.utimesSync(jar, stat.atime, new Date(stat.mtimeMs + 5000));
assert.notStrictEqual(archiveOf(appCdsJvmOptions(java, jar)), updated);
assert.notStrictEqual(archiveOf(appCdsJvmOptions(java, jar, COMPACT)), updated);

// another runtime, at the same path or elsewhere, is another archive
const other = install(path.join(tmp, 'other'), 'JAVA_VERSION="25.0.3"\n');
fs.copyFileSync(jar, other.jar);
fs.utimesSync(other.jar, fs.statSync(jar).atime, fs.statSync(jar).mtime);
assert.notStrictEqual(archiveOf(appCdsJvmOptions(other.java, other.jar)).split(path.sep).pop(),
archiveOf(appCdsJvmOptions(java, jar)).split(path.sep).pop());
assert.notStrictEqual(archiveOf(appCdsJvmOptions(other.java, other.jar, COMPACT)).split(path.sep).pop(),
archiveOf(appCdsJvmOptions(java, jar, COMPACT)).split(path.sep).pop());

// a folder which cannot be written gets no options (the JVM aborts at exit when it cannot write the archive)
assert.deepStrictEqual(appCdsJvmOptions(java, path.join(tmp, 'missing', 'wurstscript.jar')), []);
assert.deepStrictEqual(appCdsJvmOptions(java, path.join(tmp, 'missing', 'wurstscript.jar'), COMPACT), []);
// and so does a jar which is not there
fs.unlinkSync(jar);
assert.deepStrictEqual(appCdsJvmOptions(java, jar), []);
assert.deepStrictEqual(appCdsJvmOptions(java, jar, COMPACT), []);
assert.ok(!fs.readdirSync(compiler).some((entry) => entry.startsWith('.write-probe')));
console.log('appcds options ok');
} finally {
Expand Down
46 changes: 39 additions & 7 deletions scripts/test-lsp-appcds.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use strict';

// Opt-in cross-repository test: pass the compiler jar of a built distribution and the java of its runtime
// (the runtime needs the base CDS archive jlink makes): node scripts/test-lsp-appcds.js <compiler.jar> <java>
// (the runtime needs the base CDS archive for compact object headers, classes_coh.jsa): node scripts/test-lsp-appcds.js <compiler.jar> <java>
const assert = require('assert');
const fs = require('fs');
const os = require('os');
Expand All @@ -11,7 +11,18 @@ const { spawn } = require('child_process');
const { createProtocolConnection, StreamMessageReader, StreamMessageWriter } = require('vscode-languageserver-protocol/node');
const { createTsLoader } = require('../e2e/harness/tsLoader');

const { appCdsJvmOptions } = createTsLoader()('src/install/fsUtils.ts');
const { appCdsJvmOptions, COMPACT_OBJECT_HEADERS } = createTsLoader()('src/install/fsUtils.ts');

// the options the extension starts the server with, as getServerOptions builds them
function serverOptions(java, jar, javaOpts = []) {
return [COMPACT_OBJECT_HEADERS, ...appCdsJvmOptions(java, jar, [COMPACT_OBJECT_HEADERS, ...javaOpts]), ...javaOpts];
}

function archiveOf(options) {
const flag = options.find((option) => option.startsWith('-XX:SharedArchiveFile='));
assert.ok(flag, 'the options name no archive');
return flag.slice('-XX:SharedArchiveFile='.length);
}

async function session(java, jar, project, options) {
const child = spawn(java, [...options, '-jar', jar, '-languageServer'], { windowsHide: true, stdio: 'pipe' });
Expand Down Expand Up @@ -71,9 +82,9 @@ async function main() {
const copy = path.join(compilerDir, path.basename(jar));
fs.copyFileSync(jar, copy);

const options = appCdsJvmOptions(java, copy);
assert.ok(options.length > 0, 'the options are empty');
const archive = options.find((option) => option.startsWith('-XX:SharedArchiveFile=')).slice('-XX:SharedArchiveFile='.length);
const options = serverOptions(java, copy);
assert.ok(options.includes('-XX:+AutoCreateSharedArchive'), 'the options have no archive');
const archive = archiveOf(options);

const first = await session(java, copy, project, options);
assert.deepStrictEqual(first.states, ['loading', 'ready']);
Expand All @@ -92,13 +103,34 @@ async function main() {
fs.appendFileSync(copy, '');
const stat = fs.statSync(copy);
fs.utimesSync(copy, stat.atime, new Date(stat.mtimeMs + 5000));
const updatedOptions = appCdsJvmOptions(java, copy);
const updatedOptions = serverOptions(java, copy);
assert.notDeepStrictEqual(updatedOptions, options);
assert.ok(!fs.existsSync(archive), 'the archive of the old jar is removed');
const third = await session(java, copy, project, updatedOptions);
assert.deepStrictEqual(third.states, ['loading', 'ready']);
assert.strictEqual(third.code, 0);
assert.ok(fs.existsSync(updatedOptions.find((option) => option.startsWith('-XX:SharedArchiveFile=')).slice('-XX:SharedArchiveFile='.length)));
const updatedArchive = archiveOf(updatedOptions);
assert.ok(fs.existsSync(updatedArchive));

// people already have the option in wurst.javaOpts, so the server is started with it twice: the same archive,
// and a session which starts from it
const twice = serverOptions(java, copy, ['-XX:+UseCompactObjectHeaders']);
assert.strictEqual(twice.filter((option) => option === '-XX:+UseCompactObjectHeaders').length, 2);
assert.strictEqual(archiveOf(twice), updatedArchive);
const writtenUpdated = fs.statSync(updatedArchive).mtimeMs;
const duplicate = await session(java, copy, project, twice);
assert.deepStrictEqual(duplicate.states, ['loading', 'ready']);
assert.strictEqual(duplicate.protocolErrors, 0);
assert.strictEqual(duplicate.code, 0, 'the option twice starts and ends cleanly');
assert.strictEqual(fs.statSync(updatedArchive).mtimeMs, writtenUpdated, 'and starts from the archive');

// a wurst.javaOpts entry which switches the headers off: the runtime has no base archive for that mode, so the
// server starts without any archive, and ends cleanly
const plain = serverOptions(java, copy, ['-XX:-UseCompactObjectHeaders']);
const fourth = await session(java, copy, project, plain);
assert.deepStrictEqual(fourth.states, ['loading', 'ready']);
assert.strictEqual(fourth.protocolErrors, 0);
assert.strictEqual(fourth.code, 0, 'the server ends cleanly without compact object headers');
console.log(`appcds language server ok (ready after ${first.readyMs} ms, then ${second.readyMs} ms)`);
} finally {
fs.rmSync(root, { recursive: true, force: true });
Expand Down
27 changes: 23 additions & 4 deletions src/install/fsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,23 @@ import {
WURST_HOME, RUNTIME_DIR, COMPILER_DIR, GRILL_HOME_DIR, LEGACY_GRILL_DIR,
} from '../paths';

/**
* The language server holds millions of small objects, which compact object headers make 6% smaller on a large
* project (castle fight: 488 to 457 MB after a collection), at the same start time. It is always passed first, so a
* `wurst.javaOpts` entry can still switch it off.
*/
export const COMPACT_OBJECT_HEADERS = '-XX:+UseCompactObjectHeaders';

/** Whether a JVM started with these options has compact object headers: the last of the two options wins. */
export function hasCompactObjectHeaders(jvmOptions: string[]): boolean {
let compact = false;
for (const option of jvmOptions) {
if (option === '-XX:+UseCompactObjectHeaders') compact = true;
else if (option === '-XX:-UseCompactObjectHeaders') compact = false;
}
return compact;
}

/**
* JVM options which start the language server from an AppCDS archive next to the compiler jar: the JVM writes the
* archive when the first session ends, and the sessions after it start from it (about 15% sooner).
Expand All @@ -17,10 +34,11 @@ import {
* be written, because the JVM then aborts when the session ends. `-Xlog:disable` because the JVM reports archive
* trouble on stdout, which is the protocol stream.
*
* Needs the runtime's own base archive, which the distribution builds (jlink --generate-cds-archive); without it
* the JVM runs as before.
* Needs the runtime's own base archive, which the distribution builds, for compact object headers (classes_coh.jsa);
* without it the JVM runs as before. A JVM uses an archive only for the object header mode it runs with, so the mode
* is part of what the archive is named after; `jvmOptions` are the options the server is started with.
*/
export function appCdsJvmOptions(javaExecutable: string, compilerJar: string): string[] {
export function appCdsJvmOptions(javaExecutable: string, compilerJar: string, jvmOptions: string[]): string[] {
try {
const dir = path.dirname(compilerJar);
// access(W_OK) says yes to any folder on Windows
Expand All @@ -36,7 +54,8 @@ export function appCdsJvmOptions(javaExecutable: string, compilerJar: string): s
}
const jar = fs.statSync(compilerJar);
const key = crypto.createHash('sha1')
.update([javaExecutable, release, compilerJar, jar.size, Math.floor(jar.mtimeMs)].join('|'))
.update([javaExecutable, release, compilerJar, jar.size, Math.floor(jar.mtimeMs),
hasCompactObjectHeaders(jvmOptions)].join('|'))
.digest('hex').slice(0, 12);
const archive = path.join(dir, `wurstscript-${key}.jsa`);
for (const entry of fs.readdirSync(dir)) {
Expand Down
5 changes: 3 additions & 2 deletions src/languageServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as vscode from 'vscode';
import { workspace, ExtensionContext } from 'vscode';
import { LanguageClient, LanguageClientOptions, ServerOptions, Executable, State, DidOpenTextDocumentNotification } from 'vscode-languageclient/node';
import { RUNTIME_DIR, COMPILER_JAR } from './paths';
import { appCdsJvmOptions } from './install/fsUtils';
import { appCdsJvmOptions, COMPACT_OBJECT_HEADERS } from './install/fsUtils';
import { getLanguageServerJava, checkCustomJavaVersion, getInstalledVersionString, ensureInstalledOrOfferMigration, maybeOfferUpdate } from './install/installer';
import type { UpdateAvailable } from './install/installer';
import { appendDiagnostic, formatDiagnosticError } from './features/diagnostics';
Expand Down Expand Up @@ -347,7 +347,8 @@ async function getServerOptions(): Promise<ServerOptions> {
const java = getLanguageServerJava();
if (customJava) await checkCustomJavaVersion(customJava);
const platformOpts = process.platform === 'darwin' ? ['-Dapple.awt.UIElement=true'] : [];
const args = [...platformOpts, ...appCdsJvmOptions(java, COMPILER_JAR), ...javaOpts, '-jar', COMPILER_JAR, '-languageServer'];
const jvmOpts = [COMPACT_OBJECT_HEADERS, ...javaOpts];
const args = [...platformOpts, COMPACT_OBJECT_HEADERS, ...appCdsJvmOptions(java, COMPILER_JAR, jvmOpts), ...javaOpts, '-jar', COMPILER_JAR, '-languageServer'];

if (debugMode && (await isPortOpen(5005))) {
args.unshift('-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005,quiet=y');
Expand Down
Loading