From b25bd65d4b38d8ebd2ab834ad37d1a889617f7ff Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 24 Oct 2026 10:53:17 +0000 Subject: [PATCH] Add shenandoah-jdk28: generational vs SATB Shenandoah, G1 and ZGC on a Spring Boot service (JEP 535 status probe included) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01HmvoHH3ZnCEaPNXRpDxDZe --- README.md | 1 + shenandoah-jdk28/README.md | 62 ++++++++++++ shenandoah-jdk28/output/01-gc-mode-probe.txt | 20 ++++ shenandoah-jdk28/output/02-runs.txt | 20 ++++ shenandoah-jdk28/output/03-summary.txt | 7 ++ shenandoah-jdk28/pom.xml | 33 +++++++ shenandoah-jdk28/scripts/LoadGen.java | 94 +++++++++++++++++++ shenandoah-jdk28/scripts/bench-one.sh | 35 +++++++ shenandoah-jdk28/scripts/gc_pauses.py | 33 +++++++ shenandoah-jdk28/scripts/more-reps.sh | 13 +++ shenandoah-jdk28/scripts/probe.sh | 17 ++++ shenandoah-jdk28/scripts/run-all.sh | 21 +++++ shenandoah-jdk28/scripts/summarize.py | 33 +++++++ .../shenandoah/ShenandoahApplication.java | 71 ++++++++++++++ 14 files changed, 460 insertions(+) create mode 100644 shenandoah-jdk28/README.md create mode 100644 shenandoah-jdk28/output/01-gc-mode-probe.txt create mode 100644 shenandoah-jdk28/output/02-runs.txt create mode 100644 shenandoah-jdk28/output/03-summary.txt create mode 100644 shenandoah-jdk28/pom.xml create mode 100644 shenandoah-jdk28/scripts/LoadGen.java create mode 100755 shenandoah-jdk28/scripts/bench-one.sh create mode 100755 shenandoah-jdk28/scripts/gc_pauses.py create mode 100755 shenandoah-jdk28/scripts/more-reps.sh create mode 100755 shenandoah-jdk28/scripts/probe.sh create mode 100755 shenandoah-jdk28/scripts/run-all.sh create mode 100755 shenandoah-jdk28/scripts/summarize.py create mode 100644 shenandoah-jdk28/src/main/java/com/ankurm/shenandoah/ShenandoahApplication.java diff --git a/README.md b/README.md index 441c513..9905ea2 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,7 @@ the part most "ZGC is faster" posts leave out. | [`analysis/GcLogParser.java`](analysis/src/com/ankurm/zgc/analysis/GcLogParser.java) | Unified-log parser that keeps pauses and concurrent phases apart | none | | [`jmh/`](jmh/src/main/java/com/ankurm/zgc/jmh) | Throughput and barrier microbenchmarks | JMH 1.37 | | [`jdk27-memory/`](jdk27-memory) | Spring Boot 4.1 service measured on JDK 25, 26 and 27: compact object headers (JEP 534) and G1 everywhere (JEP 523), heap footprint, container sizing with real cgroup limits. Its README and `output/` replace a `docs/` folder; the write-up is [the post](https://ankurm.com/jdk-27-compact-object-headers-g1-default-memory-spring-boot/) | Spring Boot 4.1.1, three JDKs | +| [`shenandoah-jdk28/`](shenandoah-jdk28) | Spring Boot 4.1 service on G1, generational ZGC, SATB Shenandoah and generational Shenandoah (JEP 535, JDK 28 EA): latency under fixed-rate load, GC pauses, throughput, RSS. Its README and `output/` replace a `docs/` folder | Spring Boot 4.1.2, Temurin 25 and 28 EA | | [`flags/`](flags) | The flag-by-flag production cheat sheet: `MaxRAMPercentage` vs a hardcoded `-Xmx` (one fails safely, one gets the container SIGKILLed), `-XX:-UseContainerSupport`, the `ActiveProcessorCount` lie, and which diagnostic flags (`HeapDumpOnOutOfMemoryError`, `NativeMemoryTracking`) are worth leaving on in prod — on real Docker containers, JDK 25 and JDK 27. Its README and `docs/output/` replace a `docs/` folder; the write-up is [the post](https://ankurm.com/jvm-flags-for-spring-boot-in-containers-jdk-25-27/) | Spring Boot 4.1.1, Docker, two JDKs | | [`jdk28-zgc/`](jdk28-zgc) | ZGC startup and warmup versus initial heap size on JDK 25, 27 and 28 early access with a Spring Boot 4.1.1 server, plus a probe showing that JEP 545/546's `-XX:+ZAdaptiveHeapSizing` is not in any build yet. Its README and `output/` replace a `docs/` folder; the write-up is [the post](https://ankurm.com/zgc-jdk-28-adaptive-heap-sizing-faster-startup/) | Spring Boot 4.1.1, three JDKs | diff --git a/shenandoah-jdk28/README.md b/shenandoah-jdk28/README.md new file mode 100644 index 0000000..12222dc --- /dev/null +++ b/shenandoah-jdk28/README.md @@ -0,0 +1,62 @@ +# shenandoah-jdk28 — generational Shenandoah vs SATB Shenandoah vs G1 vs ZGC on a Spring Boot service + +Companion module for the ankurm.com post +**Generational Shenandoah by Default in JDK 28 (JEP 535): Benchmarked Against G1 and ZGC**. +There is no `docs/` folder: this README and the captured files in [`output/`](output) are the documentation. + +## The honest status + +JEP 535 ("Shenandoah: Generational Mode by Default", which also deprecates the SATB mode) is **Targeted** to JDK 28 on +openjdk.org, but it is **not integrated in Temurin 28+21 EA**. [`output/01-gc-mode-probe.txt`](output/01-gc-mode-probe.txt) +shows `-XX:+UseShenandoahGC` still starts in *Snapshot-At-The-Beginning (SATB)* mode on both JDK 25.0.5+8 and 28+21. +Generational mode is available on both as an explicit opt-in (`-XX:ShenandoahGCMode=generational`). +So this module benchmarks **explicit generational vs explicit SATB**, which is what the default will switch between. +Oracle's jdk.java.net builds do not contain Shenandoah at all; use Temurin (or another build that ships it). + +## Versions + +| Component | Version | +|---|---| +| JDK 25 | Temurin 25.0.5+8 (probe only) | +| JDK 28 | Temurin 28+21 EA (28-beta+21-ea, build 202610221655) | +| Spring Boot | 4.1.2 (`spring-boot-starter-webmvc`) | +| Machine | 2 vCPU, 8 GB RAM Linux sandbox. A small box: absolute numbers are illustrative | +| Heap | `-Xms1g -Xmx1g` for every configuration | + +## Quick start + +```console +mvn -B -DskipTests package +export J25=/path/to/temurin-25 J28=/path/to/temurin-28-ea +scripts/run-all.sh # probe + 3 interleaved reps of 4 configurations, ~2.5 min per run +scripts/more-reps.sh 4 5 # append more reps and re-summarise +``` + +## What is measured + +| File | What it is | +|---|---| +| [`src/main/java/com/ankurm/shenandoah/ShenandoahApplication.java`](src/main/java/com/ankurm/shenandoah/ShenandoahApplication.java) | `/work` replaces 8 of 200,000 1 KiB cache slots and allocates 100 short-lived 1 KiB arrays; `/info` reports the heap | +| [`scripts/LoadGen.java`](scripts/LoadGen.java) | `open`: fixed-rate load (3,000 req/s, 64 virtual threads), latency measured from the *intended* send time; `closed`: peak throughput | +| [`scripts/bench-one.sh`](scripts/bench-one.sh) | One run: 30 s warm-up, 60 s open-loop window, 20 s closed loop; pauses from `-Xlog:gc*`; peak RSS from `VmHWM` | +| [`scripts/gc_pauses.py`](scripts/gc_pauses.py) | Stop-the-world pauses inside the measure window | +| [`scripts/summarize.py`](scripts/summarize.py) | Median per configuration | + +## Results (median of 5 interleaved runs, JDK 28+21 EA) + +[`output/03-summary.txt`](output/03-summary.txt), from the per-run lines in [`output/02-runs.txt`](output/02-runs.txt): + +| Configuration | p50 ms | p99 ms | p99.9 ms | max ms | peak req/s | pause max ms | RSS MB | +|---|---|---|---|---|---|---|---| +| G1 | 0.63 | 29.2 | 128.1 | 179.5 | 8,900 | 111.6 | 1,205 | +| ZGC (generational) | 0.60 | 7.4 | 17.3 | 36.4 | 7,117 | 0.17 | 1,250 | +| Shenandoah SATB | 0.78 | 14.9 | 43.6 | 86.4 | 5,026 | 1.5 | 1,212 | +| Shenandoah generational | 0.82 | 1,557.5 | 1,820.4 | 1,864.7 | 5,498 | 2.6 | 1,227 | + +**Read the last row carefully.** Per-run p99 for generational Shenandoah was 1,557, 3,077, 24.5, 1,658 and 692 ms: four of five +runs contain a multi-second latency episode while the recorded stop-the-world pauses stay at a few milliseconds +(one run had a 209 ms `Pause Full`). The GC logs do not show allocation failures (`Trigger: Failed to allocate` count is 0), and +the `Cancelling GC: Concurrent GC` lines are old-generation marking being preempted for a young collection, which is normal. +So the cause of the stalls is **not established** here. One SATB run (811 ms p99) also stalled with no GC event at all, +which says this 2-vCPU machine produces stalls of its own. Treat the generational result as "did not behave on this box +and this workload", not as a general verdict. A larger machine and a bigger heap are the obvious next experiment. diff --git a/shenandoah-jdk28/output/01-gc-mode-probe.txt b/shenandoah-jdk28/output/01-gc-mode-probe.txt new file mode 100644 index 0000000..07e2e1b --- /dev/null +++ b/shenandoah-jdk28/output/01-gc-mode-probe.txt @@ -0,0 +1,20 @@ +== JDK 25: OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing) +-- java -XX:+UseShenandoahGC -Xlog:gc+init -version +[0.009s][info][gc,init] Mode: Snapshot-At-The-Beginning (SATB) +-- java -XX:+UseShenandoahGC -XX:+PrintFlagsFinal -version (ShenandoahGCMode) + ccstr ShenandoahGCMode = satb {product} {default} +-- java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xlog:gc+init -version +[0.009s][info][gc,init] Mode: Generational +-- java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=satb -Xlog:gc+init -version +[0.007s][info][gc,init] Mode: Snapshot-At-The-Beginning (SATB) + +== JDK 28 EA: OpenJDK 64-Bit Server VM Temurin-28+21-202610221655 (build 28-beta+21-ea, mixed mode, sharing) +-- java -XX:+UseShenandoahGC -Xlog:gc+init -version +[0.006s][info][gc,init] Mode: Snapshot-At-The-Beginning (SATB) +-- java -XX:+UseShenandoahGC -XX:+PrintFlagsFinal -version (ShenandoahGCMode) + ccstr ShenandoahGCMode = satb {product} {default} +-- java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational -Xlog:gc+init -version +[0.008s][info][gc,init] Mode: Generational +-- java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=satb -Xlog:gc+init -version +[0.007s][info][gc,init] Mode: Snapshot-At-The-Beginning (SATB) + diff --git a/shenandoah-jdk28/output/02-runs.txt b/shenandoah-jdk28/output/02-runs.txt new file mode 100644 index 0000000..7e47039 --- /dev/null +++ b/shenandoah-jdk28/output/02-runs.txt @@ -0,0 +1,20 @@ +label=g1 rep=1 target=3000 achieved=3000.0 requests=180000 p50_ms=0.61 p99_ms=28.69 p999_ms=95.48 max_ms=154.41 errors=0 peak_rps=8399.5 pauses=65 pause_p50_ms=23.459 pause_p99_ms=115.550 pause_max_ms=115.550 pause_total_ms=1349.5 rss_peak_mb=1205 heap_committed_mb=1024 +label=zgc rep=1 target=3000 achieved=3000.0 requests=180000 p50_ms=0.60 p99_ms=7.32 p999_ms=65.10 max_ms=108.85 errors=0 peak_rps=7540.4 pauses=158 pause_p50_ms=0.014 pause_p99_ms=0.060 pause_max_ms=0.141 pause_total_ms=2.6 rss_peak_mb=1250 heap_committed_mb=1024 +label=shenandoah-satb rep=1 target=3000 achieved=3000.0 requests=180000 p50_ms=0.80 p99_ms=14.94 p999_ms=29.12 max_ms=53.70 errors=0 peak_rps=5025.8 pauses=204 pause_p50_ms=0.165 pause_p99_ms=0.945 pause_max_ms=1.125 pause_total_ms=44.1 rss_peak_mb=1213 heap_committed_mb=1024 +label=shenandoah-generational rep=1 target=3000 achieved=3000.0 requests=180000 p50_ms=0.89 p99_ms=1557.51 p999_ms=1866.05 max_ms=1906.45 errors=0 peak_rps=5484.3 pauses=319 pause_p50_ms=0.170 pause_p99_ms=1.236 pause_max_ms=1.418 pause_total_ms=76.5 rss_peak_mb=1227 heap_committed_mb=1024 +label=g1 rep=2 target=3000 achieved=3000.0 requests=180000 p50_ms=0.66 p99_ms=29.17 p999_ms=128.06 max_ms=179.49 errors=0 peak_rps=8899.7 pauses=63 pause_p50_ms=24.557 pause_p99_ms=110.232 pause_max_ms=110.232 pause_total_ms=1366.7 rss_peak_mb=1207 heap_committed_mb=1024 +label=zgc rep=2 target=3000 achieved=3000.0 requests=180000 p50_ms=0.60 p99_ms=6.98 p999_ms=15.80 max_ms=33.36 errors=0 peak_rps=7060.8 pauses=154 pause_p50_ms=0.014 pause_p99_ms=0.090 pause_max_ms=0.183 pause_total_ms=2.6 rss_peak_mb=1241 heap_committed_mb=1024 +label=shenandoah-satb rep=2 target=3000 achieved=3000.0 requests=180000 p50_ms=0.78 p99_ms=14.58 p999_ms=27.28 max_ms=56.25 errors=0 peak_rps=4542.2 pauses=204 pause_p50_ms=0.160 pause_p99_ms=0.981 pause_max_ms=1.067 pause_total_ms=46.0 rss_peak_mb=1212 heap_committed_mb=1024 +label=shenandoah-generational rep=2 target=3000 achieved=3000.0 requests=180000 p50_ms=0.93 p99_ms=3077.07 p999_ms=3222.87 max_ms=3284.93 errors=0 peak_rps=5497.7 pauses=328 pause_p50_ms=0.184 pause_p99_ms=1.544 pause_max_ms=209.368 pause_total_ms=292.7 rss_peak_mb=1219 heap_committed_mb=1024 +label=g1 rep=3 target=3000 achieved=3000.0 requests=180000 p50_ms=0.65 p99_ms=29.18 p999_ms=137.90 max_ms=194.77 errors=0 peak_rps=9385.5 pauses=65 pause_p50_ms=24.353 pause_p99_ms=123.149 pause_max_ms=123.149 pause_total_ms=1383.9 rss_peak_mb=1203 heap_committed_mb=1024 +label=zgc rep=3 target=3000 achieved=3000.0 requests=180000 p50_ms=0.62 p99_ms=7.37 p999_ms=15.73 max_ms=36.38 errors=0 peak_rps=7816.9 pauses=156 pause_p50_ms=0.014 pause_p99_ms=0.099 pause_max_ms=0.184 pause_total_ms=2.6 rss_peak_mb=1257 heap_committed_mb=1024 +label=shenandoah-satb rep=3 target=3000 achieved=3000.0 requests=180000 p50_ms=0.69 p99_ms=811.54 p999_ms=1116.58 max_ms=1186.35 errors=0 peak_rps=5402.7 pauses=213 pause_p50_ms=0.144 pause_p99_ms=1.012 pause_max_ms=2.851 pause_total_ms=43.8 rss_peak_mb=1212 heap_committed_mb=1024 +label=shenandoah-generational rep=3 target=3000 achieved=3000.0 requests=180000 p50_ms=0.82 p99_ms=24.49 p999_ms=103.01 max_ms=150.61 errors=0 peak_rps=5270.0 pauses=282 pause_p50_ms=0.172 pause_p99_ms=1.398 pause_max_ms=2.988 pause_total_ms=72.0 rss_peak_mb=1229 heap_committed_mb=1024 +label=g1 rep=4 target=3000 achieved=3000.0 requests=180000 p50_ms=0.63 p99_ms=42.72 p999_ms=117.79 max_ms=146.07 errors=0 peak_rps=8847.7 pauses=55 pause_p50_ms=25.266 pause_p99_ms=111.576 pause_max_ms=111.576 pause_total_ms=1693.4 rss_peak_mb=1205 heap_committed_mb=1024 +label=zgc rep=4 target=3000 achieved=3000.0 requests=180000 p50_ms=0.67 p99_ms=7.98 p999_ms=17.28 max_ms=31.52 errors=0 peak_rps=6923.5 pauses=155 pause_p50_ms=0.015 pause_p99_ms=0.097 pause_max_ms=0.168 pause_total_ms=3.0 rss_peak_mb=1250 heap_committed_mb=1024 +label=shenandoah-satb rep=4 target=3000 achieved=3000.0 requests=180000 p50_ms=0.87 p99_ms=21.25 p999_ms=43.63 max_ms=86.39 errors=0 peak_rps=4639.2 pauses=210 pause_p50_ms=0.163 pause_p99_ms=0.743 pause_max_ms=1.475 pause_total_ms=44.1 rss_peak_mb=1209 heap_committed_mb=1024 +label=shenandoah-generational rep=4 target=3000 achieved=3000.0 requests=180000 p50_ms=0.75 p99_ms=1657.87 p999_ms=1820.38 max_ms=1864.68 errors=0 peak_rps=5519.6 pauses=293 pause_p50_ms=0.177 pause_p99_ms=2.230 pause_max_ms=2.553 pause_total_ms=76.6 rss_peak_mb=1220 heap_committed_mb=1024 +label=g1 rep=5 target=3000 achieved=3000.0 requests=180000 p50_ms=0.60 p99_ms=66.31 p999_ms=379.56 max_ms=423.34 errors=0 peak_rps=9775.1 pauses=70 pause_p50_ms=25.645 pause_p99_ms=101.489 pause_max_ms=101.489 pause_total_ms=1443.9 rss_peak_mb=1202 heap_committed_mb=1024 +label=zgc rep=5 target=3000 achieved=3000.0 requests=180000 p50_ms=0.59 p99_ms=7.49 p999_ms=48.76 max_ms=83.78 errors=0 peak_rps=7117.4 pauses=147 pause_p50_ms=0.014 pause_p99_ms=0.080 pause_max_ms=0.112 pause_total_ms=2.4 rss_peak_mb=1243 heap_committed_mb=1024 +label=shenandoah-satb rep=5 target=3000 achieved=3000.0 requests=180000 p50_ms=0.64 p99_ms=12.71 p999_ms=62.14 max_ms=101.88 errors=0 peak_rps=5448.0 pauses=191 pause_p50_ms=0.147 pause_p99_ms=1.192 pause_max_ms=1.512 pause_total_ms=36.9 rss_peak_mb=1214 heap_committed_mb=1024 +label=shenandoah-generational rep=5 target=3000 achieved=3000.0 requests=180000 p50_ms=0.72 p99_ms=691.58 p999_ms=916.21 max_ms=959.93 errors=0 peak_rps=5745.0 pauses=277 pause_p50_ms=0.176 pause_p99_ms=1.283 pause_max_ms=1.359 pause_total_ms=67.2 rss_peak_mb=1231 heap_committed_mb=1024 diff --git a/shenandoah-jdk28/output/03-summary.txt b/shenandoah-jdk28/output/03-summary.txt new file mode 100644 index 0000000..784c2c6 --- /dev/null +++ b/shenandoah-jdk28/output/03-summary.txt @@ -0,0 +1,7 @@ +median of 5 runs per configuration (latency from a fixed-rate open loop; throughput from a closed loop) + +configuration p50 ms p99 ms p99.9 ms max ms peak req/s pauses pause p99 ms pause max ms pause total ms RSS MB +g1 0.63 29.2 128.1 179.5 8900 65 111.576 111.576 1384 1205 +zgc 0.60 7.4 17.3 36.4 7117 155 0.090 0.168 3 1250 +shenandoah-satb 0.78 14.9 43.6 86.4 5026 204 0.981 1.475 44 1212 +shenandoah-generational 0.82 1557.5 1820.4 1864.7 5498 293 1.398 2.553 76 1227 diff --git a/shenandoah-jdk28/pom.xml b/shenandoah-jdk28/pom.xml new file mode 100644 index 0000000..8418c37 --- /dev/null +++ b/shenandoah-jdk28/pom.xml @@ -0,0 +1,33 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 4.1.2 + + + com.ankurm + shenandoah-jdk28 + 1.0.0 + shenandoah-jdk28 + Same Spring Boot load on G1, generational ZGC and Shenandoah (SATB and generational) on JDK 28 early access + + 25 + + + + org.springframework.boot + spring-boot-starter-webmvc + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/shenandoah-jdk28/scripts/LoadGen.java b/shenandoah-jdk28/scripts/LoadGen.java new file mode 100644 index 0000000..0d2c98d --- /dev/null +++ b/shenandoah-jdk28/scripts/LoadGen.java @@ -0,0 +1,94 @@ +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.LockSupport; + +/** + * Load generator, run with the source launcher: java LoadGen.java open|closed URL RATE_OR_0 CONCURRENCY WARMUP_S MEASURE_S + * + * open : requests are SCHEDULED at a fixed rate and latency is measured from the scheduled time, so a server + * that stalls is charged for the requests that queued behind the stall (no coordinated omission). + * closed : CONCURRENCY clients send back to back; the result is the peak throughput. + * + * Prints one line: mode=... achieved=... p50_ms=... p99_ms=... p999_ms=... max_ms=... errors=... window_start_ms=... window_end_ms=... + * (the window is the measured interval in epoch milliseconds, so GC pauses can be cut to exactly that interval). + */ +public class LoadGen { + + public static void main(String[] a) throws Exception { + String mode = a[0]; + URI uri = URI.create(a[1]); + double rate = Double.parseDouble(a[2]); + int concurrency = Integer.parseInt(a[3]); + int warmup = Integer.parseInt(a[4]); + int measure = Integer.parseInt(a[5]); + boolean open = mode.equals("open"); + + HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.ofSeconds(5)).build(); + HttpRequest request = HttpRequest.newBuilder(uri).timeout(Duration.ofSeconds(30)).GET().build(); + + long start = System.nanoTime(); + long epochStartMs = System.currentTimeMillis(); + long measureFrom = start + warmup * 1_000_000_000L; + long end = measureFrom + measure * 1_000_000_000L; + int capacity = (int) (open ? rate * measure * 2 + 1000 : 2_000_000); + long[] latencies = new long[capacity]; + AtomicInteger count = new AtomicInteger(); + AtomicLong errors = new AtomicLong(); + AtomicLong next = new AtomicLong(); + long intervalNanos = open ? (long) (1e9 / rate) : 0; + + ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor(); + for (int t = 0; t < concurrency; t++) { + pool.submit(() -> { + while (true) { + long intended; + if (open) { + intended = start + next.getAndIncrement() * intervalNanos; + if (intended >= end) return; + long wait = intended - System.nanoTime(); + if (wait > 0) LockSupport.parkNanos(wait); + } else { + intended = System.nanoTime(); + if (intended >= end) return; + } + try { + HttpResponse r = client.send(request, HttpResponse.BodyHandlers.discarding()); + long done = System.nanoTime(); + if (r.statusCode() != 200) errors.incrementAndGet(); + if (intended >= measureFrom) { + int i = count.getAndIncrement(); + if (i < latencies.length) latencies[i] = done - intended; + } + } catch (Exception e) { + errors.incrementAndGet(); + } + } + }); + } + pool.shutdown(); + pool.awaitTermination(measure + warmup + 120, TimeUnit.SECONDS); + + int n = Math.min(count.get(), latencies.length); + long[] sorted = Arrays.copyOf(latencies, n); + Arrays.sort(sorted); + System.out.printf("mode=%s target=%.0f achieved=%.1f requests=%d p50_ms=%.2f p99_ms=%.2f p999_ms=%.2f max_ms=%.2f errors=%d window_start_ms=%d window_end_ms=%d%n", + mode, rate, n / (double) measure, n, ms(sorted, 0.50), ms(sorted, 0.99), ms(sorted, 0.999), + n == 0 ? 0 : sorted[n - 1] / 1e6, errors.get(), + epochStartMs + warmup * 1000L, epochStartMs + (warmup + measure) * 1000L); + } + + static double ms(long[] sorted, double q) { + if (sorted.length == 0) return 0; + return sorted[Math.min(sorted.length - 1, (int) Math.ceil(q * sorted.length) - 1)] / 1e6; + } +} diff --git a/shenandoah-jdk28/scripts/bench-one.sh b/shenandoah-jdk28/scripts/bench-one.sh new file mode 100755 index 0000000..3619cb2 --- /dev/null +++ b/shenandoah-jdk28/scripts/bench-one.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +# One benchmark run: start the server with the given JVM flags, warm up, measure at a fixed request rate +# (latency), then measure peak throughput, and print ONE result line. +# +# bench-one.sh