blob: d42cfcf87bda2ae67bfccdb2a9b00fa728eb24bf [file] [log] [blame]
package org.apache.logging.log4j.layout.template.json;
import org.apache.logging.log4j.layout.template.json.util.JsonReader;
import org.apache.logging.log4j.util.Strings;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* Utility class to summarize {@link JsonTemplateLayoutBenchmark} results in Asciidoctor.
* <p>
* Usage:
* <pre>
* java \
* -cp log4j-perf/target/benchmarks.jar \
* org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutBenchmarkReport \
* log4j-perf/target/JsonTemplateLayoutBenchmarkResult.json \
* log4j-perf/target/JsonTemplateLayoutBenchmarkReport.adoc
* </pre>
* @see JsonTemplateLayoutBenchmark on how to generate JMH result JSON file
*/
public enum JsonTemplateLayoutBenchmarkReport {;
private static final Charset CHARSET = StandardCharsets.UTF_8;
public static void main(final String[] args) throws Exception {
final CliArgs cliArgs = CliArgs.parseArgs(args);
final JmhSetup jmhSetup = JmhSetup.ofJmhResult(cliArgs.jmhResultJsonFile);
final List<JmhSummary> jmhSummaries = JmhSummary.ofJmhResult(cliArgs.jmhResultJsonFile);
dumpReport(cliArgs.outputAdocFile, jmhSetup, jmhSummaries);
}
private static final class CliArgs {
private final File jmhResultJsonFile;
private final File outputAdocFile;
private CliArgs(final File jmhResultJsonFile, final File outputAdocFile) {
this.jmhResultJsonFile = jmhResultJsonFile;
this.outputAdocFile = outputAdocFile;
}
private static CliArgs parseArgs(final String[] args) {
// Check number of arguments.
if (args.length != 2) {
throw new IllegalArgumentException(
"usage: <jmhResultJsonFile> <outputAdocFile>");
}
// Parse the JMH result JSON file.
final File jmhResultJsonFile = new File(args[0]);
if (!jmhResultJsonFile.isFile()) {
throw new IllegalArgumentException(
"jmhResultJsonFile doesn't point to a regular file: " +
jmhResultJsonFile);
}
if (!jmhResultJsonFile.canRead()) {
throw new IllegalArgumentException(
"jmhResultJsonFile is not readable: " +
jmhResultJsonFile);
}
// Parse the output AsciiDoc file.
final File outputAdocFile = new File(args[1]);
touch(outputAdocFile);
// Looks okay.
return new CliArgs(jmhResultJsonFile, outputAdocFile);
}
public static void touch(final File file) {
Objects.requireNonNull(file, "file");
final Path path = file.toPath();
try {
if (Files.exists(path)) {
Files.setLastModifiedTime(path, FileTime.from(Instant.now()));
} else {
Files.createFile(path);
}
} catch (IOException error) {
throw new RuntimeException("failed to touch file: " + file, error);
}
}
}
private static final class JmhSetup {
private final String vmName;
private final String vmVersion;
private final List<String> vmArgs;
private final int forkCount;
private final int warmupIterationCount;
private final String warmupTime;
private final int measurementIterationCount;
private final String measurementTime;
private JmhSetup(
final String vmName,
final String vmVersion,
final List<String> vmArgs,
final int forkCount,
final int warmupIterationCount,
final String warmupTime,
final int measurementIterationCount,
final String measurementTime) {
this.vmName = vmName;
this.vmVersion = vmVersion;
this.vmArgs = vmArgs;
this.forkCount = forkCount;
this.warmupIterationCount = warmupIterationCount;
this.warmupTime = warmupTime;
this.measurementIterationCount = measurementIterationCount;
this.measurementTime = measurementTime;
}
private static JmhSetup ofJmhResult(final File jmhResultFile) throws IOException {
final List<Object> jmhResult = readObject(jmhResultFile);
return ofJmhResult(jmhResult);
}
private static JmhSetup ofJmhResult(final List<Object> jmhResult) {
final Object jmhResultEntry = jmhResult.stream().findFirst().get();
final String vmName = readObjectAtPath(jmhResultEntry, "vmName");
final String vmVersion = readObjectAtPath(jmhResultEntry, "vmVersion");
final List<String> vmArgs = readObjectAtPath(jmhResultEntry, "jvmArgs");
final int forkCount = readObjectAtPath(jmhResultEntry, "forks");
final int warmupIterationCount = readObjectAtPath(jmhResultEntry, "warmupIterations");
final String warmupTime = readObjectAtPath(jmhResultEntry, "warmupTime");
final int measurementIterationCount = readObjectAtPath(jmhResultEntry, "measurementIterations");
final String measurementTime = readObjectAtPath(jmhResultEntry, "measurementTime");
return new JmhSetup(
vmName,
vmVersion,
vmArgs,
forkCount,
warmupIterationCount,
warmupTime,
measurementIterationCount,
measurementTime);
}
}
private static final class JmhSummary {
private final String benchmark;
private final BigDecimal opRate;
private final BigDecimal gcRate;
private JmhSummary(
final String benchmark,
final BigDecimal opRate,
final BigDecimal gcRate) {
this.benchmark = benchmark;
this.opRate = opRate;
this.gcRate = gcRate;
}
private static List<JmhSummary> ofJmhResult(final File jmhResultFile) throws IOException {
final List<Object> jmhResult = readObject(jmhResultFile);
return ofJmhResult(jmhResult);
}
private static List<JmhSummary> ofJmhResult(final List<Object> jmhResult) {
final BigDecimal maxOpRate = jmhResult
.stream()
.map(jmhResultEntry -> readBigDecimalAtPath(jmhResultEntry, "primaryMetric", "scorePercentiles", "99.0"))
.max(BigDecimal::compareTo)
.get();
return jmhResult
.stream()
.map(jmhResultEntry -> {
final String benchmark = readObjectAtPath(jmhResultEntry, "benchmark");
final BigDecimal opRate = readBigDecimalAtPath(jmhResultEntry, "primaryMetric", "scorePercentiles", "99.0");
final BigDecimal gcRate = readBigDecimalAtPath(jmhResultEntry, "secondaryMetrics", "·gc.alloc.rate.norm", "scorePercentiles", "99.0");
return new JmhSummary(benchmark, opRate, gcRate);
})
.collect(Collectors.toList());
}
}
private static <V> V readObject(final File file) throws IOException {
final byte[] jsonBytes = Files.readAllBytes(file.toPath());
final String json = new String(jsonBytes, CHARSET);
@SuppressWarnings("unchecked")
final V object = (V) JsonReader.read(json);
return object;
}
private static <V> V readObjectAtPath(final Object object, String... path) {
Object lastObject = object;
for (final String key : path) {
@SuppressWarnings("unchecked")
Map<String, Object> lastMap = (Map<String, Object>) lastObject;
lastObject = lastMap.get(key);
}
@SuppressWarnings("unchecked")
final V typedLastObject = (V) lastObject;
return typedLastObject;
}
private static BigDecimal readBigDecimalAtPath(final Object object, String... path) {
final Number number = readObjectAtPath(object, path);
if (number instanceof BigDecimal) {
return (BigDecimal) number;
} else if (number instanceof Integer) {
final int intNumber = (int) number;
return BigDecimal.valueOf(intNumber);
} else if (number instanceof Long) {
final long longNumber = (long) number;
return BigDecimal.valueOf(longNumber);
} else if (number instanceof BigInteger) {
final BigInteger bigInteger = (BigInteger) number;
return new BigDecimal(bigInteger);
} else {
final String message = String.format(
"failed to convert the value to BigDecimal at path %s: %s",
Arrays.asList(path), number);
throw new IllegalArgumentException(message);
}
}
private static void dumpReport(
final File outputAdocFile,
final JmhSetup jmhSetup,
final List<JmhSummary> jmhSummaries) throws IOException {
try (final OutputStream outputStream = new FileOutputStream(outputAdocFile);
final PrintStream printStream = new PrintStream(outputStream, false, CHARSET.name())) {
dumpJmhSetup(printStream, jmhSetup);
dumpJmhSummaries(printStream, jmhSummaries, "lite");
dumpJmhSummaries(printStream, jmhSummaries, "full");
}
}
private static void dumpJmhSetup(
final PrintStream printStream,
final JmhSetup jmhSetup) {
printStream.println("[cols=\"1,4\", options=\"header\"]");
printStream.println(".JMH setup");
printStream.println("|===");
printStream.println("|Setting|Value");
printStream.format("|JVM name|%s%n", jmhSetup.vmName);
printStream.format("|JVM version|%s%n", jmhSetup.vmVersion);
printStream.format("|JVM args|%s%n", jmhSetup.vmArgs != null ? String.join(" ", jmhSetup.vmArgs) : "");
printStream.format("|Forks|%s%n", jmhSetup.forkCount);
printStream.format("|Warmup iterations|%d × %s%n", jmhSetup.warmupIterationCount, jmhSetup.warmupTime);
printStream.format("|Measurement iterations|%d × %s%n", jmhSetup.measurementIterationCount, jmhSetup.measurementTime);
printStream.println("|===");
}
private static void dumpJmhSummaries(
final PrintStream printStream,
final List<JmhSummary> jmhSummaries,
final String prefix) {
// Print header.
printStream.println("[cols=\"4,>2,4,>2\", options=\"header\"]");
printStream.format(".JMH result (99^th^ percentile) summary for \"%s\" log events%n", prefix);
printStream.println("|===");
printStream.println("^|Benchmark");
printStream.println("2+^|ops/sec");
printStream.println("^|B/op");
// Filter JMH summaries by prefix.
final String filterRegex = String.format("^.*\\.%s[A-Za-z0-9]+$", prefix);
final List<JmhSummary> filteredJmhSummaries = jmhSummaries
.stream()
.filter(jmhSummary -> jmhSummary.benchmark.matches(filterRegex))
.collect(Collectors.toList());
// Determine the max. op rate.
final BigDecimal maxOpRate = filteredJmhSummaries
.stream()
.map(jmhSummary -> jmhSummary.opRate)
.max(BigDecimal::compareTo)
.get();
// Print each summary.
final Comparator<JmhSummary> jmhSummaryComparator =
Comparator
.comparing((final JmhSummary jmhSummary) -> jmhSummary.opRate)
.reversed();
filteredJmhSummaries
.stream()
.sorted(jmhSummaryComparator)
.forEach((final JmhSummary jmhSummary) -> dumpJmhSummary(printStream, maxOpRate, jmhSummary));
// Print footer.
printStream.println("|===");
}
private static void dumpJmhSummary(
final PrintStream printStream,
final BigDecimal maxOpRate,
final JmhSummary jmhSummary) {
printStream.println();
final String benchmark = jmhSummary
.benchmark
.replaceAll("^.*\\.([^.]+)$", "$1");
printStream.format("|%s%n", benchmark);
final long opRatePerSec = jmhSummary
.opRate
.multiply(BigDecimal.valueOf(1_000L))
.toBigInteger()
.longValueExact();
printStream.format("|%,d%n", opRatePerSec);
final BigDecimal normalizedOpRate = jmhSummary
.opRate
.divide(maxOpRate, RoundingMode.CEILING);
final int opRateBarLength = normalizedOpRate
.multiply(BigDecimal.valueOf(19))
.toBigInteger()
.add(BigInteger.ONE)
.intValueExact();
final String opRateBar = Strings.repeat("▉", opRateBarLength);
final int opRatePercent = normalizedOpRate
.multiply(BigDecimal.valueOf(100))
.toBigInteger()
.intValueExact();
printStream.format("|%s (%d%%)%n", opRateBar, opRatePercent);
printStream.format("|%,.1f%n", jmhSummary.gcRate.doubleValue());
}
}