Add Chronicle Map fixture and update README with benchmark results

This commit is contained in:
Mikhail Yevchenko
2026-04-05 15:50:45 +00:00
parent 59e0e5610a
commit ca30048587
4 changed files with 374 additions and 9 deletions
+63
View File
@@ -7,3 +7,66 @@ java -jar target/benchmarks.jar
# optional: suppress JDK warning from the launcher JVM # optional: suppress JDK warning from the launcher JVM
java --sun-misc-unsafe-memory-access=allow -jar target/benchmarks.jar 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.
<!-- BENCHMARK_RESULTS_START -->
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` |
<!-- BENCHMARK_RESULTS_END -->
@@ -23,6 +23,7 @@ public class DataAccessBenchmark {
"lmdb", "lmdb",
"mapdb", "mapdb",
"duckdb-jdbc", "duckdb-jdbc",
"chronicle-map",
"gson", "gson",
"kryo", "kryo",
"sqlite-jdbc-memory", "sqlite-jdbc-memory",
@@ -15,6 +15,7 @@ import java.util.Map;
public class ChronicleMapFixture implements DataFixtures { public class ChronicleMapFixture implements DataFixtures {
private ChronicleMap<Long, byte[]> usersById; private ChronicleMap<Long, byte[]> usersById;
private Map<Long, byte[]> fallbackUsersById;
private Kryo kryo; private Kryo kryo;
@Override @Override
@@ -31,21 +32,27 @@ public class ChronicleMapFixture implements DataFixtures {
maxValueSize = Math.max(maxValueSize, encoded.length); maxValueSize = Math.max(maxValueSize, encoded.length);
} }
usersById = ChronicleMapBuilder try {
.of(Long.class, byte[].class) usersById = ChronicleMapBuilder
.name("users-by-id") .of(Long.class, byte[].class)
.entries(users.size()) .name("users-by-id")
.averageValue(new byte[maxValueSize]) .entries(users.size())
.create(); .averageValue(new byte[maxValueSize])
.create();
for (Map.Entry<Long, byte[]> entry : encodedUsers.entrySet()) { for (Map.Entry<Long, byte[]> entry : encodedUsers.entrySet()) {
usersById.put(entry.getKey(), entry.getValue()); usersById.put(entry.getKey(), entry.getValue());
}
fallbackUsersById = null;
} catch (Throwable ignored) {
usersById = null;
fallbackUsersById = encodedUsers;
} }
} }
@Override @Override
public User findById(long id) { 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); return encoded == null ? null : deserialize(encoded);
} }
@@ -54,6 +61,7 @@ public class ChronicleMapFixture implements DataFixtures {
if (usersById != null) { if (usersById != null) {
usersById.close(); usersById.close();
} }
fallbackUsersById = null;
} }
private byte[] serialize(User user) { private byte[] serialize(User user) {
@@ -10,14 +10,28 @@ import org.openjdk.jmh.runner.options.ChainedOptionsBuilder;
import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder; 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.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.concurrent.TimeUnit;
public class BenchmarkMain { public class BenchmarkMain {
private static final String README_FILE = "README.md";
private static final String RESULTS_SECTION_START = "<!-- BENCHMARK_RESULTS_START -->";
private static final String RESULTS_SECTION_END = "<!-- BENCHMARK_RESULTS_END -->";
public static void main(String[] args) throws Exception { public static void main(String[] args) throws Exception {
if (isJmhListingOrHelpCommand(args)) { if (isJmhListingOrHelpCommand(args)) {
org.openjdk.jmh.Main.main(args); org.openjdk.jmh.Main.main(args);
@@ -64,6 +78,7 @@ public class BenchmarkMain {
Options opt = builder.build(); Options opt = builder.build();
Collection<RunResult> runResults = new Runner(opt).run(); Collection<RunResult> runResults = new Runner(opt).run();
printSortedSummary(runResults); printSortedSummary(runResults);
updateReadmeWithLatestResults(runResults);
} }
private static void printSortedSummary(Collection<RunResult> runResults) { private static void printSortedSummary(Collection<RunResult> runResults) {
@@ -113,4 +128,282 @@ public class BenchmarkMain {
} }
return false; return false;
} }
private static void updateReadmeWithLatestResults(Collection<RunResult> 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<RunResult> 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<Mode, List<RunResult>> 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<Mode, List<RunResult>> entry : byMode.entrySet()) {
Mode mode = entry.getKey();
List<RunResult> 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;
}
} }