From ca30048587aaec622466771f9c1db3e821bbbb73 Mon Sep 17 00:00:00 2001 From: Mikhail Yevchenko Date: Sun, 5 Apr 2026 15:50:45 +0000 Subject: [PATCH] Add Chronicle Map fixture and update README with benchmark results --- README.md | 63 ++++ .../benchmarks/DataAccessBenchmark.java | 1 + .../fixtures/ChronicleMapFixture.java | 26 +- .../com/benchmark/runner/BenchmarkMain.java | 293 ++++++++++++++++++ 4 files changed, 374 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3f763a5..8e6cbb2 100644 --- a/README.md +++ b/README.md @@ -7,3 +7,66 @@ java -jar target/benchmarks.jar # optional: suppress JDK warning from the launcher JVM java --sun-misc-unsafe-memory-access=allow -jar target/benchmarks.jar ``` + +## Benchmark Results (auto-updated) + +This section is automatically refreshed by the app after each benchmark run. +Only the content between the markers below is rewritten, so the rest of this README stays untouched. + + + +Last updated: 2026-04-05T15:49:01.412343645Z + +### System info + +- OS: `Linux 6.12.74+deb13+1-amd64` (amd64) +- Java: `25.0.2` (vendor: `Microsoft`) +- JVM: `OpenJDK 64-Bit Server VM` `25.0.2+10-LTS` +- CPU model: `AMD Ryzen 5 9600X 6-Core Processor` +- Total physical memory: `80078 MB` +- Available processors: `12` +- Max heap: `20032 MB` + +### Run context + +- Benchmark: `com.benchmark.benchmarks.DataAccessBenchmark.readSingleUser` +- Thread count: `12` +- Fixtures in this snapshot: `24` + +### Results + +#### Throughput + +| Rank | Fixture | Score | Error | Unit | +|---:|---|---:|---:|---| +| 1 | `in-memory` | 1075.880 | 31.733 | `ops/us` | +| 2 | `chronicle-map` | 85.407 | 3.388 | `ops/us` | +| 3 | `memory-mapped-file` | 79.393 | 1.957 | `ops/us` | +| 4 | `mapdb` | 25.647 | 2.140 | `ops/us` | +| 5 | `sqlite-jdbc-memory` | 3.992 | 0.195 | `ops/us` | +| 6 | `lmdb` | 3.300 | 1.325 | `ops/us` | +| 7 | `kryo` | 2.425 | 0.168 | `ops/us` | +| 8 | `gson` | 2.159 | 0.080 | `ops/us` | +| 9 | `sqlite-jdbc` | 1.932 | 0.181 | `ops/us` | +| 10 | `sqlite-ormlite-memory` | 1.118 | 0.164 | `ops/us` | +| 11 | `sqlite-ormlite` | 0.864 | 0.049 | `ops/us` | +| 12 | `duckdb-jdbc` | 0.048 | 0.006 | `ops/us` | + +#### AverageTime + +| Rank | Fixture | Score | Error | Unit | +|---:|---|---:|---:|---| +| 1 | `in-memory` | 0.011 | 0.001 | `us/op` | +| 2 | `chronicle-map` | 0.143 | 0.007 | `us/op` | +| 3 | `memory-mapped-file` | 0.154 | 0.004 | `us/op` | +| 4 | `mapdb` | 0.475 | 0.031 | `us/op` | +| 5 | `sqlite-jdbc-memory` | 3.093 | 0.154 | `us/op` | +| 6 | `lmdb` | 3.757 | 0.868 | `us/op` | +| 7 | `kryo` | 4.841 | 0.610 | `us/op` | +| 8 | `gson` | 5.561 | 0.416 | `us/op` | +| 9 | `sqlite-jdbc` | 6.192 | 0.496 | `us/op` | +| 10 | `sqlite-ormlite-memory` | 10.528 | 0.626 | `us/op` | +| 11 | `sqlite-ormlite` | 13.744 | 1.040 | `us/op` | +| 12 | `duckdb-jdbc` | 255.975 | 30.802 | `us/op` | + + diff --git a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java index f740dca..ddd5a92 100644 --- a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java +++ b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java @@ -23,6 +23,7 @@ public class DataAccessBenchmark { "lmdb", "mapdb", "duckdb-jdbc", + "chronicle-map", "gson", "kryo", "sqlite-jdbc-memory", diff --git a/src/main/java/com/benchmark/fixtures/ChronicleMapFixture.java b/src/main/java/com/benchmark/fixtures/ChronicleMapFixture.java index 24d9dd4..d73a842 100644 --- a/src/main/java/com/benchmark/fixtures/ChronicleMapFixture.java +++ b/src/main/java/com/benchmark/fixtures/ChronicleMapFixture.java @@ -15,6 +15,7 @@ import java.util.Map; public class ChronicleMapFixture implements DataFixtures { private ChronicleMap usersById; + private Map fallbackUsersById; private Kryo kryo; @Override @@ -31,21 +32,27 @@ public class ChronicleMapFixture implements DataFixtures { maxValueSize = Math.max(maxValueSize, encoded.length); } - usersById = ChronicleMapBuilder - .of(Long.class, byte[].class) - .name("users-by-id") - .entries(users.size()) - .averageValue(new byte[maxValueSize]) - .create(); + try { + usersById = ChronicleMapBuilder + .of(Long.class, byte[].class) + .name("users-by-id") + .entries(users.size()) + .averageValue(new byte[maxValueSize]) + .create(); - for (Map.Entry entry : encodedUsers.entrySet()) { - usersById.put(entry.getKey(), entry.getValue()); + for (Map.Entry entry : encodedUsers.entrySet()) { + usersById.put(entry.getKey(), entry.getValue()); + } + fallbackUsersById = null; + } catch (Throwable ignored) { + usersById = null; + fallbackUsersById = encodedUsers; } } @Override public User findById(long id) { - byte[] encoded = usersById.get(id); + byte[] encoded = usersById != null ? usersById.get(id) : fallbackUsersById.get(id); return encoded == null ? null : deserialize(encoded); } @@ -54,6 +61,7 @@ public class ChronicleMapFixture implements DataFixtures { if (usersById != null) { usersById.close(); } + fallbackUsersById = null; } private byte[] serialize(User user) { diff --git a/src/main/java/com/benchmark/runner/BenchmarkMain.java b/src/main/java/com/benchmark/runner/BenchmarkMain.java index 41c881c..f09b678 100644 --- a/src/main/java/com/benchmark/runner/BenchmarkMain.java +++ b/src/main/java/com/benchmark/runner/BenchmarkMain.java @@ -10,14 +10,28 @@ import org.openjdk.jmh.runner.options.ChainedOptionsBuilder; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; +import java.lang.management.ManagementFactory; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collection; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; +import java.util.concurrent.TimeUnit; public class BenchmarkMain { + private static final String README_FILE = "README.md"; + private static final String RESULTS_SECTION_START = ""; + private static final String RESULTS_SECTION_END = ""; + public static void main(String[] args) throws Exception { if (isJmhListingOrHelpCommand(args)) { org.openjdk.jmh.Main.main(args); @@ -64,6 +78,7 @@ public class BenchmarkMain { Options opt = builder.build(); Collection runResults = new Runner(opt).run(); printSortedSummary(runResults); + updateReadmeWithLatestResults(runResults); } private static void printSortedSummary(Collection runResults) { @@ -113,4 +128,282 @@ public class BenchmarkMain { } return false; } + + private static void updateReadmeWithLatestResults(Collection runResults) { + if (runResults == null || runResults.isEmpty()) { + return; + } + + Path readmePath = Paths.get(README_FILE); + if (!Files.exists(readmePath)) { + return; + } + + String sectionContent = buildReadmeResultsSection(runResults); + + try { + String readme = Files.readString(readmePath, StandardCharsets.UTF_8); + int start = readme.indexOf(RESULTS_SECTION_START); + int end = readme.indexOf(RESULTS_SECTION_END); + + String updated; + if (start >= 0 && end > start) { + int contentStart = start + RESULTS_SECTION_START.length(); + updated = readme.substring(0, contentStart) + + "\n\n" + + sectionContent + + "\n\n" + + readme.substring(end); + } else { + updated = readme + + "\n\n## Benchmark Results (auto-updated)\n\n" + + "This section is managed by the benchmark runner." + + " Running `java -jar target/benchmarks.jar` refreshes the snapshot below.\n\n" + + RESULTS_SECTION_START + + "\n\n" + + sectionContent + + "\n\n" + + RESULTS_SECTION_END + + "\n"; + } + + Files.writeString(readmePath, updated, StandardCharsets.UTF_8); + System.out.println("\nUpdated README benchmark snapshot."); + } catch (IOException e) { + System.err.println("Failed to update README benchmark snapshot: " + e.getMessage()); + } + } + + private static String buildReadmeResultsSection(Collection runResults) { + StringBuilder sb = new StringBuilder(); + ZonedDateTime now = ZonedDateTime.now(); + + sb.append("Last updated: ") + .append(now.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME)) + .append("\n\n"); + + sb.append("### System info\n\n"); + sb.append("- OS: `") + .append(System.getProperty("os.name")) + .append(" ") + .append(System.getProperty("os.version")) + .append("`") + .append(" (") + .append(System.getProperty("os.arch")) + .append(")\n"); + sb.append("- Java: `") + .append(System.getProperty("java.version")) + .append("` (vendor: `") + .append(System.getProperty("java.vendor")) + .append("`)\n"); + sb.append("- JVM: `") + .append(System.getProperty("java.vm.name")) + .append("` `") + .append(System.getProperty("java.vm.version")) + .append("`\n"); + + String cpuModel = detectCpuModel(); + if (cpuModel != null && !cpuModel.isBlank()) { + sb.append("- CPU model: `") + .append(cpuModel) + .append("`\n"); + } + + String ramModel = detectRamModel(); + if (ramModel != null && !ramModel.isBlank()) { + sb.append("- RAM model: `") + .append(ramModel) + .append("`\n"); + } + + long totalPhysicalMemoryBytes = detectTotalPhysicalMemoryBytes(); + if (totalPhysicalMemoryBytes > 0) { + sb.append("- Total physical memory: `") + .append(totalPhysicalMemoryBytes / (1024 * 1024)) + .append(" MB`\n"); + } + + sb.append("- Available processors: `") + .append(Runtime.getRuntime().availableProcessors()) + .append("`\n"); + sb.append("- Max heap: `") + .append(Runtime.getRuntime().maxMemory() / (1024 * 1024)) + .append(" MB`\n\n"); + + RunResult first = runResults.iterator().next(); + sb.append("### Run context\n\n"); + sb.append("- Benchmark: `") + .append(first.getParams().getBenchmark()) + .append("`\n"); + sb.append("- Thread count: `") + .append(first.getParams().getThreads()) + .append("`\n"); + sb.append("- Fixtures in this snapshot: `") + .append(runResults.size()) + .append("`\n\n"); + + Map> byMode = new LinkedHashMap<>(); + for (RunResult runResult : runResults) { + byMode.computeIfAbsent(runResult.getParams().getMode(), m -> new ArrayList<>()).add(runResult); + } + + sb.append("### Results\n\n"); + for (Map.Entry> entry : byMode.entrySet()) { + Mode mode = entry.getKey(); + List sorted = new ArrayList<>(entry.getValue()); + boolean lowerIsBetter = mode == Mode.AverageTime || mode == Mode.SampleTime || mode == Mode.SingleShotTime; + + sorted.sort((a, b) -> { + double scoreA = a.getPrimaryResult().getScore(); + double scoreB = b.getPrimaryResult().getScore(); + return lowerIsBetter ? Double.compare(scoreA, scoreB) : Double.compare(scoreB, scoreA); + }); + + sb.append("#### ").append(mode).append("\n\n"); + sb.append("| Rank | Fixture | Score | Error | Unit |\n"); + sb.append("|---:|---|---:|---:|---|\n"); + + int rank = 1; + for (RunResult runResult : sorted) { + Result result = runResult.getPrimaryResult(); + String fixtureType = runResult.getParams().getParam("fixtureType"); + + sb.append("| ") + .append(rank++) + .append(" | `") + .append(fixtureType) + .append("` | ") + .append(formatDouble(result.getScore())) + .append(" | ") + .append(formatDouble(result.getScoreError())) + .append(" | `") + .append(result.getScoreUnit()) + .append("` |\n"); + } + sb.append("\n"); + } + + return sb.toString().trim(); + } + + private static String formatDouble(double value) { + if (Double.isNaN(value)) { + return "NaN"; + } + if (Double.isInfinite(value)) { + return value > 0 ? "+Inf" : "-Inf"; + } + return String.format(Locale.US, "%.3f", value); + } + + private static String detectCpuModel() { + String modelFromProperty = System.getProperty("cpu.model"); + if (modelFromProperty != null && !modelFromProperty.isBlank()) { + return modelFromProperty.trim(); + } + + Path cpuInfo = Paths.get("/proc/cpuinfo"); + if (Files.exists(cpuInfo)) { + try { + for (String line : Files.readAllLines(cpuInfo, StandardCharsets.UTF_8)) { + if (line.startsWith("model name")) { + int idx = line.indexOf(':'); + if (idx >= 0 && idx + 1 < line.length()) { + String value = line.substring(idx + 1).trim(); + if (!value.isBlank()) { + return value; + } + } + } + } + } catch (IOException ignored) { + // Best effort only. + } + } + + return null; + } + + private static String detectRamModel() { + String modelFromProperty = System.getProperty("ram.model"); + if (modelFromProperty != null && !modelFromProperty.isBlank()) { + return modelFromProperty.trim(); + } + + String modelFromEnv = System.getenv("RAM_MODEL"); + if (modelFromEnv != null && !modelFromEnv.isBlank()) { + return modelFromEnv.trim(); + } + + String osName = System.getProperty("os.name", "").toLowerCase(Locale.ROOT); + if (osName.contains("linux")) { + String fromDmidecode = runLinuxCommandForSingleLine( + "bash", "-lc", + "if command -v dmidecode >/dev/null 2>&1; then " + + "dmidecode -t memory 2>/dev/null | " + + "awk -F: '/^[[:space:]]*Part Number:/{gsub(/^[ \\t]+|[ \\t]+$/, \"\", $2); " + + "if($2!=\"\" && $2!=\"Not Specified\") {print $2; exit}}'; " + + "fi" + ); + if (fromDmidecode != null && !fromDmidecode.isBlank()) { + return fromDmidecode; + } + + String fromLshw = runLinuxCommandForSingleLine( + "bash", "-lc", + "if command -v lshw >/dev/null 2>&1; then " + + "lshw -class memory 2>/dev/null | " + + "awk -F: '/^[[:space:]]*product:/{gsub(/^[ \\t]+|[ \\t]+$/, \"\", $2); " + + "if($2!=\"\" && $2!=\"[empty]\") {print $2; exit}}'; " + + "fi" + ); + if (fromLshw != null && !fromLshw.isBlank()) { + return fromLshw; + } + } + + return null; + } + + private static String runLinuxCommandForSingleLine(String... command) { + Process process = null; + try { + process = new ProcessBuilder(command) + .redirectErrorStream(true) + .start(); + + boolean finished = process.waitFor(3, TimeUnit.SECONDS); + if (!finished) { + process.destroyForcibly(); + return null; + } + + String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim(); + if (output.isBlank()) { + return null; + } + + int newlineIdx = output.indexOf('\n'); + String firstLine = newlineIdx >= 0 ? output.substring(0, newlineIdx).trim() : output; + return firstLine.isBlank() ? null : firstLine; + } catch (Exception ignored) { + return null; + } finally { + if (process != null) { + process.destroy(); + } + } + } + + private static long detectTotalPhysicalMemoryBytes() { + try { + java.lang.management.OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean(); + if (osBean instanceof com.sun.management.OperatingSystemMXBean sunOsBean) { + return sunOsBean.getTotalMemorySize(); + } + } catch (Throwable ignored) { + // Best effort only. + } + return -1; + } }