summaryrefslogtreecommitdiff
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
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
-rw-r--r--.gitignore8
-rw-r--r--README.md119
-rw-r--r--pom.xml85
-rw-r--r--src/main/java/bench/BenchConfig.java48
-rw-r--r--src/main/java/bench/BenchRunner.java153
-rw-r--r--src/main/java/bench/LogWorker.java51
-rw-r--r--src/main/java/bench/Main.java185
-rw-r--r--src/main/java/bench/SingleBench.java84
-rw-r--r--src/main/resources/log4j2-async-10k.xml19
-rw-r--r--src/main/resources/log4j2-async-1k.xml19
-rw-r--r--src/main/resources/log4j2-async-1m.xml16
-rw-r--r--src/main/resources/log4j2-async-4k.xml19
-rw-r--r--src/main/resources/log4j2-asyncapp-10k.xml16
-rw-r--r--src/main/resources/log4j2-asyncapp-1k.xml16
-rw-r--r--src/main/resources/log4j2-asyncapp-1m.xml16
-rw-r--r--src/main/resources/log4j2-asyncapp-4k.xml16
-rw-r--r--src/main/resources/log4j2-sync-buffered.xml13
-rw-r--r--src/main/resources/log4j2-sync-immediate.xml13
18 files changed, 896 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..50c680b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+target/
+*.log
+*.class
+*.jar
+.idea/
+*.iml
+.DS_Store
+dependency-reduced-pom.xml
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0424e00
--- /dev/null
+++ b/README.md
@@ -0,0 +1,119 @@
+# Log4j2 Benchmark Tool
+
+A minimal Java tool to benchmark Log4j2 logging throughput with configurable concurrent threads and various logging configurations.
+
+## Features
+
+- Configurable number of concurrent logging threads
+- Duration-based or event-count-based test modes
+- Configurable message size (~100 chars default)
+- Multiple Log4j2 configurations to compare:
+ - `sync-immediate` - Synchronous with immediateFlush=true
+ - `sync-buffered` - Synchronous with immediateFlush=false (8KB buffer)
+ - `async-1k` - AsyncLogger with 1024 ring buffer size
+ - `async-4k` - AsyncLogger with 4096 ring buffer size
+ - `async-10k` - AsyncLogger with 10240 ring buffer size
+- CSV output support for further analysis
+
+## Requirements
+
+- Java 17+
+- Maven 3.6+
+
+## Build
+
+```bash
+mvn clean package
+```
+
+This creates an executable fat JAR at `target/log4jbench-1.0-SNAPSHOT.jar` (~2.7MB).
+
+## Usage
+
+### Basic Run (all configs, 100 threads, 10s duration)
+
+```bash
+# With Maven
+mvn exec:java
+
+# With standalone JAR
+java -jar target/log4jbench-1.0-SNAPSHOT.jar
+```
+
+### With Custom Options
+
+```bash
+java -jar target/log4jbench-1.0-SNAPSHOT.jar --threads=8 --duration=30 --warmup=5
+```
+
+### Event-Count Mode
+
+```bash
+java -jar target/log4jbench-1.0-SNAPSHOT.jar --mode=events --events=500000 --threads=2
+```
+
+### Test Specific Configs Only
+
+```bash
+java -jar target/log4jbench-1.0-SNAPSHOT.jar --configs=sync-immediate,async-4k
+```
+
+### Custom Message Size
+
+```bash
+java -jar target/log4jbench-1.0-SNAPSHOT.jar --msg-size=200 --threads=4
+```
+
+### Export to CSV
+
+```bash
+java -jar target/log4jbench-1.0-SNAPSHOT.jar --output=results.csv
+```
+
+## Command Line Options
+
+| Option | Description | Default |
+|--------|-------------|---------|
+| `-t, --threads=N` | Number of concurrent threads | 100 |
+| `-m, --mode=MODE` | Test mode: `duration` or `events` | duration |
+| `-d, --duration=N` | Test duration in seconds | 10 |
+| `-e, --events=N` | Total events (events mode) | 1000000 |
+| `-w, --warmup=N` | Warmup duration in seconds | 3 |
+| `-s, --msg-size=N` | Message size in characters | 100 |
+| `-c, --configs=LIST` | Comma-separated config names | all |
+| `-o, --output=FILE` | Output CSV file | stdout only |
+
+## Example Output
+
+```
+=== Log4j2 Benchmark ===
+Threads: 4 | Mode: DURATION | Message size: 100 chars
+Duration: 10s | Warmup: 3s
+Configs: [sync-immediate, sync-buffered, async-1k, async-4k, async-10k]
+
+Running: sync-immediate ...
+sync-immediate | 4 threads | 1,234,567 events | 10.00s | 123,456 events/s | 12.34 MB/s
+
+Running: async-4k ...
+async-4k | 4 threads | 5,678,901 events | 10.00s | 567,890 events/s | 56.78 MB/s
+```
+
+## Log Configurations
+
+### sync-immediate
+Standard synchronous file appender with `immediateFlush="true"`. Every log event is flushed to disk immediately.
+
+### sync-buffered
+Synchronous file appender with `immediateFlush="false"` and 8KB buffer. Events are batched before writing.
+
+### async-1k / async-4k / async-10k
+Async loggers using LMAX Disruptor with ring buffer sizes of 1024, 4096, and 10240 respectively. Logging threads hand off events to a background thread for I/O.
+
+## Notes
+
+- **Cache dropping**: Before each test, the tool attempts to drop Linux filesystem caches (`sync; echo 3 > /proc/sys/vm/drop_caches`) for consistent results. Run with `sudo` for this to work.
+- **Async loggers**: Use the LMAX Disruptor for high-throughput async logging.
+
+## License
+
+MIT
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..ce7b4a7
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <groupId>com.example</groupId>
+ <artifactId>log4jbench</artifactId>
+ <version>1.0-SNAPSHOT</version>
+ <packaging>jar</packaging>
+
+ <properties>
+ <maven.compiler.source>17</maven.compiler.source>
+ <maven.compiler.target>17</maven.compiler.target>
+ <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+ <log4j.version>2.23.1</log4j.version>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ <version>${log4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ <version>${log4j.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.lmax</groupId>
+ <artifactId>disruptor</artifactId>
+ <version>3.4.4</version>
+ </dependency>
+ <dependency>
+ <groupId>info.picocli</groupId>
+ <artifactId>picocli</artifactId>
+ <version>4.7.5</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <version>3.1.0</version>
+ <configuration>
+ <mainClass>bench.Main</mainClass>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>3.5.1</version>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <transformers>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+ <mainClass>bench.Main</mainClass>
+ </transformer>
+ <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+ </transformers>
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*.SF</exclude>
+ <exclude>META-INF/*.DSA</exclude>
+ <exclude>META-INF/*.RSA</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/src/main/java/bench/BenchConfig.java b/src/main/java/bench/BenchConfig.java
new file mode 100644
index 0000000..cc73444
--- /dev/null
+++ b/src/main/java/bench/BenchConfig.java
@@ -0,0 +1,48 @@
+package bench;
+
+import java.util.List;
+
+public class BenchConfig {
+ public enum Mode { DURATION, EVENTS }
+
+ private int threads = 10;
+ private Mode mode = Mode.DURATION;
+ private long durationSeconds = 10;
+ private long totalEvents = 1_000_000;
+ private long warmupSeconds = 3;
+ private int messageSize = 100;
+ private List<String> configs = List.of(
+ "sync-immediate", "sync-buffered",
+ "async-1k", "async-4k", "async-10k",
+ "asyncapp-1k", "asyncapp-4k", "asyncapp-10k"
+ );
+ private String outputFile = null;
+
+ public int getThreads() { return threads; }
+ public void setThreads(int threads) { this.threads = threads; }
+
+ public Mode getMode() { return mode; }
+ public void setMode(Mode mode) { this.mode = mode; }
+
+ public long getDurationSeconds() { return durationSeconds; }
+ public void setDurationSeconds(long durationSeconds) { this.durationSeconds = durationSeconds; }
+
+ public long getTotalEvents() { return totalEvents; }
+ public void setTotalEvents(long totalEvents) { this.totalEvents = totalEvents; }
+
+ public long getWarmupSeconds() { return warmupSeconds; }
+ public void setWarmupSeconds(long warmupSeconds) { this.warmupSeconds = warmupSeconds; }
+
+ public int getMessageSize() { return messageSize; }
+ public void setMessageSize(int messageSize) { this.messageSize = messageSize; }
+
+ public List<String> getConfigs() { return configs; }
+ public void setConfigs(List<String> configs) { this.configs = configs; }
+
+ public String getOutputFile() { return outputFile; }
+ public void setOutputFile(String outputFile) { this.outputFile = outputFile; }
+
+ public String generateMessage() {
+ return "X".repeat(messageSize);
+ }
+}
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);
+ }
+ }
+}
diff --git a/src/main/java/bench/LogWorker.java b/src/main/java/bench/LogWorker.java
new file mode 100644
index 0000000..5a9edfc
--- /dev/null
+++ b/src/main/java/bench/LogWorker.java
@@ -0,0 +1,51 @@
+package bench;
+
+import org.apache.logging.log4j.Logger;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+
+public class LogWorker implements Runnable {
+ private final Logger logger;
+ private final String message;
+ private final CountDownLatch startLatch;
+ private final AtomicBoolean running;
+ private final AtomicLong eventCounter;
+ private final AtomicLong targetEvents;
+ private final BenchConfig.Mode mode;
+
+ public LogWorker(Logger logger, String message, CountDownLatch startLatch,
+ AtomicBoolean running, AtomicLong eventCounter,
+ AtomicLong targetEvents, BenchConfig.Mode mode) {
+ this.logger = logger;
+ this.message = message;
+ this.startLatch = startLatch;
+ this.running = running;
+ this.eventCounter = eventCounter;
+ this.targetEvents = targetEvents;
+ this.mode = mode;
+ }
+
+ @Override
+ public void run() {
+ try {
+ startLatch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+
+ if (mode == BenchConfig.Mode.DURATION) {
+ while (running.get()) {
+ logger.info(message);
+ eventCounter.incrementAndGet();
+ }
+ } else {
+ while (running.get() && eventCounter.get() < targetEvents.get()) {
+ logger.info(message);
+ eventCounter.incrementAndGet();
+ }
+ }
+ }
+}
diff --git a/src/main/java/bench/Main.java b/src/main/java/bench/Main.java
new file mode 100644
index 0000000..0ce0f3c
--- /dev/null
+++ b/src/main/java/bench/Main.java
@@ -0,0 +1,185 @@
+package bench;
+
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+import picocli.CommandLine.Option;
+
+import java.io.FileWriter;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+@Command(name = "log4jbench", mixinStandardHelpOptions = true, version = "1.0",
+ description = "Benchmark Log4j2 logging throughput with various configurations")
+public class Main implements Callable<Integer> {
+
+ @Option(names = {"-t", "--threads"}, description = "Number of concurrent threads (default: 10)")
+ private int threads = 10;
+
+ @Option(names = {"-m", "--mode"}, description = "Test mode: duration or events (default: duration)")
+ private String mode = "duration";
+
+ @Option(names = {"-d", "--duration"}, description = "Test duration in seconds (default: 10)")
+ private long duration = 10;
+
+ @Option(names = {"-e", "--events"}, description = "Total events to log in events mode (default: 1000000)")
+ private long events = 1_000_000;
+
+ @Option(names = {"-w", "--warmup"}, description = "Warmup duration in seconds (default: 3)")
+ private long warmup = 3;
+
+ @Option(names = {"-s", "--msg-size"}, description = "Message size in characters (default: 100)")
+ private int msgSize = 100;
+
+ @Option(names = {"-c", "--configs"}, description = "Comma-separated list of configs to test (default: all)")
+ private String configs = null;
+
+ @Option(names = {"-o", "--output"}, description = "Output CSV file (default: stdout only)")
+ private String outputFile = null;
+
+ @Option(names = {"--flush"}, description = "Call LogManager.shutdown() to flush at end of each test (default: false)")
+ private boolean flush = false;
+
+ public static void main(String[] args) {
+ // Must set context selector before ANY Log4j initialization
+ // We'll set it dynamically per-config in BenchRunner instead
+ int exitCode = new CommandLine(new Main()).execute(args);
+ System.exit(exitCode);
+ }
+
+ @Override
+ public Integer call() throws Exception {
+ BenchConfig config = new BenchConfig();
+ config.setThreads(threads);
+ config.setMode("events".equalsIgnoreCase(mode) ? BenchConfig.Mode.EVENTS : BenchConfig.Mode.DURATION);
+ config.setDurationSeconds(duration);
+ config.setTotalEvents(events);
+ config.setWarmupSeconds(warmup);
+ config.setMessageSize(msgSize);
+ config.setOutputFile(outputFile);
+
+ if (configs != null && !configs.isBlank()) {
+ config.setConfigs(Arrays.asList(configs.split(",")));
+ }
+
+ System.out.println("=== Log4j2 Benchmark ===");
+ System.out.printf("Threads: %d | Mode: %s | Message size: %d chars | flush: %s%n",
+ config.getThreads(), config.getMode(), config.getMessageSize(), flush);
+ if (config.getMode() == BenchConfig.Mode.DURATION) {
+ System.out.printf("Duration: %ds | Warmup: %ds%n", config.getDurationSeconds(), config.getWarmupSeconds());
+ } else {
+ System.out.printf("Target events: %,d | Warmup: %ds%n", config.getTotalEvents(), config.getWarmupSeconds());
+ }
+ System.out.printf("Configs: %s%n", config.getConfigs());
+ System.out.println();
+
+ PrintWriter csvWriter = null;
+ if (outputFile != null) {
+ csvWriter = new PrintWriter(new FileWriter(outputFile));
+ csvWriter.println("config,threads,events,duration_sec,events_per_sec,mb_per_sec");
+ }
+
+ String jarPath = Main.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath();
+ String javaHome = System.getProperty("java.home");
+ String javaBin = javaHome + "/bin/java";
+
+ for (String configName : config.getConfigs()) {
+ System.out.printf("Running: %s ...%n", configName);
+ try {
+ // Drop caches before each test
+ dropCaches();
+
+ // Build command with appropriate system properties
+ List<String> cmd = new ArrayList<>();
+ cmd.add(javaBin);
+
+ // Set async properties if needed
+ if (configName.startsWith("async-")) {
+ String size = configName.substring(6);
+ int bufferSize = switch (size) {
+ case "1k" -> 1024;
+ case "4k" -> 4096;
+ case "10k" -> 10240;
+ case "1m" -> 1048576;
+ default -> 4096;
+ };
+ cmd.add("-Dlog4j2.contextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector");
+ cmd.add("-Dlog4j2.asyncLoggerRingBufferSize=" + bufferSize);
+ }
+
+ // Set log4j config file (use classpath reference)
+ cmd.add("-Dlog4j2.configurationFile=classpath:log4j2-" + configName + ".xml");
+
+ // Pass flush setting
+ cmd.add("-Dbench.flush=" + flush);
+
+ cmd.add("-cp");
+ cmd.add(jarPath);
+ cmd.add("bench.SingleBench");
+ cmd.add(configName);
+ cmd.add(String.valueOf(config.getThreads()));
+ cmd.add(String.valueOf(config.getDurationSeconds()));
+ cmd.add(String.valueOf(config.getWarmupSeconds()));
+ cmd.add(String.valueOf(config.getMessageSize()));
+
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+ pb.redirectErrorStream(true);
+ Process p = pb.start();
+
+ long pid = p.pid();
+ System.out.printf(" [PID: %d]%n", pid);
+
+ String output = new String(p.getInputStream().readAllBytes());
+ int exitCode = p.waitFor();
+
+ if (exitCode == 0 && !output.isBlank()) {
+ String[] parts = output.trim().split(",");
+ if (parts.length >= 6) {
+ String resultLine = String.format("%-16s | %3d threads | %,12d events | %.2fs | %,.0f events/s | %.2f MB/s",
+ parts[0], Integer.parseInt(parts[1]), Long.parseLong(parts[2]),
+ Double.parseDouble(parts[3]), Double.parseDouble(parts[4]), Double.parseDouble(parts[5]));
+ System.out.println(resultLine);
+ if (csvWriter != null) {
+ csvWriter.println(output.trim());
+ csvWriter.flush();
+ }
+ } else {
+ System.out.println(output);
+ }
+ } else {
+ System.err.printf("Error (exit %d): %s%n", exitCode, output);
+ }
+ } catch (Exception e) {
+ System.err.printf("Error running config %s: %s%n", configName, e.getMessage());
+ e.printStackTrace();
+ }
+ System.out.println();
+ }
+
+ if (csvWriter != null) {
+ csvWriter.close();
+ System.out.printf("Results written to: %s%n", outputFile);
+ }
+
+ return 0;
+ }
+
+ private void dropCaches() {
+ try {
+ System.out.print(" Dropping caches... ");
+ System.out.flush();
+ ProcessBuilder pb = new ProcessBuilder("sh", "-c", "sync; echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null");
+ Process p = pb.start();
+ int exitCode = p.waitFor();
+ if (exitCode == 0) {
+ System.out.println("done");
+ } else {
+ System.out.println("failed (exit code " + exitCode + "). Ensure sudo is available.");
+ }
+ } catch (Exception e) {
+ System.out.println("failed: " + e.getMessage());
+ }
+ }
+}
diff --git a/src/main/java/bench/SingleBench.java b/src/main/java/bench/SingleBench.java
new file mode 100644
index 0000000..2ce39eb
--- /dev/null
+++ b/src/main/java/bench/SingleBench.java
@@ -0,0 +1,84 @@
+package bench;
+
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+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 SingleBench {
+
+ public static void main(String[] args) throws Exception {
+ if (args.length < 5) {
+ System.err.println("Usage: SingleBench <configName> <threads> <durationSec> <warmupSec> <msgSize>");
+ System.exit(1);
+ }
+
+ String configName = args[0];
+ int threadCount = Integer.parseInt(args[1]);
+ long durationSec = Long.parseLong(args[2]);
+ long warmupSec = Long.parseLong(args[3]);
+ int msgSize = Integer.parseInt(args[4]);
+
+ String message = "X".repeat(msgSize);
+ int messageBytes = message.getBytes().length;
+
+ Logger logger = LogManager.getLogger("bench");
+
+ CountDownLatch startLatch = new CountDownLatch(1);
+ AtomicBoolean running = new AtomicBoolean(true);
+ AtomicLong eventCounter = new AtomicLong(0);
+
+ List<Thread> threads = new ArrayList<>();
+ for (int i = 0; i < threadCount; i++) {
+ Thread t = new Thread(() -> {
+ try {
+ startLatch.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ return;
+ }
+ while (running.get()) {
+ logger.info(message);
+ eventCounter.incrementAndGet();
+ }
+ });
+ t.start();
+ threads.add(t);
+ }
+
+ // Warmup phase
+ startLatch.countDown();
+ if (warmupSec > 0) {
+ Thread.sleep(warmupSec * 1000);
+ eventCounter.set(0);
+ }
+
+ long startTime = System.nanoTime();
+ Thread.sleep(durationSec * 1000);
+ running.set(false);
+ long endTime = System.nanoTime();
+
+ for (Thread t : threads) {
+ t.join(5000);
+ }
+
+ long events = eventCounter.get();
+ double duration = (endTime - startTime) / 1_000_000_000.0;
+ double eventsPerSec = events / duration;
+ double mbPerSec = (events * messageBytes) / duration / (1024 * 1024);
+
+ // Output CSV line: config,threads,events,duration,events_per_sec,mb_per_sec
+ System.out.printf("%s,%d,%d,%.2f,%.0f,%.2f%n",
+ configName, threadCount, events, duration, eventsPerSec, mbPerSec);
+
+ // Only flush/shutdown if requested (default: no flush for max perf)
+ boolean doFlush = Boolean.getBoolean("bench.flush");
+ if (doFlush) {
+ LogManager.shutdown();
+ }
+ }
+}
diff --git a/src/main/resources/log4j2-async-10k.xml b/src/main/resources/log4j2-async-10k.xml
new file mode 100644
index 0000000..b2d26a9
--- /dev/null
+++ b/src/main/resources/log4j2-async-10k.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="false" bufferSize="8192">
+ <PatternLayout pattern="[ASYNC-10K] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ </Appenders>
+ <Loggers>
+ <AsyncLogger name="bench" level="info" includeLocation="false">
+ <AppenderRef ref="File"/>
+ </AsyncLogger>
+ <Root level="info" includeLocation="false">
+ <AppenderRef ref="File"/>
+ </Root>
+ </Loggers>
+ <Properties>
+ <Property name="log4j2.asyncLoggerRingBufferSize">10240</Property>
+ </Properties>
+</Configuration>
diff --git a/src/main/resources/log4j2-async-1k.xml b/src/main/resources/log4j2-async-1k.xml
new file mode 100644
index 0000000..66389fe
--- /dev/null
+++ b/src/main/resources/log4j2-async-1k.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="false" bufferSize="8192">
+ <PatternLayout pattern="[ASYNC-1K] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ </Appenders>
+ <Loggers>
+ <AsyncLogger name="bench" level="info" includeLocation="false">
+ <AppenderRef ref="File"/>
+ </AsyncLogger>
+ <Root level="info" includeLocation="false">
+ <AppenderRef ref="File"/>
+ </Root>
+ </Loggers>
+ <Properties>
+ <Property name="log4j2.asyncLoggerRingBufferSize">1024</Property>
+ </Properties>
+</Configuration>
diff --git a/src/main/resources/log4j2-async-1m.xml b/src/main/resources/log4j2-async-1m.xml
new file mode 100644
index 0000000..596d3d5
--- /dev/null
+++ b/src/main/resources/log4j2-async-1m.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="true">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ </Appenders>
+ <Loggers>
+ <AsyncLogger name="bench" level="info" includeLocation="false">
+ <AppenderRef ref="File"/>
+ </AsyncLogger>
+ <Root level="info" includeLocation="false">
+ <AppenderRef ref="File"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/src/main/resources/log4j2-async-4k.xml b/src/main/resources/log4j2-async-4k.xml
new file mode 100644
index 0000000..9e51895
--- /dev/null
+++ b/src/main/resources/log4j2-async-4k.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="false" bufferSize="8192">
+ <PatternLayout pattern="[ASYNC-4K] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ </Appenders>
+ <Loggers>
+ <AsyncLogger name="bench" level="info" includeLocation="false">
+ <AppenderRef ref="File"/>
+ </AsyncLogger>
+ <Root level="info" includeLocation="false">
+ <AppenderRef ref="File"/>
+ </Root>
+ </Loggers>
+ <Properties>
+ <Property name="log4j2.asyncLoggerRingBufferSize">4096</Property>
+ </Properties>
+</Configuration>
diff --git a/src/main/resources/log4j2-asyncapp-10k.xml b/src/main/resources/log4j2-asyncapp-10k.xml
new file mode 100644
index 0000000..40db21e
--- /dev/null
+++ b/src/main/resources/log4j2-asyncapp-10k.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="true">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ <Async name="Async" bufferSize="10240">
+ <AppenderRef ref="File"/>
+ </Async>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="Async"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/src/main/resources/log4j2-asyncapp-1k.xml b/src/main/resources/log4j2-asyncapp-1k.xml
new file mode 100644
index 0000000..ad43e23
--- /dev/null
+++ b/src/main/resources/log4j2-asyncapp-1k.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="true">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ <Async name="Async" bufferSize="1024">
+ <AppenderRef ref="File"/>
+ </Async>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="Async"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/src/main/resources/log4j2-asyncapp-1m.xml b/src/main/resources/log4j2-asyncapp-1m.xml
new file mode 100644
index 0000000..7b5e1bb
--- /dev/null
+++ b/src/main/resources/log4j2-asyncapp-1m.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="true">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ <Async name="Async" bufferSize="1048576">
+ <AppenderRef ref="File"/>
+ </Async>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="Async"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/src/main/resources/log4j2-asyncapp-4k.xml b/src/main/resources/log4j2-asyncapp-4k.xml
new file mode 100644
index 0000000..af4a2f2
--- /dev/null
+++ b/src/main/resources/log4j2-asyncapp-4k.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="true">
+ <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ <Async name="Async" bufferSize="4096">
+ <AppenderRef ref="File"/>
+ </Async>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="Async"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/src/main/resources/log4j2-sync-buffered.xml b/src/main/resources/log4j2-sync-buffered.xml
new file mode 100644
index 0000000..ed143ab
--- /dev/null
+++ b/src/main/resources/log4j2-sync-buffered.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="false" bufferSize="8192">
+ <PatternLayout pattern="[SYNC-BUF] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="File"/>
+ </Root>
+ </Loggers>
+</Configuration>
diff --git a/src/main/resources/log4j2-sync-immediate.xml b/src/main/resources/log4j2-sync-immediate.xml
new file mode 100644
index 0000000..25d5303
--- /dev/null
+++ b/src/main/resources/log4j2-sync-immediate.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Configuration status="WARN">
+ <Appenders>
+ <File name="File" fileName="target/bench.log" immediateFlush="true">
+ <PatternLayout pattern="[SYNC-IMM] %d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
+ </File>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="File"/>
+ </Root>
+ </Loggers>
+</Configuration>