From 7a8c182e81a972c19d9fa33702b83591f303dbe7 Mon Sep 17 00:00:00 2001 From: Sam Guymer Date: Sat, 11 Oct 2025 10:57:41 +1000 Subject: [PATCH] Fix benchmarks so they run --- .../bench/PostgresConnectionState.scala | 32 ++++++++++ .../src/main/scala/doobie/bench/README.md | 21 +++++++ .../src/main/scala/doobie/bench/select.scala | 63 ++++++------------- .../src/main/scala/doobie/bench/text.scala | 55 ++++++++++------ 4 files changed, 108 insertions(+), 63 deletions(-) create mode 100644 modules/bench/src/main/scala/doobie/bench/PostgresConnectionState.scala create mode 100644 modules/bench/src/main/scala/doobie/bench/README.md diff --git a/modules/bench/src/main/scala/doobie/bench/PostgresConnectionState.scala b/modules/bench/src/main/scala/doobie/bench/PostgresConnectionState.scala new file mode 100644 index 000000000..8ae0ccb5e --- /dev/null +++ b/modules/bench/src/main/scala/doobie/bench/PostgresConnectionState.scala @@ -0,0 +1,32 @@ +package doobie.bench + +import cats.effect.IO +import doobie.free.connection.ConnectionIO +import doobie.syntax.connectionio.* +import doobie.util.transactor.Transactor +import org.openjdk.jmh.annotations.* + +import java.sql.Connection +import java.sql.DriverManager + +@State(Scope.Thread) +class PostgresConnectionState { + + var connection: Connection = _ + var xa: Transactor[IO] = _ + + @Setup() + def setup(): Unit = { + connection = DriverManager.getConnection("jdbc:postgresql:world", "postgres", "password") + xa = Transactor.fromConnection[IO](connection) + } + + @TearDown() + def tearDown(): Unit = { + connection.close() + } + + def transact[A](io: ConnectionIO[A]): A = { + io.transact(xa).unsafeRunSync()(cats.effect.unsafe.implicits.global) + } +} diff --git a/modules/bench/src/main/scala/doobie/bench/README.md b/modules/bench/src/main/scala/doobie/bench/README.md new file mode 100644 index 000000000..d5c54817c --- /dev/null +++ b/modules/bench/src/main/scala/doobie/bench/README.md @@ -0,0 +1,21 @@ + +`bench/Jmh/run -wi 2 -i 2 -f 1 doobie.bench.select` +``` +# JMH version: 1.37 +# VM version: JDK 21.0.8, OpenJDK 64-Bit Server VM +Benchmark Mode Cnt Score Error Units +d.b.s.list_accum_1000_jdbc avgt 2 172.809 ns/op +d.b.s.list_accum_1000 avgt 2 209.440 ns/op +d.b.s.stream_accum_1000 avgt 2 250.966 ns/op +``` + +`bench/Jmh/run -wi 2 -i 2 -f 1 doobie.bench.text` +``` +# JMH version: 1.37 +# VM version: JDK 21.0.8, OpenJDK 64-Bit Server VM +Benchmark Mode Cnt Score Error Units +d.b.t.batch avgt 2 2257.216 ns/op +d.b.t.batch_optimized avgt 2 2008.081 ns/op +d.b.t.copy_foldable avgt 2 306.674 ns/op +d.b.t.copy_stream avgt 2 382.481 ns/op +``` diff --git a/modules/bench/src/main/scala/doobie/bench/select.scala b/modules/bench/src/main/scala/doobie/bench/select.scala index 52b29410a..cc1b76b46 100644 --- a/modules/bench/src/main/scala/doobie/bench/select.scala +++ b/modules/bench/src/main/scala/doobie/bench/select.scala @@ -4,30 +4,21 @@ package doobie.bench -import cats.effect.IO import cats.syntax.apply.* -import doobie.syntax.connectionio.* import doobie.syntax.string.* import doobie.util.Read -import doobie.util.transactor.Transactor import org.openjdk.jmh.annotations.* -import java.sql.DriverManager +import java.sql.Connection +import java.util.concurrent.TimeUnit -object shared { - - @State(Scope.Benchmark) - val xa = Transactor.fromDriverManager[IO]("org.postgresql.Driver", "jdbc:postgresql:world", "postgres", "password") -} - -class bench { - import cats.effect.unsafe.implicits.global - import shared.* +@BenchmarkMode(Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +class select { + import select.* // Baseline hand-written JDBC code - def jdbcBench(n: Int): Int = { - Class.forName("org.postgresql.Driver") - val co = DriverManager.getConnection("jdbc:postgresql:world", "postgres", "password") + private def jdbcBench(co: Connection, n: Int): Int = { try { co.setAutoCommit(false) val ps = co.prepareStatement("select a.name, b.name, co.name from country a, country b, country co limit ?") @@ -47,58 +38,42 @@ class bench { } finally ps.close } finally { co.commit() - co.close() } } - implicit val read3Strings: Read[(String, String, String)] = ( - Read[String], - Read[String], - Read[String], - ).tupled - // Reading via .stream, which adds a fair amount of overhead - def doobieBenchP(n: Int): Int = + private def doobieBenchP(n: Int) = sql"select a.name, b.name, c.name from country a, country b, country c limit $n" .query[(String, String, String)] .stream .compile.toList - .transact(xa) .map(_.length) - .unsafeRunSync() // Reading via .list, which uses a lower-level collector - def doobieBench(n: Int): Int = + private def doobieBench(n: Int) = sql"select a.name, b.name, c.name from country a, country b, country c limit $n" .query[(String, String, String)] .to[List] - .transact(xa) .map(_.length) - .unsafeRunSync() - - // Reading via .vector, which uses a lower-level collector - def doobieBenchV(n: Int): Int = - sql"select a.name, b.name, c.name from country a, country b, country c limit $n" - .query[(String, String, String)] - .to[Vector] - .transact(xa) - .map(_.length) - .unsafeRunSync() @Benchmark @OperationsPerInvocation(1000) - def list_accum_1000_jdbc: Int = jdbcBench(1000) + def list_accum_1000_jdbc(state: PostgresConnectionState): Int = jdbcBench(state.connection, 1000) @Benchmark @OperationsPerInvocation(1000) - def list_accum_1000: Int = doobieBench(1000) + def list_accum_1000(state: PostgresConnectionState): Int = state.transact(doobieBench(1000)) @Benchmark @OperationsPerInvocation(1000) - def vector_accum_1000: Int = doobieBenchV(1000) + def stream_accum_1000(state: PostgresConnectionState): Int = state.transact(doobieBenchP(1000)) - @Benchmark - @OperationsPerInvocation(1000) - def stream_accum_1000: Int = doobieBenchP(1000) +} +object select { + implicit val read3Strings: Read[(String, String, String)] = ( + Read[String], + Read[String], + Read[String], + ).tupled } diff --git a/modules/bench/src/main/scala/doobie/bench/text.scala b/modules/bench/src/main/scala/doobie/bench/text.scala index c967164a1..acfd8faf1 100644 --- a/modules/bench/src/main/scala/doobie/bench/text.scala +++ b/modules/bench/src/main/scala/doobie/bench/text.scala @@ -12,35 +12,28 @@ import doobie.HC import doobie.HPS import doobie.free.connection.ConnectionIO import doobie.postgres.syntax.fragment.* -import doobie.syntax.connectionio.* import doobie.syntax.string.* import doobie.util.Write import fs2.Stream import org.openjdk.jmh.annotations.* -final case class Person(name: String, age: Int) -object Person { - implicit val write: Write[Person] = Write.derived -} +import java.util.concurrent.TimeUnit +@BenchmarkMode(Array(Mode.AverageTime)) +@OutputTimeUnit(TimeUnit.NANOSECONDS) class text { - import cats.effect.unsafe.implicits.global - import shared.* + import text.* def people(n: Int): List[Person] = List.fill(n)(Person("Bob", 42)) - def ddl: ConnectionIO[Unit] = - sql"drop table if exists bench_person".update.run *> - sql"create table bench_person (name varchar not null, age integer not null)".update.run.void - def naive(n: Int): ConnectionIO[Int] = - ddl *> HC.prepareStatement("insert into bench_person (name, age) values (?, ?)")( + HC.prepareStatement("insert into bench_person (name, age) values (?, ?)")( people(n).foldRight(HPS.executeBatch)((p, k) => HPS.set(p) *> HPS.addBatch *> k), ).map(_.combineAll) def optimized(n: Int): ConnectionIO[Int] = - ddl *> HC.prepareStatement("insert into bench_person (name, age) values (?, ?)")( + HC.prepareStatement("insert into bench_person (name, age) values (?, ?)")( FPS.raw { ps => people(n).foreach { p => ps.setString(1, p.name) @@ -52,24 +45,48 @@ class text { ) def copyin_stream(n: Int): ConnectionIO[Long] = - ddl *> sql"COPY bench_person (name, age) FROM STDIN".copyIn(Stream.emits[ConnectionIO, Person](people(n)), 10000) + sql"COPY bench_person (name, age) FROM STDIN".copyIn(Stream.emits[ConnectionIO, Person](people(n)), 10000) def copyin_foldable(n: Int): ConnectionIO[Long] = - ddl *> sql"COPY bench_person (name, age) FROM STDIN".copyIn(people(n)) + sql"COPY bench_person (name, age) FROM STDIN".copyIn(people(n)) @Benchmark @OperationsPerInvocation(10000) - def naive_copyin: Int = naive(10000).transact(xa).unsafeRunSync() + def batch(state: state): Int = state.transact(naive(10000)) @Benchmark @OperationsPerInvocation(10000) - def jdbc_copyin: Int = optimized(10000).transact(xa).unsafeRunSync() + def batch_optimized(state: state): Int = state.transact(optimized(10000)) @Benchmark @OperationsPerInvocation(10000) - def fast_copyin_stream: Long = copyin_stream(10000).transact(xa).unsafeRunSync() + def copy_stream(state: state): Long = state.transact(copyin_stream(10000)) @Benchmark @OperationsPerInvocation(10000) - def fast_copyin_foldable: Long = copyin_foldable(10000).transact(xa).unsafeRunSync() + def copy_foldable(state: state): Long = state.transact(copyin_foldable(10000)) +} +object text { + + final case class Person(name: String, age: Int) + object Person { + implicit val write: Write[Person] = Write.derived + } + + private val ddl: ConnectionIO[Unit] = + sql"drop table if exists bench_person".update.run *> + sql"create table bench_person (name varchar not null, age integer not null)".update.run.void + + @State(Scope.Thread) + class state extends PostgresConnectionState { + + @Setup() + override def setup(): Unit = { + super.setup() + transact(ddl) + } + + @TearDown() + override def tearDown(): Unit = super.tearDown() + } }