Add jdk27-perf: the same Spring Boot service measured on JDK 25 and JDK 27

Live heap, throughput, p99 and startup across four configurations, including opt-in and opt-out of
compact object headers.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WzVQ6zFh9cuLPj9RSbRH8U
This commit is contained in:
2026-10-12 17:30:55 +00:00
co-authored by Claude Sonnet 5.5
parent fc05eed4b5
commit d565be93c6
13 changed files with 388 additions and 1 deletions
+42
View File
@@ -0,0 +1,42 @@
import java.net.URI;
import java.net.http.*;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
/** Closed-loop load: THREADS threads, WARMUP seconds discarded, then MEASURE seconds recorded. */
public class Load {
public static void main(String[] a) throws Exception {
int port = Integer.parseInt(a[0]), warmup = Integer.parseInt(a[1]), measure = Integer.parseInt(a[2]), threads = Integer.parseInt(a[3]);
HttpClient c = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1).build();
HttpRequest rq = HttpRequest.newBuilder(URI.create("http://localhost:" + port + "/quote?n=200")).build();
long start = System.nanoTime();
long measureFrom = start + warmup * 1_000_000_000L, end = measureFrom + measure * 1_000_000_000L;
List<long[]> perThread = new CopyOnWriteArrayList<>();
ExecutorService ex = Executors.newFixedThreadPool(threads);
List<Future<?>> fs = new ArrayList<>();
for (int t = 0; t < threads; t++) fs.add(ex.submit(() -> {
long[] buf = new long[2_000_000]; int n = 0;
try {
while (true) {
long s = System.nanoTime();
if (s >= end) break;
c.send(rq, HttpResponse.BodyHandlers.discarding());
long e = System.nanoTime();
if (s >= measureFrom && n < buf.length) buf[n++] = e - s;
}
} catch (Exception e) { throw new RuntimeException(e); }
perThread.add(Arrays.copyOf(buf, n));
return null;
}));
for (Future<?> f : fs) f.get();
ex.shutdown();
long total = 0; for (long[] b : perThread) total += b.length;
long[] all = new long[(int) total]; int p = 0;
for (long[] b : perThread) { System.arraycopy(b, 0, all, p, b.length); p += b.length; }
Arrays.sort(all);
System.out.printf("requests=%d throughput=%.0f req/s p50=%.2f ms p99=%.2f ms p99.9=%.2f ms max=%.2f ms%n",
total, total / (double) measure, all[(int) (total * 0.50)] / 1e6, all[(int) (total * 0.99)] / 1e6,
all[(int) (total * 0.999)] / 1e6, all[(int) total - 1] / 1e6);
}
}
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Usage: live-set.sh <java-home> <label> [jvm args...]
# Starts the server, forces a full GC through /live, and prints the heap still in use.
# This is the resident data set: 3,000,000 two-int records plus the Spring Boot baseline.
set -u
JH=$1; LABEL=$2; shift 2
JAR="$(dirname "$0")/../target/jdk27-perf-1.0.0.jar"
PORT=${PORT:-18082}
"$JH/bin/java" "$@" -jar "$JAR" --server.port=$PORT --spring.main.banner-mode=off \
--logging.level.root=WARN > /dev/null 2>&1 &
PID=$!
until [ "$(curl -s -o /dev/null -m 2 -w '%{http_code}' "localhost:$PORT/stats")" = 200 ]; do sleep 0.05; done
live=$(curl -s "localhost:$PORT/live" | sed -n 's/.*"liveHeapMiB":\([0-9]*\).*/\1/p')
echo "$LABEL: live heap after full GC ${live} MiB"
kill $PID 2>/dev/null; wait $PID 2>/dev/null
+35
View File
@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Regenerates output/. Set J25 and J27. REPS defaults to 3. Configs are interleaved per repetition
# so slow drift on the machine hits every configuration equally. Timings are indicative only.
set -u
HERE="$(cd "$(dirname "$0")" && pwd)"
OUT="$HERE/../output"; mkdir -p "$OUT"
: "${J25:?}" "${J27:?}"
export JDK_CLIENT="$J25"
REPS=${REPS:-3}
{
for p in "25|$J25" "27|$J27"; do
v=${p%%|*}; h=${p#*|}
echo "== JDK $v"
"$h/bin/java" -version 2>&1 | grep -v Picked | sed -n 1p
"$h/bin/java" -XX:+PrintFlagsFinal -version 2>&1 | grep -E ' (UseCompactObjectHeaders|UseG1GC|UseSerialGC|UseZGC|MaxHeapSize) ' | sed 's/ \{2,\}/ /g'
echo
done
} > "$OUT/01-defaults.txt"
: > "$OUT/02-runs.txt"
for r in $(seq 1 "$REPS"); do
"$HERE/run-one.sh" "$J25" "jdk25 defaults" >> "$OUT/02-runs.txt" 2>&1
"$HERE/run-one.sh" "$J25" "jdk25 +CompactObjectHeaders" -XX:+UseCompactObjectHeaders >> "$OUT/02-runs.txt" 2>&1
"$HERE/run-one.sh" "$J27" "jdk27 defaults" >> "$OUT/02-runs.txt" 2>&1
"$HERE/run-one.sh" "$J27" "jdk27 -CompactObjectHeaders" -XX:-UseCompactObjectHeaders >> "$OUT/02-runs.txt" 2>&1
done
"$HERE/summarize.sh" "$OUT/02-runs.txt" > "$OUT/03-summary.txt"
# 04 - resident data set after a forced full GC (this is where compact headers show up)
{
for r in 1 2 3; do
"$HERE/live-set.sh" "$J25" "jdk25 defaults"
"$HERE/live-set.sh" "$J25" "jdk25 +CompactObjectHeaders" -XX:+UseCompactObjectHeaders
"$HERE/live-set.sh" "$J27" "jdk27 defaults"
"$HERE/live-set.sh" "$J27" "jdk27 -CompactObjectHeaders" -XX:-UseCompactObjectHeaders
done
} > "$OUT/04-live-set.txt" 2>&1
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Usage: run-one.sh <java-home> <label> [jvm args...]
# One cold start, then 15 s warmup + 30 s measured load, then a /stats snapshot. One result block.
set -u
JH=$1; LABEL=$2; shift 2
HERE="$(cd "$(dirname "$0")" && pwd)"
JAR="$HERE/../target/jdk27-perf-1.0.0.jar"
PORT=${PORT:-18081}
CLIENT=${JDK_CLIENT:-$JH}
t0=$(date +%s%N)
"$JH/bin/java" "$@" -jar "$JAR" --server.port=$PORT --spring.main.banner-mode=off \
--logging.level.root=WARN > /dev/null 2>&1 &
PID=$!
until [ "$(curl -s -o /dev/null -m 2 -w '%{http_code}' "localhost:$PORT/stats")" = 200 ]; do sleep 0.02; done
t1=$(date +%s%N)
echo "== $LABEL"
echo "startup_ms=$(( (t1 - t0) / 1000000 ))"
"$CLIENT/bin/java" "$HERE/Load.java" $PORT 15 30 ${THREADS:-4} 2>&1 | grep -v Picked
curl -s "localhost:$PORT/stats"; echo
kill $PID 2>/dev/null; wait $PID 2>/dev/null
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Usage: summarize.sh output/02-runs.txt -> median of each metric per label
awk '
/^== /{label=substr($0,4); next}
/^startup_ms=/{split($0,a,"="); st[label]=st[label] " " a[2]}
/^requests=/{for(i=1;i<=NF;i++){split($i,kv,"="); if(kv[1]=="throughput")tp[label]=tp[label] " " kv[2]; if(kv[1]=="p99")p99[label]=p99[label] " " kv[2]}}
/^\{"javaVersion"/{match($0,/"heapUsedMiB":[0-9]+/); h=substr($0,RSTART+14,RLENGTH-14); hu[label]=hu[label] " " h; match($0,/"rssMiB":[0-9]+/); r=substr($0,RSTART+9,RLENGTH-9); rss[label]=rss[label] " " r}
function med(s, n,arr,i,j,t){n=split(s,arr," "); for(i=1;i<=n;i++)for(j=i+1;j<=n;j++)if(arr[j]+0<arr[i]+0){t=arr[i];arr[i]=arr[j];arr[j]=t}; return arr[int((n+1)/2)]}
END{for(l in st) printf "%s | startup %s ms | throughput %s req/s | p99 %s ms | heap used %s MiB | RSS %s MiB\n", l, med(st[l]), med(tp[l]), med(p99[l]), med(hu[l]), med(rss[l])}' "$1" | sort