Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)
}
}
21 changes: 21 additions & 0 deletions modules/bench/src/main/scala/doobie/bench/README.md
Original file line number Diff line number Diff line change
@@ -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
```
63 changes: 19 additions & 44 deletions modules/bench/src/main/scala/doobie/bench/select.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?")
Expand All @@ -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
}
55 changes: 36 additions & 19 deletions modules/bench/src/main/scala/doobie/bench/text.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
}
}