From 71437456ea10128274b53ccbbd3b8ea8bb406bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Thu, 30 Jul 2026 13:05:20 +0200 Subject: [PATCH 1/2] feat(installer): AppCDS cold-start lever on -all bundle installs + batch amortization docs (REPL.5 spike) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Time-boxed spike #163 fix 3 — measured first, shipped what measured well: - launcher CDS block, bundle-gated via install-time baked CDS_BUNDLE (${USE_BUNDLE:-false} — inert until REPL.4 merges; plain/--no-extensions installs stay byte-identical on every JDK incl. 19+) - generate_cds_archive(): install-time dynamic archive (JDK 13-18, bare-jar -cp, -c "help" workload, no ES needed), set -e-hardened, success requires the .jsa to exist; JDK 19+ self-serves via -XX:+AutoCreateSharedArchive (verified: runtime dump works despite the literal lib/* wildcard) - AppCDS status line in installer summary + VERSION - docs: Cold-start & batch invocation cost (-f/;-split amortization ≈9-12ms marginal per statement; AppCDS automatic on bundle installs, JDK 13+) Measured (median of 5, Zulu 17.0.11, es8 -all bundle, warm ES 8.18.3): -c SELECT 1.924s -> 1.525s (-20.7%), prompt-and-exit 1.305s -> 0.926s, JOIN 3.187s -> 2.729s; JDK 21: 1.784s -> 1.313s. JDK 11: no flags, identical. Version bumped to 0.20.2-SNAPSHOT (shared REPL.x line — expect trivial build.sbt conflict with sibling REPL PRs). Merge order: REPL.2 -> REPL.4 -> REPL.5 (this lands LAST). Closed Issue #163 Refs #169 Co-Authored-By: Claude Fable 5 --- build.sbt | 2 +- documentation/client/repl.md | 32 +++++++++++++++ install.sh | 80 +++++++++++++++++++++++++++++++++++- 3 files changed, 112 insertions(+), 2 deletions(-) diff --git a/build.sbt b/build.sbt index 807c6178..22c7824b 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ ThisBuild / organization := "app.softnetwork" name := "softclient4es" -ThisBuild / version := "0.20.1" +ThisBuild / version := "0.20.2-SNAPSHOT" ThisBuild / scalaVersion := scala213 diff --git a/documentation/client/repl.md b/documentation/client/repl.md index c90e2383..cc027046 100644 --- a/documentation/client/repl.md +++ b/documentation/client/repl.md @@ -422,6 +422,38 @@ INSERT INTO users (id, name) VALUES (1, 'Alice'), (2, 'Bob'); SELECT * FROM users; ``` +#### Cold-start & batch invocation cost + +Each `softclient4es -c "…"` invocation starts a fresh JVM and pays the full +initialization cost (class loading, client setup, extension discovery) before the +statement runs. For scripted or CI use, prefer batching statements into **one** +invocation — both forms below execute all `;`-separated statements in a single JVM, +so the startup cost is paid once: + +```bash +# Recommended: a SQL file (N statements, one JVM) +softclient4es -f batch.sql + +# Also works: several statements in one -c +softclient4es -c "CREATE TABLE t (id INT); INSERT INTO t (id) VALUES (1); SELECT * FROM t" +``` + +In practice the marginal cost of each extra statement is a few milliseconds, versus +1.5–2s of JVM startup for every separate invocation. + +On bundle installs (the default self-contained `-all` jar) running on JDK 13+, the +launcher additionally uses [Class-Data Sharing (AppCDS)](https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html) +to cut class-loading time (~20-25% faster start on JDK 17/21): + +- **JDK 13-18** — the installer generates `cache/softclient4es.jsa` once at install + time. After a Java upgrade, re-run the installer to regenerate it. +- **JDK 19+** — the JVM creates and refreshes the archive by itself + (`-XX:+AutoCreateSharedArchive`); nothing to do. +- The archive is an internal cache: it is safe to delete `cache/` at any time (the + REPL falls back to normal class loading, and JDK 19+ recreates it on the next run). +- Plain and `--no-extensions` installs are unaffected — no CDS flags, no `cache/` + directory. + --- ## Basic Usage diff --git a/install.sh b/install.sh index 2ba51005..b281191d 100755 --- a/install.sh +++ b/install.sh @@ -792,6 +792,27 @@ if [[ -n "\$JAVA_MAJOR" ]] && [[ "\$JAVA_MAJOR" -ge 9 ]]; then EXTRA_OPTS="--add-opens=java.base/java.nio=ALL-UNNAMED -Dio.netty.tryReflectionSetAccessible=true" fi +# Class-Data Sharing (AppCDS) — cuts JVM class-load time on every start (#163 fix 3). +# BUNDLE-ONLY: CDS_BUNDLE below is baked in AT INSTALL TIME from REPL.4's USE_BUNDLE +# (intentionally NOT \$-escaped) — plain/--no-extensions installs get NO CDS behaviour +# at all (no flags, no cache/ dir), on every JDK. Without this gate the 19+ branch +# would fire on plain installs too and attempt runtime dumps over the multi-jar lib/*. +# - JDK 19+: the JVM creates/refreshes the archive itself (AutoCreateSharedArchive). +# - JDK 13-18: use the archive generated at install time, if present and not stale. +# - JDK 11/12 or no archive: no CDS flags — default behaviour, zero regression. +# -Xshare:auto (the default) silently ignores an invalid/mismatched archive. +CDS_BUNDLE=${USE_BUNDLE:-false} +CDS_ARCHIVE="\$BASE_DIR/cache/softclient4es.jsa" +CDS_OPTS="" +if [[ "\$CDS_BUNDLE" == true ]] && [[ -n "\$JAVA_MAJOR" ]]; then + if [[ "\$JAVA_MAJOR" -ge 19 ]]; then + mkdir -p "\$BASE_DIR/cache" + CDS_OPTS="-XX:+AutoCreateSharedArchive -XX:SharedArchiveFile=\$CDS_ARCHIVE" + elif [[ "\$JAVA_MAJOR" -ge 13 ]] && [[ -f "\$CDS_ARCHIVE" ]] && [[ ! "\$JAR_FILE" -nt "\$CDS_ARCHIVE" ]]; then + CDS_OPTS="-XX:SharedArchiveFile=\$CDS_ARCHIVE" + fi +fi + # Logback configuration LOGBACK_OPTS="" if [[ -f "\$LOGBACK_FILE" ]]; then @@ -801,7 +822,7 @@ fi # The engine assembly comes first on the classpath so it wins any conflict; # lib/* brings the extension jars, discovered via the ServiceLoader SPI. # (java -jar would ignore the classpath entirely — do not switch back.) -exec java \$JAVA_OPTS \$EXTRA_OPTS \\ +exec java \$JAVA_OPTS \$EXTRA_OPTS \$CDS_OPTS \\ -Dconfig.file="\$CONFIG_FILE" \\ -Dlog.dir="\$LOG_DIR" \\ \$LOGBACK_OPTS \\ @@ -815,6 +836,55 @@ LAUNCHER_EOF success "Created $TARGET_DIR/bin/softclient4es" } +# ============================================================================= +# Generate AppCDS archive (REPL.5 / #163 fix 3) — bundle installs, JDK 13-18. +# JDK 19+ needs nothing here (the launcher passes -XX:+AutoCreateSharedArchive). +# ============================================================================= + +CDS_STATUS="disabled" + +generate_cds_archive() { + [[ "$USE_BUNDLE" == true ]] || return 0 # single-jar classpath only (CDS forbids lib/*) + local jv + jv=$(get_java_version) + [[ -n "$jv" ]] || return 0 + if [[ "$jv" -ge 19 ]]; then + # The launcher's -XX:+AutoCreateSharedArchive branch dumps/refreshes at runtime. + CDS_STATUS="enabled (runtime auto-create, JDK 19+)" + return 0 + fi + [[ "$jv" -ge 13 ]] || return 0 # 11/12: no dynamic archive support + + info "Generating AppCDS archive (one-off; speeds up every REPL/batch start)..." + # The installer runs under `set -e`: every may-fail step below is guarded so a + # CDS failure can NEVER fail the install (warn-and-continue only). + if ! mkdir -p "$TARGET_DIR/cache" 2>/dev/null; then + warn "AppCDS archive generation skipped — could not create cache/ (no runtime impact)" + CDS_STATUS="disabled (generation failed)" + return 0 + fi + # Re-install into an existing dir: never dump over a stale archive. + rm -f "$TARGET_DIR/cache/softclient4es.jsa" 2>/dev/null || true + # Dump workload: a short-lived batch run. It needs NO Elasticsearch — whatever the + # statement resolves to (local help or a connection error printed by the executor), + # the process exits normally with code 0 (Repl.executeCommand always returns 0) and + # the dynamic archive is written at exit. -cp is the bare jar: wildcards are + # forbidden at dump time, and [jar] is a prefix of the runtime [jar, lib/*] path. + # The exit code alone is not proof the archive was written (an unwritable path + # still exits 0) — require the .jsa file to actually exist. + if java -XX:ArchiveClassesAtExit="$TARGET_DIR/cache/softclient4es.jsa" \ + -cp "$TARGET_DIR/lib/$JAR_NAME" \ + app.softnetwork.elastic.client.Cli -c "help" > /dev/null 2>&1 \ + && [[ -f "$TARGET_DIR/cache/softclient4es.jsa" ]]; then + success "AppCDS archive created: cache/softclient4es.jsa" + CDS_STATUS="enabled (cache/softclient4es.jsa)" + else + warn "AppCDS archive generation failed — continuing without it (no runtime impact)" + rm -f "$TARGET_DIR/cache/softclient4es.jsa" 2>/dev/null || true + CDS_STATUS="disabled (generation failed)" + fi +} + # ============================================================================= # Create Logback Configuration # ============================================================================= @@ -915,6 +985,7 @@ Scala: $SCALA_VERSION Java Required: $REQUIRED_JAVA_VERSION+ Artifact: $ARTIFACT_NAME Extensions: $EXTENSIONS_INSTALLED +AppCDS: $CDS_STATUS OS: $OS_TYPE EOF @@ -936,6 +1007,12 @@ print_summary() { echo " SoftClient4ES version: $SOFT_VERSION" echo " Java required: $REQUIRED_JAVA_VERSION+" echo " OS detected: $OS_TYPE" + if [[ "$CDS_STATUS" == enabled* ]]; then + echo " AppCDS: $CDS_STATUS" + if [[ "$CDS_STATUS" == *cache/* ]]; then + echo " (regenerate by re-running the installer after a Java upgrade)" + fi + fi echo "" echo " Directory structure:" echo " $TARGET_DIR/" @@ -1031,6 +1108,7 @@ main() { create_config create_logback_config create_launcher + generate_cds_archive create_uninstaller create_version_info print_summary From 016f71f278bc3374768cb64565236358cad2863f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Manciot?= Date: Fri, 31 Jul 2026 08:12:20 +0200 Subject: [PATCH 2/2] fix(build): bump version to 0.20.2 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index c2292d72..57b76467 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ ThisBuild / organization := "app.softnetwork" name := "softclient4es" -ThisBuild / version := "0.20.2-SNAPSHOT" +ThisBuild / version := "0.20.2" ThisBuild / scalaVersion := scala213