Add aot-cache module: the JDK 25 AOT cache on Spring Boot 4.1 vs AppCDS, Spring AOT and GraalVM native
A Spring Boot 4.1.1 order service started 12 ways (plain and extracted jar, Spring AOT output, AppCDS, AOT cache trained with and without traffic, JDK 27, native image), ten interleaved rounds each, with first-request latency; 24 cache-mismatch cases; Docker layer arithmetic. Transcripts are in output/ (no docs/ folder). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
This commit is contained in:
Executable
+14
@@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
# Where the JDKs and GraalVM live. Override with environment variables; nothing else is hard-coded.
|
||||
: "${JDK25:=/tmp/tools/j25/jdk-25.0.4.1+1}"
|
||||
: "${JDK27:=/tmp/tools/j27/jdk-27+35}"
|
||||
: "${GRAALVM:=$(ls -d /tmp/tools/graal/graalvm-community* 2>/dev/null | head -1)}"
|
||||
export JDK25 JDK27 GRAALVM
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
WORK="$ROOT/work" # generated jars, caches and logs (git-ignored)
|
||||
OUT="$ROOT/output" # transcripts that the article quotes (committed)
|
||||
export ROOT WORK OUT
|
||||
# Maven may need proxy/trust-store options that must not leak into the JVM runs being measured.
|
||||
MVN_JTO="${JAVA_TOOL_OPTIONS:-}"
|
||||
unset JAVA_TOOL_OPTIONS JDK_JAVA_OPTIONS _JAVA_OPTIONS
|
||||
mvn_run() { JAVA_TOOL_OPTIONS="$MVN_JTO" "$@"; }
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
source "$(dirname "$0")/env.sh"
|
||||
echo "# machine"; echo "cpus: $(nproc) memory: $(free -m | awk '/Mem:/ {print $2}') MB kernel: $(uname -r)"
|
||||
echo; echo "# JDK 25 (the JVM the article is about)"; "$JDK25/bin/java" -version 2>&1
|
||||
echo; echo "# JDK 27 (the comparison)"; "$JDK27/bin/java" -version 2>&1
|
||||
echo; echo "# GraalVM (native image)"; "$GRAALVM/bin/native-image" --version 2>&1 | head -2
|
||||
echo; echo "# libraries"; ls "$WORK/ext/lib" | grep -E '^(spring-boot|spring-core|spring-webmvc|tomcat-embed-core|jackson-databind|hibernate-validator)-[0-9]' | sed 's/\.jar$//'
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
# Docker layer arithmetic without a Docker daemon: which layers of the extracted application change when one line of
|
||||
# code changes, how big each layer is (uncompressed, and gzipped as a registry would store it), and where the AOT
|
||||
# cache falls. A layer is "a directory tree"; two builds share a layer when the tree is byte-identical.
|
||||
source "$(dirname "$0")/env.sh"
|
||||
cd "$ROOT"
|
||||
|
||||
layer_sum() { # dir -> one hash over every file's path and contents
|
||||
( cd "$1" && find . -type f | LC_ALL=C sort | xargs sha256sum | sha256sum | cut -c1-12 )
|
||||
}
|
||||
gz_kb() { tar -C "$1" -cf - . 2>/dev/null | gzip -6 | wc -c | awk '{printf "%d", $1/1024}'; }
|
||||
kb() { find "$1" -type f -printf '%s\n' | awk '{s+=$1} END{printf "%d", s/1024}'; }
|
||||
|
||||
echo "# java -Djarmode=tools -jar app.jar extract --layers (the layer order Spring Boot's Dockerfile support uses)"
|
||||
rm -rf "$WORK/layers-a" "$WORK/layers-b"
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app.jar" extract --layers --destination "$WORK/layers-a" >/dev/null
|
||||
printf '%-22s %10s %10s %s\n' layer "KB" "gzip KB" "files"
|
||||
for d in dependencies spring-boot-loader snapshot-dependencies application; do
|
||||
printf '%-22s %10s %10s %s\n' "$d" "$(kb "$WORK/layers-a/$d")" "$(gz_kb "$WORK/layers-a/$d")" "$(find "$WORK/layers-a/$d" -type f | wc -l)"
|
||||
done
|
||||
|
||||
echo
|
||||
echo "# the AOT cache from the training run (context-only), for scale"
|
||||
printf '%-22s %10s %10s\n' "refresh.aot" "$(( $(stat -c %s "$WORK/refresh.aot") / 1024 ))" "$(gzip -6 -c "$WORK/refresh.aot" | wc -c | awk '{printf "%d", $1/1024}')"
|
||||
|
||||
echo
|
||||
echo "# change one line of application code (the unit price in OrderService, built by prepare.sh), extract again"
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-changed.jar" extract --layers --destination "$WORK/layers-b" >/dev/null
|
||||
printf '%-22s %-14s %-14s %s\n' layer "before" "after" "verdict"
|
||||
for d in dependencies spring-boot-loader snapshot-dependencies application; do
|
||||
a=$(layer_sum "$WORK/layers-a/$d"); b=$(layer_sum "$WORK/layers-b/$d")
|
||||
printf '%-22s %-14s %-14s %s\n' "$d" "$a" "$b" "$([ "$a" = "$b" ] && echo "identical: reused from cache/registry" || echo "CHANGED: rebuilt and pushed")"
|
||||
done
|
||||
echo "AOT cache: trained against the previous jar, so it is stale for the new one (see output/04-mismatch.txt); it must be regenerated after the application layer"
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start / probe / stop helpers shared by the measurement scripts.
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/env.sh"
|
||||
|
||||
PORT=18110
|
||||
BODY='{"customer":"[email protected]","sku":"ABC-1234","quantity":3}'
|
||||
|
||||
now_ms() { echo $(( $(date +%s%N) / 1000000 )); }
|
||||
|
||||
# start_app WORKDIR LOGFILE cmd args... -> sets APP_PID (the java/native process itself, thanks to exec)
|
||||
start_app() {
|
||||
local wd=$1 log=$2; shift 2
|
||||
( cd "$wd" && exec "$@" ) > "$log" 2>&1 &
|
||||
APP_PID=$!
|
||||
}
|
||||
|
||||
# wait_ready -> 0 when GET /ping answers 200 within 60 s
|
||||
wait_ready() {
|
||||
local i
|
||||
for i in $(seq 1 3000); do
|
||||
curl -sf -o /dev/null --max-time 2 "localhost:$PORT/ping" && return 0
|
||||
kill -0 "$APP_PID" 2>/dev/null || return 1
|
||||
sleep 0.02
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
stop_app() {
|
||||
kill "$APP_PID" 2>/dev/null
|
||||
wait "$APP_PID" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
|
||||
# ms_of SECONDS -> milliseconds with one decimal
|
||||
ms_of() { awk -v s="$1" 'BEGIN{printf "%.1f", s*1000}'; }
|
||||
|
||||
# post_order -> prints curl's time_total in ms
|
||||
post_order() {
|
||||
ms_of "$(curl -s -o /dev/null -w '%{time_total}' -H 'Content-Type: application/json' -d "$BODY" "localhost:$PORT/orders")"
|
||||
}
|
||||
get_order() { ms_of "$(curl -s -o /dev/null -w '%{time_total}' "localhost:$PORT/orders/1")"; }
|
||||
|
||||
rss_mb() { awk '/VmRSS/ {printf "%d", $2/1024}' "/proc/$APP_PID/status"; }
|
||||
|
||||
median() { sort -n | awk '{a[NR]=$1} END{ if (NR==0) {print "n/a"; exit} if (NR%2) printf "%s", a[(NR+1)/2]; else printf "%.1f", (a[NR/2]+a[NR/2+1])/2 }'; }
|
||||
minmax() { sort -n | awk 'NR==1{lo=$1} {hi=$1} END{printf "%s..%s", lo, hi}'; }
|
||||
Executable
+80
@@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env bash
|
||||
# What happens when the cache does not match the run. Each case starts the service with a cache that was made
|
||||
# under different conditions, and prints (1) what the JVM logged about the cache, (2) how long start-up took.
|
||||
# -XX:AOTMode=auto is the default: use the cache if it is valid, otherwise carry on without it.
|
||||
# -XX:AOTMode=on : refuse to start if the cache cannot be used.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
J25=$JDK25/bin/java; J27=$JDK27/bin/java
|
||||
|
||||
run_case() { # title, workdir, cmd... (runs once with -Xlog:aot=warning so the cache's complaints show up)
|
||||
local title=$1 wd=$2; shift 2
|
||||
local t0 log="$WORK/mismatch.log"
|
||||
echo "== $title"
|
||||
t0=$(now_ms)
|
||||
start_app "$wd" "$log" "$@" --server.port=$PORT
|
||||
if wait_ready; then
|
||||
echo " result: started, ready after $(( $(now_ms) - t0 )) ms ($(grep -o 'Started [A-Za-z]* in [0-9.]* seconds' "$log"))"
|
||||
stop_app
|
||||
else
|
||||
wait "$APP_PID" 2>/dev/null
|
||||
echo " result: did not start (exit code $?)"
|
||||
fi
|
||||
grep -E '\[(warning|error)[ ]*\]\[(aot|cds)|saved state of|created with' "$log" | grep -v 'Skipping' | head -5 | sed -E 's/^\[[0-9.]+s\]/ log: /' | cut -c1-170
|
||||
[ -z "$(grep -E '\[(warning|error)[ ]*\]\[(aot|cds)' "$log" | grep -v Skipping)" ] && echo " log: (nothing at warning or error level)"
|
||||
echo
|
||||
}
|
||||
|
||||
echo "# cache: $WORK/refresh.aot, trained on JDK 25.0.4.1, extracted layout, default GC (G1), 2 CPUs"
|
||||
echo
|
||||
run_case "control: the same JVM, the same jar, the same path" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 1. the jar changed after training
|
||||
# (cp -a everywhere below: plain cp gives every file a new modification time, which the cache checks - see the timestamp case)
|
||||
rm -rf "$WORK/ext-changed"; cp -a "$WORK/ext" "$WORK/ext-changed"
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-changed.jar" extract --destination "$WORK/changed-tmp" >/dev/null
|
||||
cp "$WORK/changed-tmp/app-changed.jar" "$WORK/ext-changed/app.jar"; rm -rf "$WORK/changed-tmp" # only the application jar differs
|
||||
run_case "the application jar was rebuilt after a one-line code change, default AOTMode=auto" "$WORK/ext-changed" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "the same, with -XX:AOTMode=on" "$WORK/ext-changed" "$J25" -XX:AOTMode=on -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 2. a dependency jar changed
|
||||
rm -rf "$WORK/ext-dep"; cp -a "$WORK/ext" "$WORK/ext-dep"
|
||||
( cd "$WORK/ext-dep/lib" && f=$(ls spring-core-*.jar) && touch -d '2001-01-01' "$f" )
|
||||
run_case "a dependency jar has a different modification time (same bytes)" "$WORK/ext-dep" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 2b. only a copy, with fresh timestamps (what `cp -r` or a re-extract does)
|
||||
rm -rf "$WORK/ext-copy"; cp -r "$WORK/ext" "$WORK/ext-copy"
|
||||
run_case "the same files copied with plain cp (bytes and path layout identical, timestamps new)" "$WORK/ext-copy" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 3. moved to another directory
|
||||
rm -rf "$WORK/moved"; mkdir -p "$WORK/moved"; cp -a "$WORK/ext" "$WORK/moved/app-dir"
|
||||
run_case "the application directory was moved after training (same bytes, different path)" "$WORK/moved/app-dir" "$J25" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 4. fat jar cache used with the extracted layout
|
||||
run_case "cache trained on the fat jar, run from the extracted layout" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/fat.aot" -jar app.jar
|
||||
|
||||
# 5. different JVM
|
||||
run_case "JDK 25's cache used by JDK 27" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "the same, with -XX:AOTMode=on" "$WORK/ext" "$J27" -XX:AOTMode=on -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
|
||||
# 6. flags that change what the cache holds
|
||||
run_case "-XX:+UseCompactObjectHeaders added at run time only (JDK 25)" "$WORK/ext" "$J25" -XX:+UseCompactObjectHeaders -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a different collector at run time: -XX:+UseSerialGC" "$WORK/ext" "$J25" -XX:+UseSerialGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a different collector at run time: -XX:+UseParallelGC" "$WORK/ext" "$J25" -XX:+UseParallelGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a different collector at run time: -XX:+UseZGC" "$WORK/ext" "$J25" -XX:+UseZGC -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a different heap size at run time: -Xmx256m" "$WORK/ext" "$J25" -Xmx256m -XX:AOTCache="$WORK/refresh.aot" -jar app.jar
|
||||
run_case "a cache file that does not exist" "$WORK/ext" "$J25" -XX:AOTCache="$WORK/nonexistent.aot" -jar app.jar
|
||||
run_case "a cache file that does not exist, with -XX:AOTMode=on" "$WORK/ext" "$J25" -XX:AOTMode=on -XX:AOTCache="$WORK/nonexistent.aot" -jar app.jar
|
||||
|
||||
echo "# and on JDK 27, whose cache was trained under the default collector (G1):"
|
||||
echo
|
||||
run_case "JDK 27, cache from JDK 27 (control)" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
run_case "JDK 27, -XX:+UseZGC at run time" "$WORK/ext" "$J27" -XX:+UseZGC -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
run_case "JDK 27, -XX:+UseSerialGC at run time" "$WORK/ext" "$J27" -XX:+UseSerialGC -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
run_case "JDK 27, -XX:-UseCompactObjectHeaders at run time" "$WORK/ext" "$J27" -XX:-UseCompactObjectHeaders -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
|
||||
echo "# why ZGC is different: ask the JVM (-Xlog:aot=info) why it refused the G1-trained cache"
|
||||
echo
|
||||
run_case "JDK 27, -XX:+UseZGC with the G1-trained cache, -Xlog:aot=info" "$WORK/ext" "$J27" -XX:+UseZGC -Xlog:aot=info -XX:AOTCache="$WORK/refresh27.aot" -jar app.jar
|
||||
run_case "JDK 27, cache trained under ZGC, run under ZGC" "$WORK/ext" "$J27" -XX:+UseZGC -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||
run_case "JDK 27, cache trained under ZGC, run under G1 (compressed oops on again)" "$WORK/ext" "$J27" -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||
run_case "JDK 27, cache trained under ZGC, run under G1 with -XX:-UseCompressedOops" "$WORK/ext" "$J27" -XX:-UseCompressedOops -XX:AOTCache="$WORK/zgc27.aot" -jar app.jar
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the GraalVM native image of the same service (no reflection tricks: the app has none). Takes minutes.
|
||||
source "$(dirname "$0")/env.sh"
|
||||
cd "$ROOT"; mkdir -p "$WORK"
|
||||
t0=$(date +%s)
|
||||
JAVA_HOME=$GRAALVM mvn_run mvn -B -DskipTests -Pnative native:compile > "$WORK/native-build.log" 2>&1
|
||||
rc=$?
|
||||
echo "# GraalVM: $("$GRAALVM/bin/native-image" --version 2>&1 | head -1)"
|
||||
grep -E "types, .* fields, and .* methods found reachable|Peak RSS|Finished generating|BUILD (SUCCESS|FAILURE)|Total time" "$WORK/native-build.log" | sed -E 's/^\[INFO\] //; s/^ +//'
|
||||
[ $rc -eq 0 ] && cp "target/aot-cache" "$WORK/aot-cache-native" && ls -l "$WORK/aot-cache-native" "$WORK/app.jar" | awk '{printf "%-30s %d MB\n", $NF, $5/1048576}'
|
||||
echo "build wall time: $(( $(date +%s) - t0 )) s"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds everything the measurements need under work/: the plain jar, a jar with Spring's own AOT output,
|
||||
# the extracted layout, an AppCDS archive and the AOT caches. Prints what it built and how big each artifact is.
|
||||
source "$(dirname "$0")/env.sh"
|
||||
set -e
|
||||
cd "$ROOT"
|
||||
rm -rf "$WORK"; mkdir -p "$WORK"
|
||||
|
||||
echo "== build: plain jar (mvn package)"
|
||||
rm -rf "$ROOT/target" # a stale target/ would carry Spring AOT output from an earlier -Pnative build into the plain jar
|
||||
JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests package > "$WORK/build-plain.log" 2>&1
|
||||
cp target/app.jar "$WORK/app.jar"
|
||||
rm -rf "$ROOT/target"
|
||||
echo "== build: jar with Spring AOT output (mvn -Pnative package; this does NOT compile a native image)"
|
||||
JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests -Pnative package > "$WORK/build-springaot.log" 2>&1
|
||||
cp target/app.jar "$WORK/app-springaot.jar"
|
||||
echo "initializers in plain jar: $(unzip -l "$WORK/app.jar" | grep -c '__ApplicationContextInitializer')"
|
||||
echo "initializers in Spring AOT jar: $(unzip -l "$WORK/app-springaot.jar" | grep -c '__ApplicationContextInitializer')"
|
||||
|
||||
echo "== extract both jars (java -Djarmode=tools -jar app.jar extract)"
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app.jar" extract --destination "$WORK/ext" >/dev/null
|
||||
"$JDK25/bin/java" -Djarmode=tools -jar "$WORK/app-springaot.jar" extract --destination "$WORK/ext-springaot" >/dev/null
|
||||
mv "$WORK/ext-springaot/app-springaot.jar" "$WORK/ext-springaot/app.jar" # the launcher jar keeps its lib/ Class-Path
|
||||
ls "$WORK/ext" "$WORK/ext-springaot"
|
||||
|
||||
echo "== build: the same service after a one-line change (the unit price in OrderService), for the mismatch and layer checks"
|
||||
mkdir -p "$WORK/changed-src"
|
||||
cp -r pom.xml src "$WORK/changed-src/"
|
||||
sed -i 's/BigDecimal.valueOf(4_99, 2)/BigDecimal.valueOf(5_99, 2)/' "$WORK/changed-src/src/main/java/com/ankurm/aotcache/OrderService.java"
|
||||
grep -n 'valueOf(5_99' "$WORK/changed-src/src/main/java/com/ankurm/aotcache/OrderService.java" | sed 's/^/edited: /'
|
||||
( cd "$WORK/changed-src" && JAVA_HOME=$JDK25 mvn_run mvn -q -B -DskipTests package > "$WORK/build-changed.log" 2>&1 )
|
||||
cp "$WORK/changed-src/target/app.jar" "$WORK/app-changed.jar"
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
# A transcript of the service's own endpoints, so the reader knows what the training run's traffic is.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
start_app "$WORK/ext" "$WORK/sample.log" "$JDK25/bin/java" -jar app.jar --server.port=$PORT
|
||||
wait_ready || { echo "did not start"; exit 1; }
|
||||
show() { # print the command the way a shell would need it typed, then run it
|
||||
printf '$'; for a in "$@"; do case $a in *[' {}"']*) printf " '%s'" "$a";; *) printf ' %s' "$a";; esac; done; echo
|
||||
"$@" -s -w ' [HTTP %{http_code}]\n'
|
||||
}
|
||||
show curl "localhost:$PORT/ping"
|
||||
show curl -H 'Content-Type: application/json' -d "$BODY" "localhost:$PORT/orders"
|
||||
show curl "localhost:$PORT/orders/1"
|
||||
show curl -H 'Content-Type: application/json' -d '{"customer":"bad","sku":"x","quantity":0}' "localhost:$PORT/orders"
|
||||
show curl "localhost:$PORT/orders/99"
|
||||
show curl "localhost:$PORT/actuator/health"
|
||||
stop_app
|
||||
exit 0
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
# Startup and first-request latency for every configuration. Configurations are run in interleaved rounds
|
||||
# (round 1 runs all of them once, then round 2, ...) so slow drift on a shared machine hits all of them alike.
|
||||
# scripts/startup.sh [ROUNDS] default 10
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
ROUNDS=${1:-10}
|
||||
LOG="$WORK/startup-runs.txt"; : > "$LOG"
|
||||
|
||||
declare -A WD CMD
|
||||
labels=()
|
||||
add_cfg() { local l=$1 wd=$2; shift 2; labels+=("$l"); WD[$l]=$wd; CMD[$l]="$*"; }
|
||||
|
||||
J25=$JDK25/bin/java; J27=$JDK27/bin/java
|
||||
add_cfg "1 jvm-fat-jar" "$WORK" "$J25 -jar app.jar"
|
||||
add_cfg "2 jvm-extracted" "$WORK/ext" "$J25 -jar app.jar"
|
||||
add_cfg "3 jvm+spring-aot" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -jar app.jar"
|
||||
add_cfg "4 appcds" "$WORK/ext" "$J25 -XX:SharedArchiveFile=$WORK/appcds.jsa -jar app.jar"
|
||||
add_cfg "5 aot-cache fat-jar" "$WORK" "$J25 -XX:AOTCache=$WORK/fat.aot -jar app.jar"
|
||||
add_cfg "6 aot-cache context-only" "$WORK/ext" "$J25 -XX:AOTCache=$WORK/refresh.aot -jar app.jar"
|
||||
add_cfg "7 aot-cache traffic" "$WORK/ext" "$J25 -XX:AOTCache=$WORK/traffic.aot -jar app.jar"
|
||||
add_cfg "8 aot-cache+spring-aot" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -XX:AOTCache=$WORK/springaot.aot -jar app.jar"
|
||||
add_cfg "9 aot-cache+spring-aot traffic" "$WORK/ext-springaot" "$J25 -Dspring.aot.enabled=true -XX:AOTCache=$WORK/springaot-traffic.aot -jar app.jar"
|
||||
add_cfg "10 jdk27 plain" "$WORK/ext" "$J27 -jar app.jar"
|
||||
add_cfg "11 jdk27 aot-cache" "$WORK/ext" "$J27 -XX:AOTCache=$WORK/refresh27.aot -jar app.jar"
|
||||
[ -x "$WORK/aot-cache-native" ] && add_cfg "12 native image" "$WORK" "$WORK/aot-cache-native"
|
||||
|
||||
one_run() { # label round
|
||||
local l=$1 r=$2 t0 ready started first second med50 rss
|
||||
t0=$(now_ms)
|
||||
start_app "${WD[$l]}" "$WORK/run.log" ${CMD[$l]} --server.port=$PORT
|
||||
if ! wait_ready; then echo "RUN|$l|$r|FAILED" >> "$LOG"; stop_app; return; fi
|
||||
ready=$(( $(now_ms) - t0 ))
|
||||
started=$(grep -o 'Started [A-Za-z]* in [0-9.]* seconds' "$WORK/run.log" | awk '{print $4*1000}')
|
||||
first=$(post_order); second=$(get_order)
|
||||
med50=$(for i in $(seq 1 50); do post_order; echo; done | grep . | median)
|
||||
rss=$(rss_mb)
|
||||
echo "RUN|$l|$r|ready=$ready|started=${started:-n/a}|first=$first|second=$second|next50=$med50|rss=$rss" >> "$LOG"
|
||||
stop_app; sleep 0.5
|
||||
}
|
||||
|
||||
echo "# ${#labels[@]} configurations x $ROUNDS rounds; one unrecorded warm-up round first (so cache files are in the page cache)"
|
||||
for l in "${labels[@]}"; do one_run "$l" 0; done; : > "$LOG"
|
||||
for r in $(seq 1 $ROUNDS); do
|
||||
for l in "${labels[@]}"; do one_run "$l" "$r"; done
|
||||
echo "round $r done" >&2
|
||||
done
|
||||
|
||||
field() { grep -F "RUN|$1|" "$LOG" | grep -o "$2=[0-9.]*" | cut -d= -f2; }
|
||||
echo
|
||||
echo "== medians of $ROUNDS runs (min..max in brackets), milliseconds; rss in MB"
|
||||
printf '%-32s %-20s %-20s %-20s %-18s %s\n' "configuration" "ready (exec->200)" "Boot 'Started in'" "1st POST /orders" "next 50 (median)" "RSS"
|
||||
for l in "${labels[@]}"; do
|
||||
f=$(grep -cF "RUN|$l|" "$LOG"); ok=$(grep -F "RUN|$l|" "$LOG" | grep -vc FAILED)
|
||||
cell() { local v; v=$(field "$l" "$1"); printf '%-20s ' "$(echo "$v" | median) [$(echo "$v" | minmax)]"; }
|
||||
printf '%-32s ' "$l"; cell ready; cell started; cell first; printf '%-18s ' "$(field "$l" next50 | median)"; printf '%s' "$(field "$l" rss | median)"
|
||||
[ "$ok" != "$f" ] && printf ' (%s of %s runs failed)' $((f-ok)) $f
|
||||
echo
|
||||
done
|
||||
echo
|
||||
echo "== every run"
|
||||
sed 's/^RUN|//; s/|/ /g' "$LOG"
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# The training runs: one per cache flavour. Prints how long each took, how big the artifact is, and what the
|
||||
# JVM logged at warning level while writing it. Needs scripts/prepare.sh to have run.
|
||||
source "$(dirname "$0")/lib.sh"
|
||||
|
||||
exercise() { # the traffic a "real" training run sends: success, validation failure, 404, actuator
|
||||
for i in 1 2 3 4 5; do post_order >/dev/null; done
|
||||
get_order >/dev/null
|
||||
curl -s -o /dev/null -H 'Content-Type: application/json' -d '{"customer":"bad","sku":"x","quantity":0}' "localhost:$PORT/orders"
|
||||
curl -s -o /dev/null "localhost:$PORT/orders/99"
|
||||
curl -s -o /dev/null "localhost:$PORT/actuator/health"
|
||||
}
|
||||
|
||||
report() { # label file t0
|
||||
printf '%-34s %6d ms %s (%s MB)\n' "$1" $(( $(now_ms) - $3 )) "$(basename "$2")" "$(( $(stat -c %s "$2") / 1048576 ))"
|
||||
}
|
||||
|
||||
echo "# training runs (JDK 25.0.4.1 unless noted); wall time includes JVM start, context start and cache creation"
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext" && "$JDK25/bin/java" -XX:ArchiveClassesAtExit="$WORK/appcds.jsa" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-appcds.log" 2>&1
|
||||
report "AppCDS (ArchiveClassesAtExit)" "$WORK/appcds.jsa" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
"$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/fat.aot" -Dspring.context.exit=onRefresh -jar "$WORK/app.jar" > "$WORK/train-fat.log" 2>&1
|
||||
report "AOT cache, fat jar, context-only" "$WORK/fat.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext" && "$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/refresh.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-refresh.log" 2>&1
|
||||
report "AOT cache, context-only training" "$WORK/refresh.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
start_app "$WORK/ext" "$WORK/train-traffic.log" "$JDK25/bin/java" -XX:AOTCacheOutput="$WORK/traffic.aot" -jar app.jar --server.port=$PORT
|
||||
wait_ready && exercise
|
||||
kill -TERM "$APP_PID"; wait "$APP_PID" 2>/dev/null
|
||||
report "AOT cache, traffic training" "$WORK/traffic.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext-springaot" && "$JDK25/bin/java" -Dspring.aot.enabled=true -XX:AOTCacheOutput="$WORK/springaot.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-springaot.log" 2>&1
|
||||
report "AOT cache + Spring AOT, context-only" "$WORK/springaot.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
start_app "$WORK/ext-springaot" "$WORK/train-springaot-traffic.log" "$JDK25/bin/java" -Dspring.aot.enabled=true -XX:AOTCacheOutput="$WORK/springaot-traffic.aot" -jar app.jar --server.port=$PORT
|
||||
wait_ready && exercise
|
||||
kill -TERM "$APP_PID"; wait "$APP_PID" 2>/dev/null
|
||||
report "AOT cache + Spring AOT, traffic" "$WORK/springaot-traffic.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext" && "$JDK27/bin/java" -XX:AOTCacheOutput="$WORK/refresh27.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-refresh27.log" 2>&1
|
||||
report "AOT cache on JDK 27, context-only" "$WORK/refresh27.aot" $t0
|
||||
|
||||
t0=$(now_ms)
|
||||
( cd "$WORK/ext" && "$JDK27/bin/java" -XX:+UseZGC -XX:AOTCacheOutput="$WORK/zgc27.aot" -Dspring.context.exit=onRefresh -jar app.jar ) > "$WORK/train-zgc27.log" 2>&1
|
||||
report "AOT cache on JDK 27, trained under ZGC" "$WORK/zgc27.aot" $t0
|
||||
|
||||
echo
|
||||
echo "# what the JVM said at warning level while writing the context-only AOT cache (count, then the distinct kinds)"
|
||||
grep -c '\[warning\]\[aot\]' "$WORK/train-refresh.log" | sed 's/^/warning lines: /'
|
||||
grep '\[warning\]\[aot\]' "$WORK/train-refresh.log" | sed -E 's/^\[[0-9.]+s\]//; s/(Skipping) [^ ]+:/\1 <class>:/' | sort | uniq -c | sort -rn | head -5
|
||||
Reference in New Issue
Block a user