diff --git a/AGENTS.md b/AGENTS.md index d6ca47e40..b39ded8d0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -152,6 +152,17 @@ 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. 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) 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..6deed90de 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) @@ -173,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, @@ -180,8 +183,20 @@ 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 } + + 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) @@ -200,7 +215,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 +227,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 +248,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