diff --git a/pom.xml b/pom.xml index 3ece027..20146cd 100644 --- a/pom.xml +++ b/pom.xml @@ -59,6 +59,13 @@ 6.1 + + + net.openhft + chronicle-map + 2026.1 + + org.slf4j @@ -101,7 +108,7 @@ benchmarks - org.openjdk.jmh.Main + com.benchmark.runner.BenchmarkMain diff --git a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java index 4c64924..d8a8299 100644 --- a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java +++ b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java @@ -17,7 +17,16 @@ import java.util.concurrent.TimeUnit; @Threads(Threads.MAX) public class DataAccessBenchmark { - @Param({"gson", "kryo", "sqlite-jdbc", "sqlite-ormlite"}) + @Param({ + "in-memory", + "memory-mapped-file", + "gson", + "kryo", + "sqlite-jdbc-memory", + "sqlite-ormlite-memory", + "sqlite-jdbc", + "sqlite-ormlite" + }) private String fixtureType; private DataFixtures fixture; diff --git a/src/main/java/com/benchmark/fixtures/ChronicleMapFixture.java b/src/main/java/com/benchmark/fixtures/ChronicleMapFixture.java new file mode 100644 index 0000000..24d9dd4 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/ChronicleMapFixture.java @@ -0,0 +1,72 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; +import net.openhft.chronicle.map.ChronicleMap; +import net.openhft.chronicle.map.ChronicleMapBuilder; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ChronicleMapFixture implements DataFixtures { + + private ChronicleMap usersById; + private Kryo kryo; + + @Override + public void setup(List users) throws IOException { + kryo = new Kryo(); + kryo.setRegistrationRequired(false); + kryo.register(User.class); + + Map encodedUsers = new HashMap<>(users.size()); + int maxValueSize = 1; + for (User user : users) { + byte[] encoded = serialize(user); + encodedUsers.put(user.getId(), encoded); + 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(); + + for (Map.Entry entry : encodedUsers.entrySet()) { + usersById.put(entry.getKey(), entry.getValue()); + } + } + + @Override + public User findById(long id) { + byte[] encoded = usersById.get(id); + return encoded == null ? null : deserialize(encoded); + } + + @Override + public void teardown() throws IOException { + if (usersById != null) { + usersById.close(); + } + } + + private byte[] serialize(User user) { + Output output = new Output(256, -1); + kryo.writeObject(output, user); + byte[] bytes = output.toBytes(); + output.close(); + return bytes; + } + + private User deserialize(byte[] bytes) { + try (Input input = new Input(bytes)) { + return kryo.readObject(input, User.class); + } + } +} diff --git a/src/main/java/com/benchmark/fixtures/FixtureFactory.java b/src/main/java/com/benchmark/fixtures/FixtureFactory.java index d66569d..8e1c681 100644 --- a/src/main/java/com/benchmark/fixtures/FixtureFactory.java +++ b/src/main/java/com/benchmark/fixtures/FixtureFactory.java @@ -4,8 +4,13 @@ public class FixtureFactory { public static DataFixtures create(String type) { return switch (type) { + case "in-memory" -> new InMemoryFixture(); + case "memory-mapped-file" -> new MemoryMappedFileFixture(); + case "chronicle-map" -> new ChronicleMapFixture(); case "gson" -> new GsonFileFixture(); case "kryo" -> new KryoFileFixture(); + case "sqlite-jdbc-memory" -> new SqliteInMemoryJdbcFixture(); + case "sqlite-ormlite-memory" -> new SqliteInMemoryOrmLiteFixture(); case "sqlite-jdbc" -> new SqliteJdbcFixture(); case "sqlite-ormlite" -> new SqliteOrmLiteFixture(); default -> throw new IllegalArgumentException("Unknown fixture type: " + type); diff --git a/src/main/java/com/benchmark/fixtures/InMemoryFixture.java b/src/main/java/com/benchmark/fixtures/InMemoryFixture.java new file mode 100644 index 0000000..083de8c --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/InMemoryFixture.java @@ -0,0 +1,32 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class InMemoryFixture implements DataFixtures { + + private Map usersById; + + @Override + public void setup(List users) { + usersById = new HashMap<>((int) ((users.size() / 0.75f) + 1)); + for (User user : users) { + usersById.put(user.getId(), user); + } + } + + @Override + public User findById(long id) { + return usersById.get(id); + } + + @Override + public void teardown() { + if (usersById != null) { + usersById.clear(); + } + } +} diff --git a/src/main/java/com/benchmark/fixtures/MemoryMappedFileFixture.java b/src/main/java/com/benchmark/fixtures/MemoryMappedFileFixture.java new file mode 100644 index 0000000..f42d7d4 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/MemoryMappedFileFixture.java @@ -0,0 +1,98 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class MemoryMappedFileFixture implements DataFixtures { + + private Kryo kryo; + private Path dataFile; + private RandomAccessFile randomAccessFile; + private FileChannel fileChannel; + private MappedByteBuffer mappedBuffer; + private int recordSize; + + @Override + public void setup(List users) throws IOException { + kryo = new Kryo(); + kryo.setRegistrationRequired(false); + kryo.register(User.class); + + Map encodedUsers = new HashMap<>(users.size()); + int maxValueSize = 1; + for (User user : users) { + byte[] encoded = serialize(user); + encodedUsers.put(user.getId(), encoded); + maxValueSize = Math.max(maxValueSize, encoded.length); + } + + recordSize = Integer.BYTES + maxValueSize; + long fileSize = (long) recordSize * users.size(); + + dataFile = Files.createTempFile("benchmark-mapped-", ".bin"); + randomAccessFile = new RandomAccessFile(dataFile.toFile(), "rw"); + randomAccessFile.setLength(fileSize); + fileChannel = randomAccessFile.getChannel(); + mappedBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileSize); + + for (Map.Entry entry : encodedUsers.entrySet()) { + int offset = offsetFor(entry.getKey()); + mappedBuffer.position(offset); + mappedBuffer.putInt(entry.getValue().length); + mappedBuffer.put(entry.getValue()); + } + } + + @Override + public User findById(long id) { + MappedByteBuffer duplicate = mappedBuffer.duplicate(); + duplicate.position(offsetFor(id)); + int length = duplicate.getInt(); + byte[] bytes = new byte[length]; + duplicate.get(bytes); + return deserialize(bytes); + } + + @Override + public void teardown() throws IOException { + if (fileChannel != null) { + fileChannel.close(); + } + if (randomAccessFile != null) { + randomAccessFile.close(); + } + if (dataFile != null) { + Files.deleteIfExists(dataFile); + } + } + + private int offsetFor(long id) { + return (int) ((id - 1) * (long) recordSize); + } + + private byte[] serialize(User user) { + Output output = new Output(256, -1); + kryo.writeObject(output, user); + byte[] bytes = output.toBytes(); + output.close(); + return bytes; + } + + private User deserialize(byte[] bytes) { + try (Input input = new Input(bytes)) { + return kryo.readObject(input, User.class); + } + } +} diff --git a/src/main/java/com/benchmark/fixtures/SqliteInMemoryJdbcFixture.java b/src/main/java/com/benchmark/fixtures/SqliteInMemoryJdbcFixture.java new file mode 100644 index 0000000..acaf3c2 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/SqliteInMemoryJdbcFixture.java @@ -0,0 +1,62 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.benchmark.util.DbUtil; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; +import java.util.UUID; + +public class SqliteInMemoryJdbcFixture implements DataFixtures { + + private Connection connection; + private PreparedStatement selectStmt; + + @Override + public void setup(List users) throws Exception { + String dbUrl = "jdbc:sqlite:file:benchmark-jdbc-memory-" + UUID.randomUUID() + "?mode=memory&cache=shared"; + connection = DriverManager.getConnection(dbUrl); + DbUtil.createSchema(connection); + + connection.setAutoCommit(false); + try (PreparedStatement ps = connection.prepareStatement( + "INSERT INTO users(id, name, email, age, created_at) VALUES(?,?,?,?,?)")) { + for (User user : users) { + ps.setLong(1, user.getId()); + ps.setString(2, user.getName()); + ps.setString(3, user.getEmail()); + ps.setInt(4, user.getAge()); + ps.setLong(5, user.getCreatedAt()); + ps.addBatch(); + } + ps.executeBatch(); + } + connection.commit(); + connection.setAutoCommit(true); + + selectStmt = connection.prepareStatement( + "SELECT id, name, email, age, created_at FROM users WHERE id = ?"); + } + + @Override + public User findById(long id) throws SQLException { + selectStmt.setLong(1, id); + try (ResultSet rs = selectStmt.executeQuery()) { + return rs.next() ? DbUtil.mapRow(rs) : null; + } + } + + @Override + public void teardown() throws Exception { + if (selectStmt != null) { + selectStmt.close(); + } + if (connection != null) { + connection.close(); + } + } +} diff --git a/src/main/java/com/benchmark/fixtures/SqliteInMemoryOrmLiteFixture.java b/src/main/java/com/benchmark/fixtures/SqliteInMemoryOrmLiteFixture.java new file mode 100644 index 0000000..062a20d --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/SqliteInMemoryOrmLiteFixture.java @@ -0,0 +1,43 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.j256.ormlite.dao.Dao; +import com.j256.ormlite.dao.DaoManager; +import com.j256.ormlite.jdbc.JdbcConnectionSource; +import com.j256.ormlite.table.TableUtils; + +import java.util.List; +import java.util.UUID; + +public class SqliteInMemoryOrmLiteFixture implements DataFixtures { + + private JdbcConnectionSource connectionSource; + private Dao dao; + + @Override + public void setup(List users) throws Exception { + String dbUrl = "jdbc:sqlite:file:benchmark-ormlite-memory-" + UUID.randomUUID() + "?mode=memory&cache=shared"; + connectionSource = new JdbcConnectionSource(dbUrl); + TableUtils.createTableIfNotExists(connectionSource, User.class); + dao = DaoManager.createDao(connectionSource, User.class); + + dao.callBatchTasks(() -> { + for (User user : users) { + dao.create(user); + } + return null; + }); + } + + @Override + public User findById(long id) throws Exception { + return dao.queryForId(id); + } + + @Override + public void teardown() throws Exception { + if (connectionSource != null) { + connectionSource.close(); + } + } +} diff --git a/src/main/java/com/benchmark/runner/BenchmarkMain.java b/src/main/java/com/benchmark/runner/BenchmarkMain.java index 796e195..5ee4a64 100644 --- a/src/main/java/com/benchmark/runner/BenchmarkMain.java +++ b/src/main/java/com/benchmark/runner/BenchmarkMain.java @@ -1,20 +1,65 @@ package com.benchmark.runner; import com.benchmark.benchmarks.DataAccessBenchmark; +import org.openjdk.jmh.runner.options.CommandLineOptions; import org.openjdk.jmh.runner.Runner; +import org.openjdk.jmh.runner.options.ChainedOptionsBuilder; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; public class BenchmarkMain { public static void main(String[] args) throws Exception { - Options opt = new OptionsBuilder() - .include(DataAccessBenchmark.class.getSimpleName()) - .jvmArgsPrepend( - "--enable-native-access=ALL-UNNAMED", - "--sun-misc-unsafe-memory-access=allow" - ) - .build(); + if (isJmhListingOrHelpCommand(args)) { + org.openjdk.jmh.Main.main(args); + return; + } + + CommandLineOptions commandLineOptions = new CommandLineOptions(args); + + ChainedOptionsBuilder builder = new OptionsBuilder() + .parent(commandLineOptions) + .jvmArgsPrepend( + "--enable-native-access=ALL-UNNAMED", + "--sun-misc-unsafe-memory-access=allow", + "--add-opens=java.base/java.lang=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", + "--add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED" + ); + + if (args.length == 0) { + builder.include(DataAccessBenchmark.class.getSimpleName()); + } + + Options opt = builder.build(); new Runner(opt).run(); } + + private static boolean isJmhListingOrHelpCommand(String[] args) { + for (String arg : args) { + if ("-l".equals(arg) || "-lp".equals(arg) || "-lprof".equals(arg) + || "-h".equals(arg) || "--help".equals(arg)) { + return true; + } + } + return false; + } }