commit f4ed92b41573ff89981da7e37625654cbf59ea59 Author: Mikhail Yevchenko Date: Sun Apr 5 14:31:17 2026 +0000 Add JMH data access benchmark app diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb5a316 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target diff --git a/README.md b/README.md new file mode 100644 index 0000000..23038f8 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# data-access-benchmark + +```bash +mvn package -q +java --enable-native-access=ALL-UNNAMED --sun-misc-unsafe-memory-access=allow -jar target/benchmarks.jar +``` diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3ece027 --- /dev/null +++ b/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + com.benchmark + data-access-benchmark + 1.0-SNAPSHOT + jar + + + 17 + 17 + UTF-8 + 1.37 + + + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + provided + + + + + com.google.code.gson + gson + 2.10.1 + + + + + com.esotericsoftware + kryo + 5.6.0 + + + + + org.xerial + sqlite-jdbc + 3.45.1.0 + + + + + com.j256.ormlite + ormlite-jdbc + 6.1 + + + + + org.slf4j + slf4j-nop + 2.0.13 + runtime + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + benchmarks + + + org.openjdk.jmh.Main + + + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + + + + + + + diff --git a/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java new file mode 100644 index 0000000..4c64924 --- /dev/null +++ b/src/main/java/com/benchmark/benchmarks/DataAccessBenchmark.java @@ -0,0 +1,42 @@ +package com.benchmark.benchmarks; + +import com.benchmark.fixtures.DataFixtures; +import com.benchmark.fixtures.DataGenerator; +import com.benchmark.fixtures.FixtureFactory; +import com.benchmark.model.User; +import org.openjdk.jmh.annotations.*; + +import java.util.concurrent.TimeUnit; + +@State(Scope.Thread) +@BenchmarkMode({Mode.AverageTime, Mode.Throughput}) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +@Warmup(iterations = 3, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(1) +@Threads(Threads.MAX) +public class DataAccessBenchmark { + + @Param({"gson", "kryo", "sqlite-jdbc", "sqlite-ormlite"}) + private String fixtureType; + + private DataFixtures fixture; + + private static final long TARGET_ID = 500L; + + @Setup(Level.Trial) + public void setup() throws Exception { + fixture = FixtureFactory.create(fixtureType); + fixture.setup(DataGenerator.generate(1000)); + } + + @Benchmark + public User readSingleUser() throws Exception { + return fixture.findById(TARGET_ID); + } + + @TearDown(Level.Trial) + public void teardown() throws Exception { + fixture.teardown(); + } +} diff --git a/src/main/java/com/benchmark/fixtures/DataFixtures.java b/src/main/java/com/benchmark/fixtures/DataFixtures.java new file mode 100644 index 0000000..1be37f9 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/DataFixtures.java @@ -0,0 +1,11 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; + +import java.util.List; + +public interface DataFixtures { + void setup(List users) throws Exception; + User findById(long id) throws Exception; + void teardown() throws Exception; +} diff --git a/src/main/java/com/benchmark/fixtures/DataGenerator.java b/src/main/java/com/benchmark/fixtures/DataGenerator.java new file mode 100644 index 0000000..9ca3adc --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/DataGenerator.java @@ -0,0 +1,26 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; + +import java.util.ArrayList; +import java.util.List; + +public class DataGenerator { + + /** + * Generates {@code count} deterministic User objects with IDs 1..count. + */ + public static List generate(int count) { + List users = new ArrayList<>(count); + for (int i = 1; i <= count; i++) { + users.add(new User( + i, + "User" + i, + "user" + i + "@example.com", + 20 + (i % 60), + 1_000_000L + i + )); + } + return users; + } +} diff --git a/src/main/java/com/benchmark/fixtures/FixtureFactory.java b/src/main/java/com/benchmark/fixtures/FixtureFactory.java new file mode 100644 index 0000000..d66569d --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/FixtureFactory.java @@ -0,0 +1,14 @@ +package com.benchmark.fixtures; + +public class FixtureFactory { + + public static DataFixtures create(String type) { + return switch (type) { + case "gson" -> new GsonFileFixture(); + case "kryo" -> new KryoFileFixture(); + 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/GsonFileFixture.java b/src/main/java/com/benchmark/fixtures/GsonFileFixture.java new file mode 100644 index 0000000..bf63735 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/GsonFileFixture.java @@ -0,0 +1,43 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.benchmark.util.FileUtil; +import com.google.gson.Gson; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +public class GsonFileFixture implements DataFixtures { + + private Gson gson; + private Path dataDir; + + @Override + public void setup(List users) throws IOException { + gson = new Gson(); + dataDir = Files.createTempDirectory("benchmark-gson-"); + for (User user : users) { + Path file = FileUtil.userJsonPath(dataDir, user.getId()); + Files.writeString(file, gson.toJson(user)); + } + } + + @Override + public User findById(long id) throws IOException { + Path file = FileUtil.userJsonPath(dataDir, id); + String json = Files.readString(file); + return gson.fromJson(json, User.class); + } + + @Override + public void teardown() throws IOException { + if (dataDir != null && Files.exists(dataDir)) { + Files.walk(dataDir) + .sorted(Comparator.reverseOrder()) + .forEach(p -> p.toFile().delete()); + } + } +} diff --git a/src/main/java/com/benchmark/fixtures/KryoFileFixture.java b/src/main/java/com/benchmark/fixtures/KryoFileFixture.java new file mode 100644 index 0000000..e149026 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/KryoFileFixture.java @@ -0,0 +1,53 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.benchmark.util.FileUtil; +import com.esotericsoftware.kryo.Kryo; +import com.esotericsoftware.kryo.io.Input; +import com.esotericsoftware.kryo.io.Output; + +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.List; + +public class KryoFileFixture implements DataFixtures { + + private Kryo kryo; + private Path dataDir; + + @Override + public void setup(List users) throws IOException { + kryo = new Kryo(); + kryo.setRegistrationRequired(false); + kryo.register(User.class); + + dataDir = Files.createTempDirectory("benchmark-kryo-"); + for (User user : users) { + Path file = FileUtil.userBinPath(dataDir, user.getId()); + try (Output output = new Output(new FileOutputStream(file.toFile()))) { + kryo.writeObject(output, user); + } + } + } + + @Override + public User findById(long id) throws IOException { + Path file = FileUtil.userBinPath(dataDir, id); + try (Input input = new Input(new FileInputStream(file.toFile()))) { + return kryo.readObject(input, User.class); + } + } + + @Override + public void teardown() throws IOException { + if (dataDir != null && Files.exists(dataDir)) { + Files.walk(dataDir) + .sorted(Comparator.reverseOrder()) + .forEach(p -> p.toFile().delete()); + } + } +} diff --git a/src/main/java/com/benchmark/fixtures/SqliteJdbcFixture.java b/src/main/java/com/benchmark/fixtures/SqliteJdbcFixture.java new file mode 100644 index 0000000..9d09508 --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/SqliteJdbcFixture.java @@ -0,0 +1,57 @@ +package com.benchmark.fixtures; + +import com.benchmark.model.User; +import com.benchmark.util.DbUtil; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.*; +import java.util.List; + +public class SqliteJdbcFixture implements DataFixtures { + + private Connection connection; + private PreparedStatement selectStmt; + private Path dbFile; + + @Override + public void setup(List users) throws SQLException, Exception { + dbFile = Files.createTempFile("benchmark-jdbc-", ".db"); + connection = DriverManager.getConnection("jdbc:sqlite:" + dbFile.toAbsolutePath()); + 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(); + if (dbFile != null) Files.deleteIfExists(dbFile); + } +} diff --git a/src/main/java/com/benchmark/fixtures/SqliteOrmLiteFixture.java b/src/main/java/com/benchmark/fixtures/SqliteOrmLiteFixture.java new file mode 100644 index 0000000..012511a --- /dev/null +++ b/src/main/java/com/benchmark/fixtures/SqliteOrmLiteFixture.java @@ -0,0 +1,44 @@ +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.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +public class SqliteOrmLiteFixture implements DataFixtures { + + private JdbcConnectionSource connectionSource; + private Dao dao; + private Path dbFile; + + @Override + public void setup(List users) throws Exception { + dbFile = Files.createTempFile("benchmark-ormlite-", ".db"); + connectionSource = new JdbcConnectionSource("jdbc:sqlite:" + dbFile.toAbsolutePath()); + 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(); + if (dbFile != null) Files.deleteIfExists(dbFile); + } +} diff --git a/src/main/java/com/benchmark/model/User.java b/src/main/java/com/benchmark/model/User.java new file mode 100644 index 0000000..0ce74a6 --- /dev/null +++ b/src/main/java/com/benchmark/model/User.java @@ -0,0 +1,46 @@ +package com.benchmark.model; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; + +@DatabaseTable(tableName = "users") +public class User { + + @DatabaseField(id = true) + private long id; + + @DatabaseField + private String name; + + @DatabaseField + private String email; + + @DatabaseField + private int age; + + @DatabaseField(columnName = "created_at") + private long createdAt; + + /** Required by Gson and ORMLite. */ + public User() {} + + public User(long id, String name, String email, int age, long createdAt) { + this.id = id; + this.name = name; + this.email = email; + this.age = age; + this.createdAt = createdAt; + } + + public long getId() { return id; } + public String getName() { return name; } + public String getEmail() { return email; } + public int getAge() { return age; } + public long getCreatedAt() { return createdAt; } + + public void setId(long id) { this.id = id; } + public void setName(String name) { this.name = name; } + public void setEmail(String email) { this.email = email; } + public void setAge(int age) { this.age = age; } + public void setCreatedAt(long createdAt) { this.createdAt = createdAt; } +} diff --git a/src/main/java/com/benchmark/runner/BenchmarkMain.java b/src/main/java/com/benchmark/runner/BenchmarkMain.java new file mode 100644 index 0000000..796e195 --- /dev/null +++ b/src/main/java/com/benchmark/runner/BenchmarkMain.java @@ -0,0 +1,20 @@ +package com.benchmark.runner; + +import com.benchmark.benchmarks.DataAccessBenchmark; +import org.openjdk.jmh.runner.Runner; +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(); + new Runner(opt).run(); + } +} diff --git a/src/main/java/com/benchmark/util/DbUtil.java b/src/main/java/com/benchmark/util/DbUtil.java new file mode 100644 index 0000000..3956e37 --- /dev/null +++ b/src/main/java/com/benchmark/util/DbUtil.java @@ -0,0 +1,32 @@ +package com.benchmark.util; + +import com.benchmark.model.User; + +import java.sql.*; + +public class DbUtil { + + public static void createSchema(Connection conn) throws SQLException { + try (Statement stmt = conn.createStatement()) { + stmt.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + email TEXT NOT NULL, + age INTEGER NOT NULL, + created_at INTEGER NOT NULL + ) + """); + } + } + + public static User mapRow(ResultSet rs) throws SQLException { + return new User( + rs.getLong("id"), + rs.getString("name"), + rs.getString("email"), + rs.getInt("age"), + rs.getLong("created_at") + ); + } +} diff --git a/src/main/java/com/benchmark/util/FileUtil.java b/src/main/java/com/benchmark/util/FileUtil.java new file mode 100644 index 0000000..2292478 --- /dev/null +++ b/src/main/java/com/benchmark/util/FileUtil.java @@ -0,0 +1,14 @@ +package com.benchmark.util; + +import java.nio.file.Path; + +public class FileUtil { + + public static Path userJsonPath(Path dir, long id) { + return dir.resolve(id + ".json"); + } + + public static Path userBinPath(Path dir, long id) { + return dir.resolve(id + ".bin"); + } +}