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; } }