summaryrefslogtreecommitdiff
path: root/src/main/java/bench/BenchRunner.java
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-01-09 23:26:31 +0200
committerPaul Buetow <paul@buetow.org>2026-01-09 23:26:31 +0200
commit15ea7f40cd7302b9bf9f0aea0d85a970a8a7c07f (patch)
tree897055fcb651cae1f5e96e14c966243325e95286 /src/main/java/bench/BenchRunner.java
Add Log4j2 benchmark tool
- Configurable thread count, duration, message size - Multiple logging configurations: sync-immediate, sync-buffered - AsyncLogger variants: 1k, 4k, 10k, 1m ring buffer sizes - AsyncAppender variants: 1k, 4k, 10k, 1m buffer sizes - Subprocess isolation for proper async logger initialization - Cache dropping between tests for accurate benchmarks - CSV output support
Diffstat (limited to 'src/main/java/bench/BenchRunner.java')
-rw-r--r--src/main/java/bench/BenchRunner.java153
1 files changed, 153 insertions, 0 deletions
diff --git a/src/main/java/bench/BenchRunner.java b/src/main/java/bench/BenchRunner.java
new file mode 100644
index 0000000..5312266
--- /dev/null
+++ b/src/main/java/bench/BenchRunner.java
@@ -0,0 +1,153 @@
+package bench;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configurator;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class BenchRunner {
+ private final BenchConfig config;
+
+ public BenchRunner(BenchConfig config) {
+ this.config = config;
+ }
+
+ public BenchResult run(String configName) throws Exception {
+ dropCaches();
+ reconfigure(configName);
+
+ Logger logger = LogManager.getLogger("bench");
+ String message = config.generateMessage();
+ int messageBytes = message.getBytes().length;
+
+ CountDownLatch startLatch = new CountDownLatch(1);
+ AtomicBoolean running = new AtomicBoolean(true);
+ AtomicLong eventCounter = new AtomicLong(0);
+ AtomicLong targetEvents = new AtomicLong(config.getTotalEvents());
+
+ List<Thread> threads = new ArrayList<>();
+ for (int i = 0; i < config.getThreads(); i++) {
+ Thread t = new Thread(new LogWorker(
+ logger, message, startLatch, running, eventCounter, targetEvents, config.getMode()
+ ));
+ t.start();
+ threads.add(t);
+ }
+
+ // Warmup phase
+ if (config.getWarmupSeconds() > 0) {
+ startLatch.countDown();
+ Thread.sleep(config.getWarmupSeconds() * 1000);
+ eventCounter.set(0);
+ } else {
+ startLatch.countDown();
+ }
+
+ long startTime = System.nanoTime();
+
+ if (config.getMode() == BenchConfig.Mode.DURATION) {
+ Thread.sleep(config.getDurationSeconds() * 1000);
+ running.set(false);
+ } else {
+ while (eventCounter.get() < targetEvents.get()) {
+ Thread.sleep(10);
+ }
+ running.set(false);
+ }
+
+ long endTime = System.nanoTime();
+
+ for (Thread t : threads) {
+ t.join(5000);
+ }
+
+ long events = eventCounter.get();
+ double durationSec = (endTime - startTime) / 1_000_000_000.0;
+ double eventsPerSec = events / durationSec;
+ double bytesPerSec = (events * messageBytes) / durationSec;
+ double mbPerSec = bytesPerSec / (1024 * 1024);
+
+ // Shutdown log4j context to flush and release resources
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ ctx.stop();
+
+ return new BenchResult(configName, config.getThreads(), events, durationSec, eventsPerSec, mbPerSec);
+ }
+
+ private void dropCaches() {
+ try {
+ ProcessBuilder pb = new ProcessBuilder("sh", "-c", "sync; echo 3 > /proc/sys/vm/drop_caches");
+ pb.inheritIO();
+ Process p = pb.start();
+ int exitCode = p.waitFor();
+ if (exitCode != 0) {
+ System.err.println("Warning: Failed to drop caches (exit code " + exitCode + "). Run as root for accurate benchmarks.");
+ }
+ } catch (Exception e) {
+ System.err.println("Warning: Could not drop caches: " + e.getMessage());
+ }
+ }
+
+ private void reconfigure(String configName) throws Exception {
+ // Fully shutdown existing context
+ LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
+ ctx.stop();
+
+ // Set async ring buffer size via system property (must be set before reconfigure)
+ if (configName.startsWith("async-")) {
+ String size = configName.substring(6); // e.g., "1k", "4k", "10k"
+ int bufferSize = switch (size) {
+ case "1k" -> 1024;
+ case "4k" -> 4096;
+ case "10k" -> 10240;
+ default -> 4096;
+ };
+ System.setProperty("log4j2.asyncLoggerRingBufferSize", String.valueOf(bufferSize));
+ System.setProperty("log4j2.contextSelector", "org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
+ } else {
+ System.clearProperty("log4j2.asyncLoggerRingBufferSize");
+ System.clearProperty("log4j2.contextSelector");
+ }
+
+ String resourcePath = "log4j2-" + configName + ".xml";
+ URI uri = getClass().getClassLoader().getResource(resourcePath).toURI();
+ Configurator.reconfigure(uri);
+ }
+
+ public static class BenchResult {
+ public final String configName;
+ public final int threads;
+ public final long events;
+ public final double durationSec;
+ public final double eventsPerSec;
+ public final double mbPerSec;
+
+ public BenchResult(String configName, int threads, long events,
+ double durationSec, double eventsPerSec, double mbPerSec) {
+ this.configName = configName;
+ this.threads = threads;
+ this.events = events;
+ this.durationSec = durationSec;
+ this.eventsPerSec = eventsPerSec;
+ this.mbPerSec = mbPerSec;
+ }
+
+ public String toCsv() {
+ return String.format("%s,%d,%d,%.2f,%.0f,%.2f",
+ configName, threads, events, durationSec, eventsPerSec, mbPerSec);
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%-16s | %3d threads | %,12d events | %.2fs | %,.0f events/s | %.2f MB/s",
+ configName, threads, events, durationSec, eventsPerSec, mbPerSec);
+ }
+ }
+}