Skip to content

Commit

Permalink
chore: Add benchmark module
Browse files Browse the repository at this point in the history
  • Loading branch information
Chuckame committed May 9, 2024
1 parent 0ec51a5 commit a0d4272
Show file tree
Hide file tree
Showing 10 changed files with 480 additions and 0 deletions.
38 changes: 38 additions & 0 deletions benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Kotlin Avro Benchmark

This project contains a benchmark that compares the serialization / deserialization performance of the following avro libraries:

- [Avro4k](https://github.com/avro-kotlin/avro4k/)
- [Jackson Avro](https://github.com/FasterXML/jackson-dataformats-binary/tree/master/avro)
- Coming soon: [Avro](https://avro.apache.org/)

## Results

<details>
<summary>Macbook air M2</summary>

```
Benchmark Mode Cnt Score Error Units
Avro4kClientsBenchmark.read thrpt 2 439983.130 ops/s
Avro4kClientsBenchmark.write thrpt 2 474453.236 ops/s
JacksonAvroClientsBenchmark.read thrpt 2 577757.798 ops/s
JacksonAvroClientsBenchmark.write thrpt 2 649982.820 ops/s
```

For the moment, Jackson Avro is faster than Avro4k because Avro4k is still not doing direct encoding so there is an intermediate generic data step.

</details>

## Run the benchmark locally

Just execute the benchmark:

```shell
../gradlew benchmark
```

You can get the results in the `build/reports/benchmarks/main` directory.

## Other information

Thanks for [@twinprime](https://github.com/twinprime) for this initiative.
Empty file added benchmark/api/benchmark.api
Empty file.
34 changes: 34 additions & 0 deletions benchmark/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
plugins {
java
kotlin("jvm") version libs.versions.kotlin
id("org.jetbrains.kotlinx.benchmark") version "0.4.10"
kotlin("plugin.allopen") version libs.versions.kotlin
kotlin("plugin.serialization") version libs.versions.kotlin
}

allOpen {
annotation("org.openjdk.jmh.annotations.State")
}

benchmark {
configurations {
named("main") {
reportFormat = "text"
}
}
targets {
register("main")
}
}

dependencies {
implementation("org.apache.commons:commons-lang3:3.14.0")
implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:0.4.10")

val jacksonVersion = "2.17.0"
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jacksonVersion")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-avro:$jacksonVersion")

implementation(project(":"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.github.avrokotlin.benchmark

import com.github.avrokotlin.avro4k.Avro
import com.github.avrokotlin.avro4k.decodeFromByteArray
import com.github.avrokotlin.avro4k.encodeToByteArray
import kotlinx.benchmark.Benchmark

internal class Avro4kClientsStaticReadBenchmark {
fun main() {
Avro4kClientsBenchmark().apply {
initTestData()
for (i in 0 until 1000000) {
if (i % 100000 == 0) println("Iteration $i")
read()
}
}
}
}

internal class Avro4kClientsStaticWriteBenchmark {
fun main() {
Avro4kClientsBenchmark().apply {
initTestData()
for (i in 0 until 1000000) {
if (i % 100000 == 0) println("Iteration $i")
write()
}
}
}
}

internal class Avro4kClientsBenchmark : SerializationBenchmark() {
lateinit var data: ByteArray
var writeMode = false

override fun prepareBinaryData() {
data = Avro.encodeToByteArray(clients)
}

@Benchmark
fun read() {
if (writeMode) writeMode = false
Avro.decodeFromByteArray<Clients>(data)
}

@Benchmark
fun write() {
if (!writeMode) writeMode = true
Avro.encodeToByteArray(clients)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.github.avrokotlin.benchmark

import com.github.avrokotlin.avro4k.AvroDecimal
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.math.BigDecimal
import java.time.Instant
import java.time.LocalDate
import java.util.*

@Serializable
internal data class Clients(
var clients: MutableList<Client> = mutableListOf()
)
@Serializable
internal data class Client(
var id: Long = 0,
var index: Int = 0,
@Contextual
var guid: UUID? = null,
var isActive: Boolean = false,
@Contextual
@AvroDecimal(5,10)
var balance: BigDecimal? = null,
var picture: ByteArray? = null,
var age: Int = 0,
var eyeColor: EyeColor? = null,
var name: String? = null,
var gender: String? = null,
var company: String? = null,
var emails: Array<String> = emptyArray(),
var phones: LongArray = LongArray(0),
var address: String? = null,
var about: String? = null,
@Contextual
var registered: LocalDate? = null,
var latitude : Double = 0.0,
var longitude: Double = 0.0,
var tags: List<String> = emptyList(),
var partners: List<Partner> = emptyList(),
)

@Serializable
internal enum class EyeColor {
BROWN,
BLUE,
GREEN;
}
@Serializable
internal class Partner(
val id: Long = 0,
val name: String? = null,
@Contextual
val since: Instant? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.github.avrokotlin.benchmark

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.ObjectReader
import com.fasterxml.jackson.databind.ObjectWriter
import com.fasterxml.jackson.dataformat.avro.AvroFactory
import com.fasterxml.jackson.dataformat.avro.AvroMapper
import com.fasterxml.jackson.dataformat.avro.jsr310.AvroJavaTimeModule
import com.fasterxml.jackson.dataformat.avro.schema.AvroSchemaGenerator
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import kotlinx.benchmark.Benchmark
import kotlinx.benchmark.Setup

internal class JacksonAvroClientsBenchmark : SerializationBenchmark() {
lateinit var writer: ObjectWriter
lateinit var reader: ObjectReader

lateinit var data: ByteArray
var writeMode = false

@Setup
fun setup() {
val schemaMapper = ObjectMapper(AvroFactory())
.registerKotlinModule()
.registerModule(AvroJavaTimeModule())
writer = Clients::class.java.createWriter(schemaMapper)
reader = Clients::class.java.createReader(schemaMapper)
}

override fun prepareBinaryData() {
data = writer.writeValueAsBytes(clients)
}

@Benchmark
fun read() {
if (writeMode) writeMode = false
reader.readValue<Clients>(data)
}

@Benchmark
fun write() {
if (!writeMode) writeMode = true
writer.writeValueAsBytes(clients)
}
}

private fun <T> Class<T>.createWriter(schemaMapper: ObjectMapper): ObjectWriter {
val gen = AvroSchemaGenerator()
schemaMapper.acceptJsonFormatVisitor(this, gen)

val mapper = AvroMapper().registerModule(kotlinModule()).registerModule(AvroJavaTimeModule())
return mapper.writer(gen.generatedSchema)
}

private fun <T> Class<T>.createReader(schemaMapper: ObjectMapper): ObjectReader {
val gen = AvroSchemaGenerator()
schemaMapper.acceptJsonFormatVisitor(this, gen)

val mapper = AvroMapper().registerModule(kotlinModule()).registerModule(AvroJavaTimeModule())
return mapper.reader(gen.generatedSchema).forType(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.github.avrokotlin.benchmark

import com.github.avrokotlin.benchmark.gen.ClientsGenerator
import kotlinx.benchmark.*
import java.util.concurrent.TimeUnit


@State(Scope.Benchmark)
@Warmup(iterations = 3)
@BenchmarkMode(Mode.Throughput)
@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS)
internal abstract class SerializationBenchmark {
lateinit var clients: Clients

@Setup
fun initTestData(){
clients = Clients()
ClientsGenerator.populate(clients, 1000)
prepareBinaryData()
}

abstract fun prepareBinaryData()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.github.avrokotlin.benchmark.gen

import com.github.avrokotlin.benchmark.Client
import com.github.avrokotlin.benchmark.Clients
import com.github.avrokotlin.benchmark.EyeColor
import com.github.avrokotlin.benchmark.Partner

import java.time.LocalDate
import java.time.OffsetDateTime
import java.time.ZoneOffset

internal object ClientsGenerator {
fun populate(obj: Clients, size: Int): Int {
var approxSize = 14 // {'clients':[]}

obj.clients = mutableListOf()
while (approxSize < size) {
approxSize += appendClient(obj, size - approxSize)
approxSize += 1 // ,
}
return approxSize
}

private fun appendClient(uc: Clients, sizeAvailable: Int): Int {
var expectedSize = 2 // {}
val u = Client()
u.id = Math.abs(RandomUtils.nextLong())
expectedSize += 9 + u.id.toString().length // ,'id':''
u.index = (RandomUtils.nextInt(0, Int.MAX_VALUE))
expectedSize += 11 + u.index.toString().length // ,'index':''
u.guid = (RandomUtils.nextUUID())
expectedSize += 10 + 36 // ,'guid':''
u.isActive = (RandomUtils.nextInt(0, 2) == 1)
expectedSize += 17 + if (u.isActive) 4 else 5 // ,'isActive':''
u.balance = (RandomUtils.randomBigDecimal())
expectedSize += 16 + u.balance!!.toPlainString().length // ,'balance':''
u.picture = RandomUtils.randomBytes(4048)
expectedSize += 16 + u.picture!!.size // ,'picture':''
u.age = (RandomUtils.nextInt(0, 100))
expectedSize += 9 + u.age.toString().length // ,'age':''
u.eyeColor = (EyeColor.entries[RandomUtils.nextInt(3)])
expectedSize += 17 + u.eyeColor!!.name.length // ,'eyeColor':''
u.name = (RandomUtils.randomAlphanumeric(20))
expectedSize += 10 + u.name!!.length // ,'name':''
u.gender = (RandomUtils.randomAlphanumeric(20))
expectedSize += 12 + u.gender!!.length // ,'gender':''
u.company = (RandomUtils.randomAlphanumeric(20))
expectedSize += 13 + u.company!!.length // ,'company':''
u.emails = (RandomUtils.stringArray(RandomUtils.nextInt(10), 20))
var calcSize = 0
for (e in u.emails) {
calcSize += 3 + e.length
}
expectedSize += 11 + calcSize // ,'email':''
u.phones = (RandomUtils.longArray(RandomUtils.nextInt(10)))
calcSize = 0
for (p in u.phones) {
calcSize += 1 + p.toString().length
}
expectedSize += 11 + calcSize // ,'phone':''
u.address = (RandomUtils.randomAlphanumeric(20))
expectedSize += 13 + u.address!!.length // ,'address':''
u.about = (RandomUtils.randomAlphanumeric(20))
expectedSize += 11 + u.about!!.length // ,'about':''
u.registered = (
LocalDate.of(
1900 + RandomUtils.nextInt(110),
1 + RandomUtils.nextInt(12),
1 + RandomUtils.nextInt(28)
)
)
expectedSize += 16 + 10 // ,'registered':''
u.latitude = (RandomUtils.nextDouble(0.0, 90.0))
expectedSize += 14 + u.latitude.toString().length // ,'latitude':''
u.longitude = (RandomUtils.nextDouble(0.0, 180.0))
expectedSize += 15 + u.longitude.toString().length // ,'longitude':''
val tags = mutableListOf<String>()
expectedSize += 10 // ,'tags':[]
val nTags: Int = RandomUtils.nextInt(0, 50)
for (i in 0 until nTags) {
if (expectedSize > sizeAvailable) {
break
}
val t: String = RandomUtils.randomAlphanumeric(10)
tags.add(t)
expectedSize += t.length // '',
}
u.tags = tags
val nPartners: Int = RandomUtils.nextInt(0, 30)
val partners = mutableListOf<Partner>()
expectedSize += 13 // ,'partners':[]
for (i in 0 until nPartners) {
if (expectedSize > sizeAvailable) {
break
}
val id: Long = RandomUtils.nextLong()
val name: String = RandomUtils.randomAlphabetic(30)
val at = OffsetDateTime.of(
1900 + RandomUtils.nextInt(110),
1 + RandomUtils.nextInt(12),
1 + RandomUtils.nextInt(28),
RandomUtils.nextInt(24),
RandomUtils.nextInt(60),
RandomUtils.nextInt(60),
RandomUtils.nextInt(1000000000),
ZoneOffset.UTC
).toInstant()
partners.add(Partner(id, name, at))
expectedSize += id.toString().length + name.length + 50 // {'id':'','name':'','since':''},
}
u.partners = partners
uc.clients.add(u)
return expectedSize
}
}

Loading

0 comments on commit a0d4272

Please sign in to comment.