From dfe57916e5b425c89a95c9770164595395521ec9 Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 8 Oct 2026 12:48:29 +0200 Subject: [PATCH 1/2] Dist: give the runtime its base CDS archive, drop the AppCDS archive that was never made The distribution tried to ship an AppCDS archive for the language server (generateAppCdsArchive, -XX:ArchiveClassesAtExit). An archive like that sits on the base archive of the runtime, and the slim runtime built by jlink had none (no classes.jsa next to jvm.dll), so the JVM could not write it, the task only warned, and nothing ever passed the archive to a JVM anyway. An installed runtime confirms it: no .jsa anywhere. jlink now runs with --generate-cds-archive, the 14 MB variant for heaps above 32 GB is left out of the copy, and assembleSlimCompilerDist fails if the runtime has no classes.jsa (bin/server or lib/server; macOS copies a full JDK, which has one), because the failure was silent before. The dead task and the -languageServerAppCdsTrain option it used are removed. The archive of the application is written by the JVM itself on the user's machine: wurst4vscode starts the server with -XX:+AutoCreateSharedArchive. An archive recorded at build time cannot be shipped, as the JVM refuses one for another jar path or modification time. Language server start to ready, interleaved, median of four starts: small project no archives 4.39 s, base archive only 4.42 s, both 3.71 s castle fight no archives 18.98 s, base archive only 19.25 s, both 17.28 s The base archive alone changes nothing; it is what makes the second possible. --- AGENTS.md | 9 +++ CHANGELOG.md | 8 +++ de.peeeq.wurstscript/deploy.gradle | 58 +++++-------------- .../src/main/java/de/peeeq/wurstio/Main.java | 5 -- .../languageserver/LanguageServerStarter.java | 10 ---- .../java/de/peeeq/wurstscript/RunArgs.java | 6 -- .../tests/wurstscript/tests/RunArgsTests.java | 3 +- 7 files changed, 33 insertions(+), 66 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d6ca47e40..aec46867c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,6 +152,15 @@ This repository has multiple entry points that may trigger compilation/build beh * Model-dependent requests, including formatting, must use `LanguageWorker.handle` so progress negotiation, initialization, and the initial build complete before model access. Formatting keeps duplicate requests because separate documents must each receive an answer. * Run `InitialBuildProgressTest` and `LanguageWorkerTest` for startup changes; the extension's opt-in `scripts/test-lsp-readiness.js ` verifies the actual stdio protocol. +### Language server startup archive + +wurst4vscode starts the server with `-XX:+AutoCreateSharedArchive`: the JVM writes an AppCDS archive next to the +compiler jar when the first session ends, and the sessions after it start from it. That archive sits on the +runtime's own base archive, which `deploy.gradle` makes (`jlink --generate-cds-archive`; macOS copies a full JDK, which +has one) and `assembleSlimCompilerDist` checks for. Without it the JVM silently runs without any archive, which is +how the earlier attempt went unnoticed. The extension passes `-Xlog:disable` because the JVM reports archive trouble +on stdout, which is the protocol stream: never print anything of your own there. + ### Build-map pipeline (centralized) Map build behavior is centralized in: diff --git a/CHANGELOG.md b/CHANGELOG.md index 6994f4ea1..c93930773 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## 1.9 (in progress) +- The runtime of the distribution carries the base class data sharing archive of the JVM, which it never did: the + slim runtime is built with jlink, and the AppCDS archive the build tried to make for the language server needs a + base archive to sit on, so it could not be made and was never shipped. The build now makes the base archive + (`jlink --generate-cds-archive`) and fails when the runtime has none, and the dead archive task and its + `-languageServerAppCdsTrain` option are gone. With wurst4vscode starting the server with an archive of its own + (written next to the compiler jar when the first session ends), the language server of a small project is ready + after 3.7 s instead of 4.4 s, and castle fight after 17.3 s instead of 19.0 s (medians of four starts). + - The language server checks every file it was meant to check. A check which cannot start, because an import does not resolve, used to forget the files it was planned for. A file edited in the meantime then got no diagnostics once the import was fixed, since nothing it imports had changed. Those files are kept and checked diff --git a/de.peeeq.wurstscript/deploy.gradle b/de.peeeq.wurstscript/deploy.gradle index 77281d3d2..661ef0814 100644 --- a/de.peeeq.wurstscript/deploy.gradle +++ b/de.peeeq.wurstscript/deploy.gradle @@ -42,7 +42,6 @@ def jreImageDir = layout.buildDirectory.dir("jre-wurst-25") def distRoot = layout.buildDirectory.dir("dist/slim-${plat}") def releasesDir = layout.buildDirectory.dir("releases") -def appCdsArchive = distRoot.map { it.file("wurst-runtime/wurst-lsp.jsa") } // ----------------------- Toolchain / tool paths (providers) ------------------------- def toolchainSvc = extensions.getByType(JavaToolchainService) @@ -180,6 +179,10 @@ tasks.register("jlinkRuntime25", Exec) { "--no-man-pages", "--strip-debug", "--compress=zip-9", + // The language server starts from an AppCDS archive which the JVM writes next to the compiler + // jar (wurst4vscode, -XX:+AutoCreateSharedArchive). That archive sits on top of the base archive + // of the runtime; without one the JVM silently runs without any. + "--generate-cds-archive", "--output", outDir.absolutePath } } @@ -200,7 +203,11 @@ tasks.register("assembleSlimCompilerDist", Copy) { } } else { dependsOn("jlinkRuntime25", "shadowJar") - from(jreImageDir) { into("wurst-runtime") } + from(jreImageDir) { + into("wurst-runtime") + // for heaps above 32 GB, which this does not run with; it would add 14 MB to the download + exclude("**/classes_nocoops*.jsa") + } } from(fatJar) { into("wurst-compiler") } @@ -208,6 +215,11 @@ tasks.register("assembleSlimCompilerDist", Copy) { doLast { logger.lifecycle("[dist] Folder ready at: ${distRoot.get().asFile}") + def runtime = new File(distRoot.get().asFile, "wurst-runtime") + if (!["bin/server/classes.jsa", "lib/server/classes.jsa"].any { new File(runtime, it).exists() }) { + throw new GradleException("The runtime has no base CDS archive (classes.jsa under bin/server or lib/server), " + + "so the language server cannot start from an AppCDS archive.") + } } } @@ -224,51 +236,11 @@ tasks.named("assembleSlimCompilerDist", Copy) { t -> } } -tasks.register("generateAppCdsArchive", Exec) { - description = "Best-effort AppCDS archive generation for language-server startup." - group = "distribution" - dependsOn("assembleSlimCompilerDist") - - inputs.file(fatJar) - inputs.dir(distRoot) - outputs.file(appCdsArchive) - ignoreExitValue = true - - doFirst { - def distDir = distRoot.get().asFile - def archiveFile = appCdsArchive.get().asFile - def runtimeJava = new File(distDir, "wurst-runtime/bin/java${os.isWindows() ? '.exe' : ''}") - def compilerJar = new File(distDir, "wurst-compiler/${fatJar.get().asFile.name}") - - archiveFile.parentFile.mkdirs() - if (archiveFile.exists()) { - archiveFile.delete() - } - - executable = runtimeJava.absolutePath - args "-Xshare:auto", - "-XX:ArchiveClassesAtExit=${archiveFile.absolutePath}", - "-jar", compilerJar.absolutePath, - "-languageServerAppCdsTrain" - - logger.lifecycle("[appcds] Training LS startup with ${runtimeJava.absolutePath}") - } - - doLast { - def archiveFile = appCdsArchive.get().asFile - if (executionResult.get().exitValue != 0 || !archiveFile.exists()) { - logger.warn("[appcds] Archive generation did not succeed on ${plat}; continuing without shipped AppCDS archive.") - } else { - logger.lifecycle("[appcds] Wrote ${archiveFile.absolutePath}") - } - } -} - // 4) Package ZIP on all platforms tasks.register("packageSlimCompilerDistZip", Zip) { description = "Packages slim dist as a ZIP archive (all platforms)." group = "distribution" - dependsOn("generateAppCdsArchive") + dependsOn("assembleSlimCompilerDist") from(distRoot) destinationDirectory.set(releasesDir) diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/Main.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/Main.java index 23830f462..e6a9ab200 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/Main.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/Main.java @@ -76,11 +76,6 @@ public static void main(String[] args) { return; } - if (runArgs.isLanguageServerAppCdsTrain()) { - LanguageServerStarter.trainForAppCds(); - return; - } - if (runArgs.isLanguageServer()) { LanguageServerStarter.start(); return; diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageServerStarter.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageServerStarter.java index a507909b8..4581ecea7 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageServerStarter.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstio/languageserver/LanguageServerStarter.java @@ -22,15 +22,5 @@ public static void start() { launcher.startListening(); } - public static void trainForAppCds() { - WurstLanguageServer server = new WurstLanguageServer(); - try { - server.getTextDocumentService(); - server.getWorkspaceService(); - } finally { - server.shutdown().join(); - } - } - } diff --git a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/RunArgs.java b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/RunArgs.java index 74ef7be0d..b2275c4ec 100644 --- a/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/RunArgs.java +++ b/de.peeeq.wurstscript/src/main/java/de/peeeq/wurstscript/RunArgs.java @@ -45,7 +45,6 @@ public class RunArgs { private final RunOption optionExportObjects; private final RunOption optionStartServer; private final RunOption optionLanguageServer; - private final RunOption optionLanguageServerAppCdsTrain; private final RunOption optionNoExtractMapScript; private final RunOption optionFixInstall; private final RunOption optionCopyMap; @@ -142,7 +141,6 @@ public RunArgs(String... args) { optionLanguageServer = addOption("languageServer", "Starts a language server which can be used by editors to get services " + "like code completion, validations, and find declaration. The communication to the language server is via standard input output."); - optionLanguageServerAppCdsTrain = addOption("languageServerAppCdsTrain", "Starts and immediately stops a lightweight language-server startup path for AppCDS training."); optionHelp = addOption("help", "Prints this help message."); optionDisablePjass = addOption("noPJass", "Disables PJass checks for the generated code."); @@ -371,10 +369,6 @@ public boolean isLanguageServer() { return optionLanguageServer.isSet; } - public boolean isLanguageServerAppCdsTrain() { - return optionLanguageServerAppCdsTrain.isSet; - } - public boolean isNoExtractMapScript() { return optionNoExtractMapScript.isSet; } diff --git a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunArgsTests.java b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunArgsTests.java index d16557160..e3a2464b9 100644 --- a/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunArgsTests.java +++ b/de.peeeq.wurstscript/src/test/java/tests/wurstscript/tests/RunArgsTests.java @@ -38,7 +38,7 @@ public void exposesModeAndSafetyFlags() { RunArgs args = new RunArgs("-build", "-dev", "-stacktraces", "-nodebug", "-uncheckedDispatch", "-injectobjects", "-hotreload", "-hotstart", "-noPJass", "-legacyJassChecks", "-compactOutput", "-measure", "-compiletimeCache", "-noExtractMapScript", - "-prettyPrint", "-languageServer", "-languageServerAppCdsTrain"); + "-prettyPrint", "-languageServer"); Assert.assertTrue(args.isBuild()); Assert.assertTrue(args.isDevBuild()); @@ -56,7 +56,6 @@ public void exposesModeAndSafetyFlags() { Assert.assertTrue(args.isNoExtractMapScript()); Assert.assertTrue(args.isPrettyPrint()); Assert.assertTrue(args.isLanguageServer()); - Assert.assertTrue(args.isLanguageServerAppCdsTrain()); } @Test From e711ad3fe0e7e4fcbd065245b6d8932bbf4acbe7 Mon Sep 17 00:00:00 2001 From: Frotty Date: Thu, 8 Oct 2026 14:23:47 +0200 Subject: [PATCH 2/2] Dist: make the base CDS archive without the options of the build machine jlink writes the archive with a JVM of its own, and that JVM takes on the object header mode of the options it is started with. The CI exports JAVA_TOOL_OPTIONS=-XX:+UseCompactObjectHeaders for the tests, so jlink made only classes_coh.jsa and classes_nocoops_coh.jsa, no classes.jsa, and the guard added by this change failed the master build (reproduced with that environment and a clean dist folder: "The runtime has no base CDS archive"). A runtime which is started without the option, as the language server and grill do, cannot use a _coh archive. The jlink step now runs without JAVA_TOOL_OPTIONS, _JAVA_OPTIONS and JDK_JAVA_OPTIONS. The guard is also made on the jlink image itself: the dist folder is only added to by the copy, so an archive left by an earlier build could hide a missing one there. Under that environment, from a clean dist folder: classes.jsa is made and ends up in the runtime, classes_nocoops.jsa is left out. --- AGENTS.md | 4 +++- de.peeeq.wurstscript/deploy.gradle | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index aec46867c..b39ded8d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -158,7 +158,9 @@ wurst4vscode starts the server with `-XX:+AutoCreateSharedArchive`: the JVM writ compiler jar when the first session ends, and the sessions after it start from it. That archive sits on the runtime's own base archive, which `deploy.gradle` makes (`jlink --generate-cds-archive`; macOS copies a full JDK, which has one) and `assembleSlimCompilerDist` checks for. Without it the JVM silently runs without any archive, which is -how the earlier attempt went unnoticed. The extension passes `-Xlog:disable` because the JVM reports archive trouble +how the earlier attempt went unnoticed. jlink writes the archive with a JVM of its own, which takes on the options it is started with: the +CI exports `JAVA_TOOL_OPTIONS=-XX:+UseCompactObjectHeaders`, so `deploy.gradle` removes it (and the other JVM option variables) for that +step, or only `classes_coh.jsa` is made, which a runtime started without that option cannot use. The extension passes `-Xlog:disable` because the JVM reports archive trouble on stdout, which is the protocol stream: never print anything of your own there. ### Build-map pipeline (centralized) diff --git a/de.peeeq.wurstscript/deploy.gradle b/de.peeeq.wurstscript/deploy.gradle index 661ef0814..6deed90de 100644 --- a/de.peeeq.wurstscript/deploy.gradle +++ b/de.peeeq.wurstscript/deploy.gradle @@ -172,6 +172,10 @@ tasks.register("jlinkRuntime25", Exec) { logger.lifecycle("[jlink] Modules: ${mods}") executable = jlinkPathProvider.get() + // The base archive is written by a JVM which jlink starts, and takes on the object header mode of the options + // that JVM is given. CI exports JAVA_TOOL_OPTIONS=-XX:+UseCompactObjectHeaders for the tests, which would + // leave only classes_coh.jsa, which a runtime started without that option cannot use. + ["JAVA_TOOL_OPTIONS", "_JAVA_OPTIONS", "JDK_JAVA_OPTIONS"].each { environment.remove(it) } args "--verbose", "--module-path", jmodsDirProvider.get(), "--add-modules", mods, @@ -185,6 +189,14 @@ tasks.register("jlinkRuntime25", Exec) { "--generate-cds-archive", "--output", outDir.absolutePath } + + doLast { + // checked on the image itself: the dist folder is only added to, and may still hold an archive of an earlier build + def image = jreImageDir.get().asFile + if (!["bin/server/classes.jsa", "lib/server/classes.jsa"].any { new File(image, it).exists() }) { + throw new GradleException("jlink made no base CDS archive (classes.jsa under bin/server or lib/server) in " + image) + } + } } // 3) Assemble folder layout: jre + compiler.jar (no manifest)