From 11e9e3d93c0a6d65ee598ec9f494fa5d4d2f9409 Mon Sep 17 00:00:00 2001 From: Francesco Nigro Date: Thu, 29 Oct 2026 09:36:43 +0100 Subject: [PATCH] Run the wrk/wrk2 load generator on a pinned JDK with a pause-free GC The jbang-launched wrk/wrk2@hyperfoil (hyperfoil-cli 0.30.0) ran on JAVA_HOME, i.e. on the JDK under test, with the JVM defaults: G1, 8 MB initial heap, 1/4 of RAM max. With 10K connections at 50K req/s wrk2 keeps ~1 GB of pre-allocated sessions live (1,050,000 SessionImpl + HttpRequest + per-session maps), and every young GC (every 8-25 s, 144-216 MB eden) pauses it for 60-110 ms. During the pause no requests are sent; afterwards wrk2 sends the missed requests as a burst and, with coordinated-omission correction, charges the stall to their latency. The reported p99/p99.9 was the load generator's GC, not the server's: the server-side request latency (kernel timestamps, arrival to response) had no burst while the server was idle during each wrk2 pause. Two new environment settings, documented next to JAVA_OPTS: LOAD_GEN_JAVA JDK for the load generator (jbang --java), default 25 LOAD_GEN_JAVA_OPTS its JVM options, default -XX:+UseShenandoahGC -Xms5g -Xmx5g applied to the warmup and the measured wrk/wrk2 launches through -R=. Pinning the JDK also stops the load generator from running on the experimental JDK under test, and makes the GC flag independent of that JDK (Oracle builds have no Shenandoah; the JDK 25 that jbang resolves is Temurin). Same server (4 carriers, 10K connections, 50K req/s, 30 ms mock), wrk2 numbers: load generator GC rps p99 p99.9 max wrk2 GC pauses in 60 s G1 default (JDK under test, 28) 48,528 55.3 - 163.6 4 x 67-108 ms Epsilon (-Xms4g -Xmx16g) 49,974 36.96 55.84 96.47 none ZGC (JDK 28, -Xmx16g) 49,959 381.7 - 530.6 none, but a 2.6 s major collection with allocation stalls Shenandoah (JDK 25, -Xms5g -Xmx5g) 49,975 36.18 88.60 147.85 16 x <= 0.2 ms Shenandoah generational (JDK 25) 47,984 34.87 75.50 173.02 21 x <= 0.2 ms, rate missed Shenandoah adds ~0.12 cores of GC threads on the load generator's CPUs (4 cycles per minute) and the loops keep their cost; the generational mode cost the loops ~10 % more CPU and missed the rate, so the non-generational mode is the default. Verified with -Xlog:gc* on both launches: "Version: 25.0.4.1+1-LTS", "Using Shenandoah", heap 5G. --- benchmark-runner/scripts/run-benchmark.sh | 28 ++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/benchmark-runner/scripts/run-benchmark.sh b/benchmark-runner/scripts/run-benchmark.sh index e7ec20c..d4d07d9 100755 --- a/benchmark-runner/scripts/run-benchmark.sh +++ b/benchmark-runner/scripts/run-benchmark.sh @@ -20,6 +20,13 @@ set -euo pipefail # Java configuration JAVA_HOME="${JAVA_HOME:-}" JAVA_OPTS="${JAVA_OPTS:--Xms1g -Xmx1g}" +# Load generator (jbang wrk/wrk2@hyperfoil) JVM: a pinned JDK (jbang --java, resolved or downloaded by jbang) +# and its JVM options. The default GC/heap keeps the load generator free of GC pauses: with the default G1 its +# ~1 GB of pre-allocated sessions (10K connections at 50K req/s) gives 60-110 ms young pauses every 8-25 s, and +# wrk2 charges each pause to the latency of the requests it delayed (coordinated-omission correction), so the +# measured p99/p99.9 was the load generator's GC, not the server's. +LOAD_GEN_JAVA="${LOAD_GEN_JAVA:-25}" +LOAD_GEN_JAVA_OPTS="${LOAD_GEN_JAVA_OPTS:--XX:+UseShenandoahGC -Xms5g -Xmx5g}" # Mock server configuration MOCK_PORT="${MOCK_PORT:-8080}" @@ -286,6 +293,19 @@ build_taskset_cmd() { fi } +# jbang arguments that pin the load generator's JDK and pass LOAD_GEN_JAVA_OPTS to its JVM (-R= each) +load_gen_jbang_args() { + local args=() + if [[ -n "$LOAD_GEN_JAVA" ]]; then + args+=(--java "$LOAD_GEN_JAVA") + fi + local opt + for opt in $LOAD_GEN_JAVA_OPTS; do + args+=("-R=$opt") + done + echo "${args[@]}" +} + cleanup() { log "Cleaning up..." @@ -474,7 +494,7 @@ run_warmup() { local taskset_cmd=$(build_taskset_cmd "$LOAD_GEN_CPUSET") # Use wrk for warmup (no rate limiting) - $taskset_cmd jbang wrk@hyperfoil \ + $taskset_cmd jbang $(load_gen_jbang_args) wrk@hyperfoil \ -t "$LOAD_GEN_THREADS" \ -c "$LOAD_GEN_CONNECTIONS" \ -d "$WARMUP_DURATION" \ @@ -845,7 +865,7 @@ run_load_test() { # Use wrk2 with rate limiting log "Using wrk2 with rate: $LOAD_GEN_RATE req/s" - $taskset_cmd jbang wrk2@hyperfoil \ + $taskset_cmd jbang $(load_gen_jbang_args) wrk2@hyperfoil \ -t "$LOAD_GEN_THREADS" \ -c "$LOAD_GEN_CONNECTIONS" \ -d "${test_secs}s" \ @@ -856,7 +876,7 @@ run_load_test() { # Use wrk for max throughput log "Using wrk for max throughput" - $taskset_cmd jbang wrk@hyperfoil \ + $taskset_cmd jbang $(load_gen_jbang_args) wrk@hyperfoil \ -t "$LOAD_GEN_THREADS" \ -c "$LOAD_GEN_CONNECTIONS" \ -d "${test_secs}s" \ @@ -1089,6 +1109,8 @@ Output: Environment-only settings: JAVA_HOME Path to Java installation (required) JAVA_OPTS JVM options (default: -Xms1g -Xmx1g) + LOAD_GEN_JAVA JDK for the jbang load generator, wrk/wrk2@hyperfoil (jbang --java; default: 25) + LOAD_GEN_JAVA_OPTS JVM options of the load generator (default: -XX:+UseShenandoahGC -Xms5g -Xmx5g) PROFILING_DELAY_SECONDS Profiling start delay (default: 10) PROFILING_DURATION_SECONDS Profiling duration (default: 10)