summaryrefslogtreecommitdiff
path: root/src/main/java/bench/Main.java
blob: 565ae135b66cb9556984e6303b865334cb7a19a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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: available CPUs)")
    private int threads = Runtime.getRuntime().availableProcessors();

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

    @Option(names = {"--drop-caches"}, description = "Drop OS file caches before each test (requires sudo, default: false)")
    private boolean dropCaches = 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 if requested
                if (dropCaches) {
                    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());
        }
    }
}