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