Spring Boot 4.1.1 catalog service measured on JDK 25, 26 and 27: live heap 320 MB to 273 MB with compact headers, per-class bytes per instance, container-sizing sweeps with real cgroup limits, 1-CPU Serial vs G1 with Native Memory Tracking, removed flags (MaxRAM, UseCompressedClassPointers), and a kernel OOM-kill check. Outputs in jdk27-memory/output. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01TF9JWFvJSNm6HVzswzZU5a
97 lines
4.8 KiB
Bash
Executable File
97 lines
4.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Shared helpers. Source this; do not run it.
|
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
MOD="$(cd "$HERE/.." && pwd)"
|
|
source "$HERE/env.sh"
|
|
JAR="$MOD/target/catalog.jar"
|
|
|
|
# ---- containers -----------------------------------------------------------------------------------------
|
|
# There is no Docker daemon in the sandbox, so limits are set the way Docker sets them: a cgroup with a
|
|
# memory limit and a CPU quota. The JVM's container support reads exactly those files (cgroup v1 here).
|
|
CGROOT=/sys/fs/cgroup
|
|
cg_create() { # NAME MEMORY_BYTES CPUS
|
|
mkdir -p "$CGROOT/memory/$1" "$CGROOT/cpu/$1"
|
|
echo "$2" >"$CGROOT/memory/$1/memory.limit_in_bytes"
|
|
echo 100000 >"$CGROOT/cpu/$1/cpu.cfs_period_us"
|
|
echo $(( $3 * 100000 )) >"$CGROOT/cpu/$1/cpu.cfs_quota_us"
|
|
}
|
|
cg_remove() { rmdir "$CGROOT/memory/$1" "$CGROOT/cpu/$1" 2>/dev/null; }
|
|
# In run_app the JVM is started as: bash -c '<join the cgroup>; exec java ...' &
|
|
# so that $! is the JVM's own pid (a function wrapper would make $! the wrapper's pid).
|
|
CG_JOIN='n=$1; shift; echo $$ >'"$CGROOT"'/memory/$n/cgroup.procs; echo $$ >'"$CGROOT"'/cpu/$n/cgroup.procs; exec "$@"'
|
|
|
|
# run_app JAVA_HOME N OUTDIR [jvm flags...]
|
|
# Starts the catalog on that JVM, loads N products, and leaves these files in OUTDIR:
|
|
# startup.txt memory-idle.txt load.txt memory.txt lookups.txt histogram.txt outcome.txt
|
|
# Set CG_MEM (bytes) and CG_CPUS to run inside a container-style cgroup. Set NMT=1 for native memory tracking.
|
|
# Returns 1 if the load did not complete; outcome.txt says why: java-OOM, killed (kernel OOM kill),
|
|
# no-result-in-25s (the JVM is alive but collecting garbage without making progress; a healthy load takes
|
|
# about a second), or error.
|
|
run_app_once() {
|
|
local jh="$1" n="$2" out="$3"; shift 3
|
|
local port=$((20000 + RANDOM % 20000))
|
|
local cg=""
|
|
mkdir -p "$out"
|
|
local extra=()
|
|
if [ -n "${NMT:-}" ]; then extra+=(-XX:NativeMemoryTracking=summary); fi
|
|
if [ -n "${CG_MEM:-}" ]; then
|
|
cg="jm$$-$RANDOM"; cg_create "$cg" "$CG_MEM" "${CG_CPUS:-2}"
|
|
bash -c "$CG_JOIN" _ "$cg" "$jh/bin/java" "${extra[@]}" "$@" -jar "$JAR" --server.port=$port >"$out/app.log" 2>&1 &
|
|
else
|
|
"$jh/bin/java" "${extra[@]}" "$@" -jar "$JAR" --server.port=$port >"$out/app.log" 2>&1 &
|
|
fi
|
|
local pid=$!
|
|
local up=0
|
|
for _ in $(seq 1 400); do
|
|
kill -0 $pid 2>/dev/null || break
|
|
if curl -sf "localhost:$port/ping" >/dev/null 2>&1; then up=1; break; fi
|
|
sleep 0.25
|
|
done
|
|
local rc=0
|
|
if [ $up = 0 ]; then
|
|
wait $pid 2>/dev/null; local st=$?
|
|
if grep -q OutOfMemoryError "$out/app.log"; then echo "java-OOM-at-startup" >"$out/outcome.txt"
|
|
elif [ $st -ge 128 ]; then echo "killed(exit $st)" >"$out/outcome.txt"; else echo "error(exit $st)" >"$out/outcome.txt"; fi
|
|
: >"$out/startup.txt"; [ -n "$cg" ] && cg_remove "$cg"; return 1
|
|
fi
|
|
grep -o 'Started MemoryApplication in [0-9.]* seconds' "$out/app.log" >"$out/startup.txt"
|
|
curl -s --max-time 60 "localhost:$port/memory" >"$out/memory-idle.txt"
|
|
curl -s --max-time 25 "localhost:$port/load?n=$n" >"$out/load.txt"; local crc=$?
|
|
if ! grep -q '^loaded=' "$out/load.txt"; then
|
|
rc=1
|
|
if [ $crc -eq 28 ]; then echo "no-result-in-25s" >"$out/outcome.txt"
|
|
elif ! kill -0 $pid 2>/dev/null; then wait $pid 2>/dev/null; echo "killed(exit $?)" >"$out/outcome.txt"
|
|
elif grep -q OutOfMemoryError "$out/app.log" "$out/load.txt"; then echo "java-OOM" >"$out/outcome.txt"
|
|
else echo "error" >"$out/outcome.txt"; fi
|
|
else
|
|
curl -s --max-time 60 "localhost:$port/memory" >"$out/memory.txt"
|
|
curl -s --max-time 60 "localhost:$port/lookups?n=2000000" >"$out/lookups.txt"
|
|
"$jh/bin/jcmd" $pid GC.class_histogram >"$out/histogram.txt" 2>&1
|
|
if [ -n "${NMT:-}" ]; then "$jh/bin/jcmd" $pid VM.native_memory summary >"$out/nmt.txt" 2>&1; fi
|
|
if kill -0 $pid 2>/dev/null; then echo "ok" >"$out/outcome.txt"; else wait $pid 2>/dev/null; echo "killed(exit $?)" >"$out/outcome.txt"; rc=1; fi
|
|
fi
|
|
kill $pid 2>/dev/null; wait $pid 2>/dev/null
|
|
[ -n "$cg" ] && cg_remove "$cg"
|
|
return $rc
|
|
}
|
|
|
|
# run_app: run_app_once, but a run whose JVM never came up for a reason that is neither OutOfMemoryError
|
|
# nor a kernel kill (in practice: the random port was taken) is retried, and the retry is counted in
|
|
# $out/retries so that a summary can say so instead of hiding it.
|
|
run_app() {
|
|
local out="$3" tries=0
|
|
while :; do
|
|
run_app_once "$@"; local rc=$?
|
|
if [ $rc -ne 0 ] && grep -q '^error(exit' "$out/outcome.txt" 2>/dev/null && [ ! -s "$out/startup.txt" ] && [ $tries -lt 2 ]; then
|
|
tries=$((tries+1)); echo $tries >"$out/retries"; continue
|
|
fi
|
|
return $rc
|
|
done
|
|
}
|
|
|
|
# kv FILE KEY -> value
|
|
kv() { grep "^$2=" "$1" | head -1 | cut -d= -f2-; }
|
|
|
|
# median of numbers on stdin
|
|
median() { sort -n | awk '{a[NR]=$1} END{ if(NR%2){print a[(NR+1)/2]} else {printf "%d\n",(a[NR/2]+a[NR/2+1])/2} }'; }
|