From e5f513702ded191edee5e1b443b68addfdd2c7eb Mon Sep 17 00:00:00 2001 From: Chuckame Date: Tue, 25 Jun 2024 18:57:50 +0200 Subject: [PATCH 1/3] benchmark: Added apache avro lib generic<->binary benchmark improve benchmark --- benchmark/README.md | 54 ++-- benchmark/api/benchmark.api | 19 +- benchmark/build.gradle.kts | 7 +- .../benchmark/ApacheAvroReflectBenchmark.kt | 54 ++++ .../avrokotlin/benchmark/Avro4kBenchmark.kt | 35 +++ .../benchmark/Avro4kClientsBenchmark.kt | 53 ---- .../Avro4kGenericWithApacheAvroBenchmark.kt | 59 ++++ .../github/avrokotlin/benchmark/Clients.kt | 76 ----- .../benchmark/JacksonAvroBenchmark.kt | 62 +++++ .../benchmark/JacksonAvroClientsBenchmark.kt | 62 ----- .../avrokotlin/benchmark/ManualProfiling.kt | 26 ++ .../benchmark/SerializationBenchmark.kt | 18 +- .../benchmark/gen/ClientsGenerator.kt | 127 --------- .../avrokotlin/benchmark/internal/Clients.kt | 101 +++++++ .../benchmark/internal/ClientsGenerator.kt | 60 ++++ .../{gen => internal}/RandomUtils.kt | 24 +- .../avrokotlin/benchmark/internal/avro4k.json | 245 +++++++++++++++++ .../benchmark/internal/jackson.json | 260 ++++++++++++++++++ 18 files changed, 976 insertions(+), 366 deletions(-) create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ApacheAvroReflectBenchmark.kt create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kBenchmark.kt delete mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kClientsBenchmark.kt create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kGenericWithApacheAvroBenchmark.kt delete mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Clients.kt create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/JacksonAvroBenchmark.kt delete mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/JacksonAvroClientsBenchmark.kt create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ManualProfiling.kt delete mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/gen/ClientsGenerator.kt create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/Clients.kt create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/ClientsGenerator.kt rename benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/{gen => internal}/RandomUtils.kt (84%) create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/avro4k.json create mode 100644 benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/jackson.json diff --git a/benchmark/README.md b/benchmark/README.md index b64b2ac0..15a32391 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -4,39 +4,43 @@ This project contains a benchmark that compares the serialization / deserializat - [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/) +- [Avro (using ReflectData)](https://avro.apache.org/) + +Each benchmark is executed with the following configuration: +- Reading from a prepared byte array +- Writing to a null output stream +- All with the exact same schema generated by avro4k +- Generating a maximum of use cases: + - unions (including nullable fields) + - arrays + - records + - enums + - primitives (string, int, float, double, boolean) + - logical types (date, timestamp-millis, char, uuid) +- not benchmarking uuid as jackson uses a different representation (fixed) than avro4k and apache avro (string) ## Results -
-Macbook air M2 - without direct encoding +Computer: Macbook air M2 ``` -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 +Benchmark Mode Cnt Score Error Units Relative Difference (%) +ApacheAvroReflectBenchmark.read thrpt 5 20073.322 ± 878.268 ops/s +1.70% +Avro4kBenchmark.read thrpt 5 19738.061 ± 930.026 ops/s 0.00% +Avro4kGenericWithApacheAvroBenchmark.read thrpt 5 7538.287 ± 112.690 ops/s -61.78% + +Avro4kBenchmark.write thrpt 5 40780.118 ± 4136.921 ops/s 0.00% +ApacheAvroReflectBenchmark.write thrpt 5 37632.786 ± 117.940 ops/s -7.72% +JacksonAvroBenchmark.write thrpt 5 30544.663 ± 1004.357 ops/s -25.08% +Avro4kGenericWithApacheAvroBenchmark.write thrpt 5 21088.555 ± 1280.548 ops/s -48.31% ``` -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. +> [!WARNING] +> JacksonAvroBenchmark.read is failing because of a bug in the library when combining kotlin and avro format. -
- -
- -
-Macbook air M2 - with direct encoding but without direct decoding - -``` -Benchmark Mode Cnt Score Error Units -Avro4kClientsBenchmark.read thrpt 2 471489.689 ops/s -Avro4kClientsBenchmark.write thrpt 2 686791.337 ops/s -JacksonAvroClientsBenchmark.read thrpt 2 513425.052 ops/s -JacksonAvroClientsBenchmark.write thrpt 2 627412.940 ops/s -``` - -
+> [!NOTE] +> To add the relative difference, just ask to chatgpt "can you add another column in this benchmark that indicates the relative difference in percent regarding +> Avro4kDirectBenchmark:" ## Run the benchmark locally diff --git a/benchmark/api/benchmark.api b/benchmark/api/benchmark.api index e469b3eb..505100f0 100644 --- a/benchmark/api/benchmark.api +++ b/benchmark/api/benchmark.api @@ -1,8 +1,21 @@ -public abstract interface class com/github/avrokotlin/benchmark/Partner { - public static final field Companion Lcom/github/avrokotlin/benchmark/Partner$Companion; +public final class com/github/avrokotlin/benchmark/internal/CharJacksonDeserializer : com/fasterxml/jackson/databind/deser/std/StdDeserializer { + public fun ()V + public fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Character; + public synthetic fun deserialize (Lcom/fasterxml/jackson/core/JsonParser;Lcom/fasterxml/jackson/databind/DeserializationContext;)Ljava/lang/Object; } -public final class com/github/avrokotlin/benchmark/Partner$Companion { +public final class com/github/avrokotlin/benchmark/internal/CharJacksonSerializer : com/fasterxml/jackson/databind/ser/std/StdSerializer { + public fun ()V + public fun acceptJsonFormatVisitor (Lcom/fasterxml/jackson/databind/jsonFormatVisitors/JsonFormatVisitorWrapper;Lcom/fasterxml/jackson/databind/JavaType;)V + public fun serialize (Ljava/lang/Character;Lcom/fasterxml/jackson/core/JsonGenerator;Lcom/fasterxml/jackson/databind/SerializerProvider;)V + public synthetic fun serialize (Ljava/lang/Object;Lcom/fasterxml/jackson/core/JsonGenerator;Lcom/fasterxml/jackson/databind/SerializerProvider;)V +} + +public abstract interface class com/github/avrokotlin/benchmark/internal/Partner { + public static final field Companion Lcom/github/avrokotlin/benchmark/internal/Partner$Companion; +} + +public final class com/github/avrokotlin/benchmark/internal/Partner$Companion { public final fun serializer ()Lkotlinx/serialization/KSerializer; } diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts index 7ea41eac..5a4e71fb 100644 --- a/benchmark/build.gradle.kts +++ b/benchmark/build.gradle.kts @@ -6,12 +6,17 @@ plugins { id("org.jetbrains.kotlinx.benchmark") version "0.4.11" kotlin("plugin.allopen") version libs.versions.kotlin kotlin("plugin.serialization") version libs.versions.kotlin + kotlin("plugin.noarg") version libs.versions.kotlin } allOpen { annotation("org.openjdk.jmh.annotations.State") } +noArg { + annotation("kotlinx.serialization.Serializable") +} + benchmark { configurations { named("main") { @@ -30,7 +35,7 @@ dependencies { implementation("org.apache.commons:commons-lang3:3.14.0") implementation("org.jetbrains.kotlinx:kotlinx-benchmark-runtime:0.4.11") - val jacksonVersion = "2.17.0" + val jacksonVersion = "2.17.1" 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") diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ApacheAvroReflectBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ApacheAvroReflectBenchmark.kt new file mode 100644 index 00000000..588dd35e --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ApacheAvroReflectBenchmark.kt @@ -0,0 +1,54 @@ +package com.github.avrokotlin.benchmark + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.benchmark.internal.Clients +import kotlinx.benchmark.Benchmark +import org.apache.avro.Conversions +import org.apache.avro.data.TimeConversions +import org.apache.avro.io.DatumReader +import org.apache.avro.io.DatumWriter +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.Encoder +import org.apache.avro.io.EncoderFactory +import org.apache.avro.reflect.ReflectData +import java.io.ByteArrayInputStream +import java.io.OutputStream + +internal class ApacheAvroReflectBenchmark : SerializationBenchmark() { + lateinit var writer: DatumWriter + lateinit var encoder: Encoder + lateinit var reader: DatumReader + + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + ReflectData.get().addLogicalTypeConversion(Conversions.UUIDConversion()) + ReflectData.get().addLogicalTypeConversion(Conversions.DecimalConversion()) + ReflectData.get().addLogicalTypeConversion(TimeConversions.DateConversion()) + ReflectData.get().addLogicalTypeConversion(TimeConversions.TimestampMillisConversion()) + + writer = ReflectData.get().createDatumWriter(schema) as DatumWriter + encoder = EncoderFactory.get().directBinaryEncoder(OutputStream.nullOutputStream(), null) + + reader = ReflectData.get().createDatumReader(schema) as DatumReader + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + if (writeMode) writeMode = false + val decoder = DecoderFactory.get().directBinaryDecoder(ByteArrayInputStream(data), null) + reader.read(null, decoder) + } + + @Benchmark + fun write() { + if (!writeMode) writeMode = true + writer.write(clients, encoder) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kBenchmark.kt new file mode 100644 index 00000000..ba16ed45 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kBenchmark.kt @@ -0,0 +1,35 @@ +package com.github.avrokotlin.benchmark + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.decodeFromByteArray +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.avro4k.encodeToStream +import com.github.avrokotlin.benchmark.internal.Clients +import kotlinx.benchmark.Benchmark +import kotlinx.serialization.ExperimentalSerializationApi +import java.io.OutputStream + +internal class Avro4kBenchmark : SerializationBenchmark() { + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + if (writeMode) writeMode = false + Avro.decodeFromByteArray(schema, data) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun write() { + if (!writeMode) writeMode = true + Avro.encodeToStream(schema, clients, OutputStream.nullOutputStream()) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kClientsBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kClientsBenchmark.kt deleted file mode 100644 index d92e54a1..00000000 --- a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kClientsBenchmark.kt +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.avrokotlin.benchmark - -import com.github.avrokotlin.avro4k.Avro -import kotlinx.benchmark.Benchmark -import kotlinx.serialization.decodeFromByteArray -import kotlinx.serialization.encodeToByteArray - -internal object Avro4kClientsStaticReadBenchmark { - @JvmStatic - fun main(vararg args: String) { - Avro4kClientsBenchmark().apply { - initTestData() - for (i in 0 until 1000000) { - if (i % 100000 == 0) println("Iteration $i") - read() - } - } - } -} - -internal object Avro4kClientsStaticWriteBenchmark { - @JvmStatic - fun main(vararg args: String) { - 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(data) - } - - @Benchmark - fun write() { - if (!writeMode) writeMode = true - Avro.encodeToByteArray(clients) - } -} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kGenericWithApacheAvroBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kGenericWithApacheAvroBenchmark.kt new file mode 100644 index 00000000..f18f30bc --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Avro4kGenericWithApacheAvroBenchmark.kt @@ -0,0 +1,59 @@ +package com.github.avrokotlin.benchmark + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.decodeFromGenericData +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.avro4k.encodeToGenericData +import com.github.avrokotlin.benchmark.internal.Clients +import kotlinx.benchmark.Benchmark +import kotlinx.serialization.ExperimentalSerializationApi +import org.apache.avro.Conversions +import org.apache.avro.generic.GenericData +import org.apache.avro.io.DatumReader +import org.apache.avro.io.DatumWriter +import org.apache.avro.io.DecoderFactory +import org.apache.avro.io.Encoder +import org.apache.avro.io.EncoderFactory +import java.io.ByteArrayInputStream +import java.io.OutputStream + +internal class Avro4kGenericWithApacheAvroBenchmark : SerializationBenchmark() { + lateinit var writer: DatumWriter + lateinit var encoder: Encoder + lateinit var reader: DatumReader + + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + GenericData.get().addLogicalTypeConversion(Conversions.DecimalConversion()) +// GenericData.get().addLogicalTypeConversion(TimeConversions.DateConversion()) +// GenericData.get().addLogicalTypeConversion(TimeConversions.TimestampMillisConversion()) + + writer = GenericData.get().createDatumWriter(schema) as DatumWriter + encoder = EncoderFactory.get().directBinaryEncoder(OutputStream.nullOutputStream(), null) + + reader = GenericData.get().createDatumReader(schema) as DatumReader + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun read() { + if (writeMode) writeMode = false + val decoder = DecoderFactory.get().directBinaryDecoder(ByteArrayInputStream(data), null) + val genericData = reader.read(null, decoder) + Avro { validateSerialization = true }.decodeFromGenericData(schema, genericData) + } + + @OptIn(ExperimentalSerializationApi::class) + @Benchmark + fun write() { + if (!writeMode) writeMode = true + val genericData = Avro.encodeToGenericData(schema, clients) + writer.write(genericData, encoder) + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Clients.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Clients.kt deleted file mode 100644 index 9be39855..00000000 --- a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/Clients.kt +++ /dev/null @@ -1,76 +0,0 @@ -@file:OptIn(ExperimentalSerializationApi::class) - -package com.github.avrokotlin.benchmark - -import com.github.avrokotlin.avro4k.AvroDecimal -import java.math.BigDecimal -import java.time.Instant -import java.time.LocalDate -import java.util.UUID -import kotlinx.serialization.Contextual -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.Serializable - -@Serializable -internal data class Clients( - var clients: MutableList = 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 = 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 = emptyList(), - var partners: List = emptyList(), -) - -@Serializable -internal enum class EyeColor { - BROWN, - BLUE, - GREEN; -} - -@Serializable -sealed interface Partner - -@Serializable -internal class GoodPartner( - val id: Long = 0, - val name: String? = null, - @Contextual - val since: Instant? = null -) : Partner - -@Serializable -internal class BadPartner( - val id: Long = 0, - val name: String? = null, - @Contextual - val since: Instant? = null -) : Partner - -@Serializable -internal enum class Stranger : Partner { - KNOWN_STRANGER, - UNKNOWN_STRANGER -} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/JacksonAvroBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/JacksonAvroBenchmark.kt new file mode 100644 index 00000000..2336397d --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/JacksonAvroBenchmark.kt @@ -0,0 +1,62 @@ +package com.github.avrokotlin.benchmark + +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.ObjectReader +import com.fasterxml.jackson.databind.ObjectWriter +import com.fasterxml.jackson.dataformat.avro.AvroMapper +import com.fasterxml.jackson.dataformat.avro.AvroSchema +import com.fasterxml.jackson.dataformat.avro.jsr310.AvroJavaTimeModule +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.encodeToByteArray +import com.github.avrokotlin.benchmark.internal.Clients +import kotlinx.benchmark.Benchmark +import java.io.OutputStream + + +internal class JacksonAvroBenchmark : SerializationBenchmark() { + lateinit var writer: ObjectWriter + lateinit var reader: ObjectReader + + lateinit var data: ByteArray + var writeMode = false + + override fun setup() { + writer = Clients::class.java.createWriter() + reader = Clients::class.java.createReader() + } + + override fun prepareBinaryData() { + data = Avro.encodeToByteArray(schema, clients) + } + + @Benchmark + fun read() { + if (writeMode) writeMode = false + reader.readValue(data) + } + + @Benchmark + fun write() { + if (!writeMode) writeMode = true + writer.writeValue(OutputStream.nullOutputStream(), clients) + } + + private fun Class.createWriter(): ObjectWriter { + val mapper = avroMapper() + + return mapper.writer(AvroSchema(schema)).forType(this) + } + + private fun Class.createReader(): ObjectReader { + val mapper = avroMapper() + + return mapper.reader(AvroSchema(schema)).forType(this) + } + + private fun avroMapper(): ObjectMapper = AvroMapper() + .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) + .registerKotlinModule() + .registerModule(AvroJavaTimeModule()) +} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/JacksonAvroClientsBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/JacksonAvroClientsBenchmark.kt deleted file mode 100644 index e93b589d..00000000 --- a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/JacksonAvroClientsBenchmark.kt +++ /dev/null @@ -1,62 +0,0 @@ -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(data) - } - - @Benchmark - fun write() { - if (!writeMode) writeMode = true - writer.writeValueAsBytes(clients) - } -} - -private fun Class.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 Class.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) -} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ManualProfiling.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ManualProfiling.kt new file mode 100644 index 00000000..463f2f2d --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/ManualProfiling.kt @@ -0,0 +1,26 @@ +package com.github.avrokotlin.benchmark + +internal object ManualProfilingWrite { + @JvmStatic + fun main(vararg args: String) { + Avro4kBenchmark().apply { + initTestData() + for (i in 0 until 100_000) { + if (i % 1_000 == 0) println("Iteration $i") + write() + } + } + } +} +internal object ManualProfilingRead { + @JvmStatic + fun main(vararg args: String) { + Avro4kBenchmark().apply { + initTestData() + for (i in 0 until 100_000) { + if (i % 1_000 == 0) println("Iteration $i") + read() + } + } + } +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/SerializationBenchmark.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/SerializationBenchmark.kt index f584dcdc..48fff619 100644 --- a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/SerializationBenchmark.kt +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/SerializationBenchmark.kt @@ -1,23 +1,29 @@ package com.github.avrokotlin.benchmark -import com.github.avrokotlin.benchmark.gen.ClientsGenerator +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.schema +import com.github.avrokotlin.benchmark.internal.Clients +import com.github.avrokotlin.benchmark.internal.ClientsGenerator import kotlinx.benchmark.* import java.util.concurrent.TimeUnit @State(Scope.Benchmark) -@Warmup(iterations = 3) +@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) @BenchmarkMode(Mode.Throughput) -@Measurement(iterations = 2, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 3, timeUnit = TimeUnit.SECONDS) internal abstract class SerializationBenchmark { lateinit var clients: Clients + val schema = Avro.schema() @Setup - fun initTestData(){ - clients = Clients() - ClientsGenerator.populate(clients, 1000) + fun initTestData() { + setup() + clients = ClientsGenerator.generate(15) prepareBinaryData() } + abstract fun setup() + abstract fun prepareBinaryData() } \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/gen/ClientsGenerator.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/gen/ClientsGenerator.kt deleted file mode 100644 index 72cfff2a..00000000 --- a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/gen/ClientsGenerator.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.github.avrokotlin.benchmark.gen - -import com.github.avrokotlin.benchmark.BadPartner -import com.github.avrokotlin.benchmark.Client -import com.github.avrokotlin.benchmark.Clients -import com.github.avrokotlin.benchmark.EyeColor -import com.github.avrokotlin.benchmark.GoodPartner -import com.github.avrokotlin.benchmark.Partner -import com.github.avrokotlin.benchmark.Stranger - -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() - 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() - 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( - when (RandomUtils.nextInt(4)) { - 0 -> GoodPartner(id, name, at) - 1 -> BadPartner(id, name, at) - 2 -> if (RandomUtils.nextBoolean()) Stranger.KNOWN_STRANGER else Stranger.UNKNOWN_STRANGER - 3 -> null - else -> throw IllegalStateException("Unexpected value") - } - ) - expectedSize += id.toString().length + name.length + 50 // {'id':'','name':'','since':''}, - } - u.partners = partners - uc.clients.add(u) - return expectedSize - } -} - diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/Clients.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/Clients.kt new file mode 100644 index 00000000..45b4c9a7 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/Clients.kt @@ -0,0 +1,101 @@ +package com.github.avrokotlin.benchmark.internal + +import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.JavaType +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper +import com.fasterxml.jackson.databind.ser.std.StdSerializer +import com.github.avrokotlin.avro4k.serializer.BigDecimalAsStringSerializer +import com.github.avrokotlin.avro4k.serializer.InstantSerializer +import com.github.avrokotlin.avro4k.serializer.LocalDateSerializer +import kotlinx.serialization.Serializable +import java.math.BigDecimal +import java.time.Instant +import java.time.LocalDate + +@Serializable +internal data class Clients( + val clients: List +) + +class CharJacksonSerializer : StdSerializer(Char::class.java) { + override fun serialize(value: Char?, gen: JsonGenerator, provider: SerializerProvider) { + value?.code?.let { gen.writeNumber(it) } ?: gen.writeNull() + } + + override fun acceptJsonFormatVisitor(visitor: JsonFormatVisitorWrapper, typeHint: JavaType) { + visitor.expectIntegerFormat(typeHint).numberType(JsonParser.NumberType.INT) + } +} + +class CharJacksonDeserializer : StdDeserializer(Char::class.java) { + override fun deserialize(p0: JsonParser, p1: com.fasterxml.jackson.databind.DeserializationContext): Char { + return p0.intValue.toChar() + } +} + +@Serializable +internal data class Client( + val id: Long = 0, + val index: Int = 0, + val isActive: Boolean = false, + @Serializable(with = BigDecimalAsStringSerializer::class) + @JsonFormat(shape = JsonFormat.Shape.STRING) + val balance: BigDecimal? = null, + val picture: ByteArray? = null, + val age: Int = 0, + val eyeColor: EyeColor? = null, + val name: String? = null, + @JsonSerialize(using = CharJacksonSerializer::class) + @JsonDeserialize(using = CharJacksonDeserializer::class) + val gender: Char? = null, + val company: String? = null, + val emails: Array = emptyArray(), + val phones: LongArray = LongArray(0), + val address: String? = null, + val about: String? = null, + @Serializable(with = LocalDateSerializer::class) + val registered: LocalDate? = null, + val latitude: Double = 0.0, + val longitude: Float = 0.0f, + val tags: List = emptyList(), + val partner: Partner, + val map: Map = emptyMap(), +) + +@Serializable +internal enum class EyeColor { + BROWN, + BLUE, + GREEN; +} + +@Serializable +sealed interface Partner + +@Serializable +internal class GoodPartner( + val id: Long = 0, + val name: String? = null, + @Serializable(with = InstantSerializer::class) + val since: Instant? = null +) : Partner + +@Serializable +internal class BadPartner( + val id: Long = 0, + val name: String? = null, + @Serializable(with = InstantSerializer::class) + val since: Instant? = null +) : Partner + +@Serializable +internal enum class Stranger : Partner { + KNOWN_STRANGER, + UNKNOWN_STRANGER +} diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/ClientsGenerator.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/ClientsGenerator.kt new file mode 100644 index 00000000..6180eb49 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/ClientsGenerator.kt @@ -0,0 +1,60 @@ +package com.github.avrokotlin.benchmark.internal + +import java.time.Instant +import java.time.LocalDate +import kotlin.math.absoluteValue + +internal object ClientsGenerator { + fun generate(size: Int) = Clients( + clients = buildList { + repeat(size) { + add(newClient()) + } + } + ) + + private fun newClient(): Client { + val u = Client( + partner = when (RandomUtils.nextInt(3)) { + 0 -> GoodPartner(RandomUtils.nextLong(), RandomUtils.randomAlphabetic(30), Instant.ofEpochMilli(RandomUtils.nextLong())) + 1 -> BadPartner(RandomUtils.nextLong(), RandomUtils.randomAlphabetic(30), Instant.ofEpochMilli(RandomUtils.nextLong().absoluteValue)) + 2 -> if (RandomUtils.nextBoolean()) Stranger.KNOWN_STRANGER else Stranger.UNKNOWN_STRANGER + else -> throw IllegalStateException("Unexpected value") + }, + id = RandomUtils.nextLong().absoluteValue, + index = RandomUtils.nextInt(0, Int.MAX_VALUE), + isActive = RandomUtils.nextBoolean(), + balance = RandomUtils.randomBigDecimal(), + picture = RandomUtils.randomBytes(4048), + age = RandomUtils.nextInt(0, 100), + eyeColor = EyeColor.entries[RandomUtils.nextInt(3)], + name = RandomUtils.randomAlphanumeric(20), + gender = if (RandomUtils.nextBoolean()) 'M' else 'F', + company = RandomUtils.randomAlphanumeric(20), + emails = RandomUtils.stringArray(RandomUtils.nextInt(5, 10), 10), + phones = RandomUtils.longArray(RandomUtils.nextInt(5, 10)), + address = RandomUtils.randomAlphanumeric(20), + about = RandomUtils.randomAlphanumeric(20), + registered = + LocalDate.of( + 1900 + RandomUtils.nextInt(110), + 1 + RandomUtils.nextInt(12), + 1 + RandomUtils.nextInt(28) + ), + latitude = RandomUtils.nextDouble(0.0, 90.0), + longitude = RandomUtils.nextFloat(0.0f, 180.0f), + tags = buildList { + repeat(RandomUtils.nextInt(5, 25)) { + add(RandomUtils.randomAlphanumeric(10)) + } + }, + map = buildMap { + repeat(10) { + put(RandomUtils.randomAlphanumeric(10), RandomUtils.randomAlphanumeric(10)) + } + } + ) + return u + } +} + diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/gen/RandomUtils.kt b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/RandomUtils.kt similarity index 84% rename from benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/gen/RandomUtils.kt rename to benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/RandomUtils.kt index 2c8b36a6..367d5b6f 100644 --- a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/gen/RandomUtils.kt +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/RandomUtils.kt @@ -1,25 +1,15 @@ -package com.github.avrokotlin.benchmark.gen +package com.github.avrokotlin.benchmark.internal import org.apache.commons.lang3.RandomStringUtils import java.math.BigDecimal import java.math.RoundingMode -import java.util.* +import java.util.UUID import kotlin.math.abs import kotlin.random.Random import kotlin.random.asJavaRandom internal object RandomUtils { - private val RANDOM: Random - - init { - val seedStr = System.getenv("SEED") - val seed = - try { - seedStr?.toLong() - } catch (e: NumberFormatException) { null }?: System.nanoTime() - println("Using SEED=$seed as seed for Random") - RANDOM = Random(seed) - } + private val RANDOM: Random = Random(139793881379292435L) fun randomAlphabetic(count: Int): String { return random(count, true, false) @@ -100,4 +90,12 @@ internal object RandomUtils { startInclusive } else startInclusive + (endInclusive - startInclusive) * RANDOM.nextDouble() } + + fun nextFloat(startInclusive: Float, endInclusive: Float): Float { + assert(endInclusive >= startInclusive) + assert(startInclusive >= 0) + return if (startInclusive == endInclusive) { + startInclusive + } else startInclusive + (endInclusive - startInclusive) * RANDOM.nextFloat() + } } diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/avro4k.json b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/avro4k.json new file mode 100644 index 00000000..188e8495 --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/avro4k.json @@ -0,0 +1,245 @@ +{ + "type": "record", + "name": "Clients", + "namespace": "com.github.avrokotlin.benchmark.internal", + "fields": [ + { + "name": "clients", + "type": { + "type": "array", + "items": { + "type": "record", + "name": "Client", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "index", + "type": "int" + }, + { + "name": "guid", + "type": [ + "null", + { + "type": "string", + "logicalType": "uuid" + } + ], + "default": null + }, + { + "name": "isActive", + "type": "boolean" + }, + { + "name": "balance", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "picture", + "type": [ + "null", + "bytes" + ], + "default": null + }, + { + "name": "age", + "type": "int" + }, + { + "name": "eyeColor", + "type": [ + "null", + { + "type": "enum", + "name": "EyeColor", + "symbols": [ + "BROWN", + "BLUE", + "GREEN" + ] + } + ], + "default": null + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "gender", + "type": [ + "null", + { + "type": "int", + "logicalType": "char" + } + ], + "default": null + }, + { + "name": "company", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "emails", + "type": { + "type": "array", + "items": "string" + } + }, + { + "name": "phones", + "type": { + "type": "array", + "items": "long" + } + }, + { + "name": "address", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "about", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "registered", + "type": [ + "null", + { + "type": "int", + "logicalType": "date" + } + ], + "default": null + }, + { + "name": "latitude", + "type": "double" + }, + { + "name": "longitude", + "type": "float" + }, + { + "name": "tags", + "type": { + "type": "array", + "items": [ + "null", + "string" + ] + } + }, + { + "name": "partners", + "type": { + "type": "array", + "items": [ + { + "type": "record", + "name": "BadPartner", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "since", + "type": [ + "null", + { + "type": "long", + "logicalType": "timestamp-millis" + } + ], + "default": null + } + ] + }, + { + "type": "record", + "name": "GoodPartner", + "fields": [ + { + "name": "id", + "type": "long" + }, + { + "name": "name", + "type": [ + "null", + "string" + ], + "default": null + }, + { + "name": "since", + "type": [ + "null", + { + "type": "long", + "logicalType": "timestamp-millis" + } + ], + "default": null + } + ] + }, + { + "type": "enum", + "name": "Stranger", + "symbols": [ + "KNOWN_STRANGER", + "UNKNOWN_STRANGER" + ] + } + ] + } + }, + { + "name": "map", + "type": { + "type": "map", + "values": "string" + } + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/jackson.json b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/jackson.json new file mode 100644 index 00000000..eac886bb --- /dev/null +++ b/benchmark/src/main/kotlin/com/github/avrokotlin/benchmark/internal/jackson.json @@ -0,0 +1,260 @@ +{ + "type": "record", + "name": "Clients", + "namespace": "com.github.avrokotlin.benchmark.internal", + "fields": [ + { + "name": "clients", + "type": { + "type": "array", + "items": { + "type": "record", + "name": "Client", + "fields": [ + { + "name": "about", + "type": [ + "null", + "string" + ] + }, + { + "name": "address", + "type": [ + "null", + "string" + ] + }, + { + "name": "age", + "type": { + "type": "int", + "java-class": "java.lang.Integer" + } + }, + { + "name": "balance", + "type": [ + "null", + { + "type": "string", + "java-class": "java.math.BigDecimal" + } + ] + }, + { + "name": "company", + "type": [ + "null", + "string" + ] + }, + { + "name": "emails", + "type": { + "type": "array", + "items": "string", + "java-class": "[Ljava.lang.String;" + } + }, + { + "name": "eyeColor", + "type": [ + "null", + { + "type": "enum", + "name": "EyeColor", + "symbols": [ + "BROWN", + "BLUE", + "GREEN" + ] + } + ] + }, + { + "name": "gender", + "type": [ + "null", + { + "type": "int", + "java-class": "java.lang.Character" + } + ] + }, + { + "name": "guid", + "type": [ + "null", + { + "type": "fixed", + "name": "UUID", + "namespace": "java.util", + "doc": "", + "size": 16 + } + ] + }, + { + "name": "id", + "type": { + "type": "long", + "java-class": "java.lang.Long" + } + }, + { + "name": "index", + "type": { + "type": "int", + "java-class": "java.lang.Integer" + } + }, + { + "name": "isActive", + "type": "boolean" + }, + { + "name": "latitude", + "type": { + "type": "double", + "java-class": "java.lang.Double" + } + }, + { + "name": "longitude", + "type": { + "type": "float", + "java-class": "java.lang.Float" + } + }, + { + "name": "map", + "type": { + "type": "map", + "values": "string" + } + }, + { + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "name": "partners", + "type": { + "type": "array", + "items": [ + { + "type": "record", + "name": "BadPartner", + "fields": [ + { + "name": "id", + "type": { + "type": "long", + "java-class": "java.lang.Long" + } + }, + { + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "name": "since", + "type": [ + "null", + { + "type": "long", + "java-class": "java.time.Instant" + } + ] + } + ] + }, + { + "type": "record", + "name": "GoodPartner", + "fields": [ + { + "name": "id", + "type": { + "type": "long", + "java-class": "java.lang.Long" + } + }, + { + "name": "name", + "type": [ + "null", + "string" + ] + }, + { + "name": "since", + "type": [ + "null", + { + "type": "long", + "java-class": "java.time.Instant" + } + ] + } + ] + }, + { + "type": "enum", + "name": "Stranger", + "symbols": [ + "KNOWN_STRANGER", + "UNKNOWN_STRANGER" + ] + } + ] + } + }, + { + "name": "phones", + "type": { + "type": "array", + "items": "long", + "java-class": "[J" + } + }, + { + "name": "picture", + "type": [ + "null", + { + "type": "bytes", + "java-class": "[B" + } + ] + }, + { + "name": "registered", + "type": [ + "null", + { + "type": "int", + "java-class": "java.time.LocalDate" + } + ] + }, + { + "name": "tags", + "type": { + "type": "array", + "items": "string" + } + } + ] + } + } + } + ] +} \ No newline at end of file From 7793a060ea8da8807a1a9c2b313145ceab1cd0cd Mon Sep 17 00:00:00 2001 From: Chuckame Date: Tue, 25 Jun 2024 18:58:38 +0200 Subject: [PATCH 2/3] little refactor --- .../avro4k/internal/RecordResolver.kt | 2 + .../decoder/AbstractPolymorphicDecoder.kt | 65 +++++++++++++++++++ .../direct/AbstractAvroDirectDecoder.kt | 47 ++++---------- .../direct/CollectionsDirectDecoder.kt | 8 +-- .../decoder/direct/RecordDirectDecoder.kt | 65 +++++++++++-------- .../generic/AbstractAvroGenericDecoder.kt | 5 +- .../generic/PolymorphicGenericDecoder.kt | 45 ++++--------- .../encoder/direct/RecordDirectEncoder.kt | 4 +- .../avrokotlin/avro4k/internal/helpers.kt | 4 ++ 9 files changed, 143 insertions(+), 102 deletions(-) create mode 100644 src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/AbstractPolymorphicDecoder.kt diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/RecordResolver.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/RecordResolver.kt index 78d30de9..ac3bdd45 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/RecordResolver.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/RecordResolver.kt @@ -233,6 +233,8 @@ internal class ClassDescriptorForWriterSchema( */ val encodingSteps: Array, ) { + val hasMissingWriterField by lazy { encodingSteps.any { it is EncodingStep.MissingWriterFieldFailure } } + companion object { val EMPTY = ClassDescriptorForWriterSchema( diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/AbstractPolymorphicDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/AbstractPolymorphicDecoder.kt new file mode 100644 index 00000000..431615e5 --- /dev/null +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/AbstractPolymorphicDecoder.kt @@ -0,0 +1,65 @@ +package com.github.avrokotlin.avro4k.internal.decoder + +import com.github.avrokotlin.avro4k.Avro +import com.github.avrokotlin.avro4k.internal.IllegalIndexedAccessError +import com.github.avrokotlin.avro4k.internal.isNamedSchema +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.SerializationException +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.modules.SerializersModule +import org.apache.avro.Schema + +internal abstract class AbstractPolymorphicDecoder( + protected val avro: Avro, + private val descriptor: SerialDescriptor, + private val schema: Schema, +) : AbstractDecoder() { + final override val serializersModule: SerializersModule + get() = avro.serializersModule + + private lateinit var chosenSchema: Schema + + final override fun decodeString(): String { + return tryFindSerialName()?.also { chosenSchema = it.second }?.first + ?: throw SerializationException("Unknown schema name '${schema.fullName}' for polymorphic type ${descriptor.serialName}. Full schema: $schema") + } + + private fun tryFindSerialName(): Pair? { + val namesAndAliasesToSerialName: Map = avro.polymorphicResolver.getFullNamesAndAliasesToSerialName(descriptor) + return tryFindSerialName(namesAndAliasesToSerialName, schema) + } + + protected abstract fun tryFindSerialNameForUnion( + namesAndAliasesToSerialName: Map, + schema: Schema, + ): Pair? + + protected fun tryFindSerialName( + namesAndAliasesToSerialName: Map, + schema: Schema, + ): Pair? { + if (schema.isUnion) { + return tryFindSerialNameForUnion(namesAndAliasesToSerialName, schema) + } + return ( + namesAndAliasesToSerialName[schema.fullName] + ?: schema.takeIf { it.isNamedSchema() }?.aliases?.firstNotNullOfOrNull { namesAndAliasesToSerialName[it] } + ) + ?.let { it to schema } + } + + final override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + return newDecoder(chosenSchema) + .decodeSerializableValue(deserializer) + } + + abstract fun newDecoder(chosenSchema: Schema): Decoder + + final override fun decodeSequentially() = true + + final override fun decodeElementIndex(descriptor: SerialDescriptor): Int { + throw IllegalIndexedAccessError() + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt index 925dfe34..db440f68 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/AbstractAvroDirectDecoder.kt @@ -16,8 +16,8 @@ import com.github.avrokotlin.avro4k.decodeResolvingDouble import com.github.avrokotlin.avro4k.decodeResolvingFloat import com.github.avrokotlin.avro4k.decodeResolvingInt import com.github.avrokotlin.avro4k.decodeResolvingLong -import com.github.avrokotlin.avro4k.internal.IllegalIndexedAccessError import com.github.avrokotlin.avro4k.internal.UnexpectedDecodeSchemaError +import com.github.avrokotlin.avro4k.internal.decoder.AbstractPolymorphicDecoder import com.github.avrokotlin.avro4k.internal.decoder.direct.AbstractAvroDirectDecoder.SizeGetter import com.github.avrokotlin.avro4k.internal.getElementIndexNullable import com.github.avrokotlin.avro4k.internal.isFullNameOrAliasMatch @@ -33,8 +33,8 @@ import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.descriptors.PolymorphicKind import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.internal.AbstractCollectionSerializer import kotlinx.serialization.modules.SerializersModule import org.apache.avro.Schema @@ -523,42 +523,19 @@ internal abstract class AbstractAvroDirectDecoder( } private class PolymorphicDecoder( - private val avro: Avro, - private val descriptor: SerialDescriptor, - private val schema: Schema, + avro: Avro, + descriptor: SerialDescriptor, + schema: Schema, private val binaryDecoder: org.apache.avro.io.Decoder, -) : AbstractDecoder() { - override val serializersModule: SerializersModule - get() = avro.serializersModule - - private lateinit var chosenSchema: Schema - - override fun decodeString(): String { - chosenSchema = - if (schema.isUnion) { - schema.types[binaryDecoder.readIndex()] - } else { - schema - } - - return tryFindSerialName(chosenSchema) - ?: throw SerializationException("Unknown schema name ${schema.fullName} for polymorphic type ${descriptor.nonNullSerialName}") +) : AbstractPolymorphicDecoder(avro, descriptor, schema) { + override fun tryFindSerialNameForUnion( + namesAndAliasesToSerialName: Map, + schema: Schema, + ): Pair? { + return tryFindSerialName(namesAndAliasesToSerialName, schema.types[binaryDecoder.readIndex()]) } - private fun tryFindSerialName(schema: Schema): String? { - val namesAndAliasesToSerialName = avro.polymorphicResolver.getFullNamesAndAliasesToSerialName(descriptor) - return namesAndAliasesToSerialName[schema.fullName] - ?: schema.aliases.firstNotNullOfOrNull { namesAndAliasesToSerialName[it] } - } - - override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { + override fun newDecoder(chosenSchema: Schema): Decoder { return AvroValueDirectDecoder(chosenSchema, avro, binaryDecoder) - .decodeSerializableValue(deserializer) - } - - override fun decodeSequentially() = true - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - throw IllegalIndexedAccessError() } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt index 08d74cc0..ffd71d7e 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/CollectionsDirectDecoder.kt @@ -39,8 +39,6 @@ internal class ArrayBlockDirectDecoder( ) : AbstractAvroDirectDecoder(avro, binaryDecoder) { override lateinit var currentWriterSchema: Schema - override fun decodeSequentially() = true - override fun decodeCollectionSize(descriptor: SerialDescriptor): Int { return if (decodeFirstBlock) { binaryDecoder.readArrayStart().toInt() @@ -57,6 +55,8 @@ internal class ArrayBlockDirectDecoder( currentWriterSchema = arraySchema.elementType } + override fun decodeSequentially() = true + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { throw IllegalIndexedAccessError() } @@ -70,8 +70,6 @@ internal class MapBlockDirectDecoder( ) : AbstractAvroDirectDecoder(avro, binaryDecoder) { override lateinit var currentWriterSchema: Schema - override fun decodeSequentially() = true - override fun decodeCollectionSize(descriptor: SerialDescriptor): Int { return if (decodeFirstBlock) { binaryDecoder.readMapStart().toInt() @@ -88,6 +86,8 @@ internal class MapBlockDirectDecoder( currentWriterSchema = if (index % 2 == 0) KEY_SCHEMA else mapSchema.valueType } + override fun decodeSequentially() = true + override fun decodeElementIndex(descriptor: SerialDescriptor): Int { throw IllegalIndexedAccessError() } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/RecordDirectDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/RecordDirectDecoder.kt index b3683575..5b5446c7 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/RecordDirectDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/direct/RecordDirectDecoder.kt @@ -37,15 +37,18 @@ internal class RecordDirectDecoder( is DecodingStep.IgnoreOptionalElement -> { // loop again to ignore the optional element } + is DecodingStep.SkipWriterField -> binaryDecoder.skip(field.schema) is DecodingStep.MissingElementValueFailure -> { throw SerializationException("No writer schema field matching element index ${field.elementIndex} in descriptor $descriptor") } + is DecodingStep.DeserializeWriterField -> { currentDecodingStep = field currentWriterSchema = field.schema return field.elementIndex } + is DecodingStep.GetDefaultValue -> { currentDecodingStep = field currentWriterSchema = field.schema @@ -55,16 +58,12 @@ internal class RecordDirectDecoder( } } - private inline fun decodeDefaultIfMissing( + private fun decodeDefault( + element: DecodingStep.GetDefaultValue, deserializer: DeserializationStrategy, - block: () -> T, ): T { - return when (val element = currentDecodingStep) { - is DecodingStep.DeserializeWriterField -> block() - is DecodingStep.GetDefaultValue -> - AvroValueGenericDecoder(avro, element.defaultValue, currentWriterSchema) - .decodeSerializableValue(deserializer) - } + return AvroValueGenericDecoder(avro, element.defaultValue, currentWriterSchema) + .decodeSerializableValue(deserializer) } override fun decodeNotNullMark(): Boolean { @@ -95,62 +94,72 @@ internal class RecordDirectDecoder( } override fun decodeInt(): Int { - return decodeDefaultIfMissing(Int.serializer()) { - super.decodeInt() + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeInt() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Int.serializer()) } } override fun decodeLong(): Long { - return decodeDefaultIfMissing(Long.serializer()) { - super.decodeLong() + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeLong() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Long.serializer()) } } override fun decodeBoolean(): Boolean { - return decodeDefaultIfMissing(Boolean.serializer()) { - super.decodeBoolean() + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeBoolean() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Boolean.serializer()) } } override fun decodeChar(): Char { - return decodeDefaultIfMissing(Char.serializer()) { - super.decodeChar() + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeChar() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Char.serializer()) } } override fun decodeString(): String { - return decodeDefaultIfMissing(String.serializer()) { - super.decodeString() + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeString() + is DecodingStep.GetDefaultValue -> decodeDefault(element, String.serializer()) } } override fun decodeDouble(): Double { - return decodeDefaultIfMissing(Double.serializer()) { - super.decodeDouble() + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeDouble() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Double.serializer()) } } override fun decodeFloat(): Float { - return decodeDefaultIfMissing(Float.serializer()) { - super.decodeFloat() + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeFloat() + is DecodingStep.GetDefaultValue -> decodeDefault(element, Float.serializer()) } } override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - return decodeDefaultIfMissing(deserializer) { - super.decodeSerializableValue(deserializer) + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeSerializableValue(deserializer) + is DecodingStep.GetDefaultValue -> decodeDefault(element, deserializer) } } override fun decodeEnum(enumDescriptor: SerialDescriptor): Int { - return decodeDefaultIfMissing(Int.serializer()) { - super.decodeEnum(enumDescriptor) + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeEnum(enumDescriptor) + is DecodingStep.GetDefaultValue -> decodeDefault(element, Int.serializer()) } } override fun decodeBytes(): ByteArray { - return decodeDefaultIfMissing(ByteArraySerializer()) { - super.decodeBytes() + return when (val element = currentDecodingStep) { + is DecodingStep.DeserializeWriterField -> super.decodeBytes() + is DecodingStep.GetDefaultValue -> decodeDefault(element, ByteArraySerializer()) } } } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt index 38528af2..ede3dd5c 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/AbstractAvroGenericDecoder.kt @@ -21,6 +21,7 @@ import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.modules.SerializersModule import org.apache.avro.generic.GenericArray +import org.apache.avro.generic.GenericContainer import org.apache.avro.generic.GenericEnumSymbol import org.apache.avro.generic.GenericFixed import org.apache.avro.generic.IndexedRecord @@ -92,8 +93,8 @@ internal abstract class AbstractAvroGenericDecoder : AbstractDecoder(), AvroDeco is PolymorphicKind -> when (val value = decodeValue()) { - is IndexedRecord -> PolymorphicGenericDecoder(avro, descriptor, value) - else -> throw BadDecodedValueError(value, descriptor.kind, IndexedRecord::class) + is GenericContainer -> PolymorphicGenericDecoder(avro, descriptor, value.schema, value) + else -> PolymorphicGenericDecoder(avro, descriptor, currentWriterSchema, value) } else -> throw SerializationException("Unsupported descriptor for structure decoding: $descriptor") diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/PolymorphicGenericDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/PolymorphicGenericDecoder.kt index bad86e17..213854ff 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/PolymorphicGenericDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/decoder/generic/PolymorphicGenericDecoder.kt @@ -1,42 +1,25 @@ package com.github.avrokotlin.avro4k.internal.decoder.generic import com.github.avrokotlin.avro4k.Avro -import com.github.avrokotlin.avro4k.internal.IllegalIndexedAccessError -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.SerializationException +import com.github.avrokotlin.avro4k.internal.decoder.AbstractPolymorphicDecoder import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.AbstractDecoder -import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.encoding.Decoder import org.apache.avro.Schema -import org.apache.avro.generic.IndexedRecord internal class PolymorphicGenericDecoder( - private val avro: Avro, - private val descriptor: SerialDescriptor, - private val value: IndexedRecord, -) : AbstractDecoder() { - override val serializersModule: SerializersModule - get() = avro.serializersModule - - override fun decodeString(): String { - return tryFindSerialName(value.schema) - ?: throw SerializationException("Unknown schema name ${value.schema.fullName} for polymorphic type ${descriptor.serialName}") - } - - private fun tryFindSerialName(schema: Schema): String? { - val namesAndAliasesToSerialName = avro.polymorphicResolver.getFullNamesAndAliasesToSerialName(descriptor) - return namesAndAliasesToSerialName[schema.fullName] - ?: schema.aliases.firstNotNullOfOrNull { namesAndAliasesToSerialName[it] } + avro: Avro, + descriptor: SerialDescriptor, + schema: Schema, + private val value: Any?, +) : AbstractPolymorphicDecoder(avro, descriptor, schema) { + override fun tryFindSerialNameForUnion( + namesAndAliasesToSerialName: Map, + schema: Schema, + ): Pair? { + return schema.types.firstNotNullOfOrNull { tryFindSerialName(namesAndAliasesToSerialName, it) } } - override fun decodeSerializableValue(deserializer: DeserializationStrategy): T { - return AvroValueGenericDecoder(avro, value, value.schema) - .decodeSerializableValue(deserializer) - } - - override fun decodeSequentially() = true - - override fun decodeElementIndex(descriptor: SerialDescriptor): Int { - throw IllegalIndexedAccessError() + override fun newDecoder(chosenSchema: Schema): Decoder { + return AvroValueGenericDecoder(avro, value, chosenSchema) } } \ No newline at end of file diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/RecordDirectEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/RecordDirectEncoder.kt index bedcbadf..5c0e1fc8 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/RecordDirectEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/RecordDirectEncoder.kt @@ -34,7 +34,7 @@ internal fun RecordDirectEncoder( */ private class RecordSequentialDirectEncoder( private val classDescriptor: ClassDescriptorForWriterSchema, - protected val schema: Schema, + private val schema: Schema, avro: Avro, binaryEncoder: org.apache.avro.io.Encoder, ) : AbstractAvroDirectEncoder(avro, binaryEncoder) { @@ -63,7 +63,7 @@ private class RecordSequentialDirectEncoder( } override fun endStructure(descriptor: SerialDescriptor) { - if (descriptor.elementsCount < classDescriptor.encodingSteps.size) { + if (classDescriptor.hasMissingWriterField) { throw SerializationException("The descriptor is not writing all the expected fields of writer schema. Schema: $schema, descriptor: $descriptor") } } diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt index a35d7f99..f6ace386 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt @@ -31,6 +31,10 @@ internal inline fun SerialDescriptor.findElementAnnotat internal val SerialDescriptor.nonNullSerialName: String get() = nonNullOriginal.serialName +internal fun Schema.isNamedSchema(): Boolean { + return this.type == Schema.Type.RECORD || this.type == Schema.Type.ENUM || this.type == Schema.Type.FIXED +} + internal fun Schema.isFullNameOrAliasMatch(descriptor: SerialDescriptor): Boolean { return isFullNameMatch(descriptor.nonNullSerialName) || descriptor.findAnnotation()?.value?.any { isFullNameMatch(it) } == true } From 948643e1e93bdeb9535c5e1b6e80b88e248a418f Mon Sep 17 00:00:00 2001 From: Chuckame Date: Tue, 25 Jun 2024 19:22:29 +0200 Subject: [PATCH 3/3] unifying union checks --- benchmark/README.md | 16 ++++++++-------- .../com/github/avrokotlin/avro4k/AvroDecoder.kt | 2 +- .../com/github/avrokotlin/avro4k/AvroEncoder.kt | 2 +- .../encoder/direct/AbstractAvroDirectEncoder.kt | 4 ++-- .../generic/AbstractAvroGenericEncoder.kt | 4 ++-- .../github/avrokotlin/avro4k/internal/helpers.kt | 2 +- .../avro4k/internal/schema/ValueVisitor.kt | 2 +- .../avrokotlin/avro4k/RecordBuilderForTest.kt | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) diff --git a/benchmark/README.md b/benchmark/README.md index 15a32391..677b8604 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -25,14 +25,14 @@ Computer: Macbook air M2 ``` Benchmark Mode Cnt Score Error Units Relative Difference (%) -ApacheAvroReflectBenchmark.read thrpt 5 20073.322 ± 878.268 ops/s +1.70% -Avro4kBenchmark.read thrpt 5 19738.061 ± 930.026 ops/s 0.00% -Avro4kGenericWithApacheAvroBenchmark.read thrpt 5 7538.287 ± 112.690 ops/s -61.78% - -Avro4kBenchmark.write thrpt 5 40780.118 ± 4136.921 ops/s 0.00% -ApacheAvroReflectBenchmark.write thrpt 5 37632.786 ± 117.940 ops/s -7.72% -JacksonAvroBenchmark.write thrpt 5 30544.663 ± 1004.357 ops/s -25.08% -Avro4kGenericWithApacheAvroBenchmark.write thrpt 5 21088.555 ± 1280.548 ops/s -48.31% +Avro4kBenchmark.read thrpt 5 20537.185 ± 135.318 ops/s 0.00% +ApacheAvroReflectBenchmark.read thrpt 5 20059.982 ± 241.854 ops/s -2.32% +Avro4kGenericWithApacheAvroBenchmark.read thrpt 5 7591.527 ± 172.173 ops/s -63.03% + +Avro4kBenchmark.write thrpt 5 41215.703 ± 1274.692 ops/s 0.00% +ApacheAvroReflectBenchmark.write thrpt 5 37188.260 ± 115.447 ops/s -9.74% +JacksonAvroBenchmark.write thrpt 5 30757.363 ± 1557.034 ops/s -25.39% +Avro4kGenericWithApacheAvroBenchmark.write thrpt 5 21305.149 ± 830.640 ops/s -48.33% ``` > [!WARNING] diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroDecoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroDecoder.kt index 482ac111..569bf6a4 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroDecoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroDecoder.kt @@ -306,7 +306,7 @@ internal inline fun AvroDecoder.findValueDecoder( val schema = currentWriterSchema val foundResolver = - if (schema.type == Schema.Type.UNION) { + if (schema.isUnion) { if (this is UnionDecoder) { decodeAndResolveUnion() resolver(currentWriterSchema) diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroEncoder.kt index cf4eaddc..1e791ec2 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/AvroEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/AvroEncoder.kt @@ -83,7 +83,7 @@ public inline fun AvroEncoder.encodeResolving( resolver: (Schema) -> (() -> T)?, ): T { val schema = currentWriterSchema - return if (schema.type == Schema.Type.UNION) { + return if (schema.isUnion) { resolveUnion(schema, error, resolver) } else { resolver(schema)?.invoke() ?: throw error() diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt index 1b49b445..6bef64d0 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/direct/AbstractAvroDirectEncoder.kt @@ -52,7 +52,7 @@ internal sealed class AbstractAvroDirectEncoder( private fun Schema.isTypeOfBytes() = type == Schema.Type.BYTES || - type == Schema.Type.UNION && types.any { it.type == Schema.Type.BYTES } + isUnion && types.any { it.type == Schema.Type.BYTES } override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { return when (descriptor.kind) { @@ -124,7 +124,7 @@ internal sealed class AbstractAvroDirectEncoder( if (selectedUnionIndex > -1) { throw SerializationException("Already selected union index: $selectedUnionIndex, got $index, for selected schema $currentWriterSchema") } - if (currentWriterSchema.type == Schema.Type.UNION) { + if (currentWriterSchema.isUnion) { binaryEncoder.writeIndex(index) selectedUnionIndex = index currentWriterSchema = currentWriterSchema.types[index] diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt index eee38721..f3f33679 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/encoder/generic/AbstractAvroGenericEncoder.kt @@ -44,7 +44,7 @@ internal abstract class AbstractAvroGenericEncoder : AbstractEncoder(), AvroEnco if (selectedUnionIndex > -1) { throw SerializationException("Already selected union index: $selectedUnionIndex, got $index, for selected schema $currentWriterSchema") } - if (currentWriterSchema.type == Schema.Type.UNION) { + if (currentWriterSchema.isUnion) { selectedUnionIndex = index currentWriterSchema = currentWriterSchema.types[index] } else { @@ -60,7 +60,7 @@ internal abstract class AbstractAvroGenericEncoder : AbstractEncoder(), AvroEnco value: T, ) { if (currentWriterSchema.type == Schema.Type.BYTES || - currentWriterSchema.type == Schema.Type.UNION && currentWriterSchema.types.any { it.type == Schema.Type.BYTES } + currentWriterSchema.isUnion && currentWriterSchema.types.any { it.type == Schema.Type.BYTES } ) { when (value) { is ByteArray -> encodeBytes(value) diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt index f6ace386..f298ef62 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/helpers.kt @@ -81,7 +81,7 @@ internal fun Schema.copy( namespace: String? = if (this.type == Schema.Type.RECORD || this.type == Schema.Type.ENUM || this.type == Schema.Type.FIXED) this.namespace else null, aliases: Set = if (this.type == Schema.Type.RECORD || this.type == Schema.Type.ENUM || this.type == Schema.Type.FIXED) this.aliases else emptySet(), isError: Boolean = if (this.type == Schema.Type.RECORD) this.isError else false, - types: List = if (this.type == Schema.Type.UNION) this.types.toList() else emptyList(), + types: List = if (this.isUnion) this.types.toList() else emptyList(), enumSymbols: List = if (this.type == Schema.Type.ENUM) this.enumSymbols.toList() else emptyList(), fields: List? = if (this.type == Schema.Type.RECORD && this.hasFields()) this.fields.map { it.copy() } else null, enumDefault: String? = if (this.type == Schema.Type.ENUM) this.enumDefault else null, diff --git a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt index db224618..6318b9ff 100644 --- a/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt +++ b/src/main/kotlin/com/github/avrokotlin/avro4k/internal/schema/ValueVisitor.kt @@ -124,7 +124,7 @@ internal class ValueVisitor internal constructor( } private fun Schema.toNullableSchema(): Schema { - return if (this.type == Schema.Type.UNION) { + return if (this.isUnion) { Schema.createUnion(listOf(Schema.create(Schema.Type.NULL)) + this.types) } else { Schema.createUnion(Schema.create(Schema.Type.NULL), this) diff --git a/src/test/kotlin/com/github/avrokotlin/avro4k/RecordBuilderForTest.kt b/src/test/kotlin/com/github/avrokotlin/avro4k/RecordBuilderForTest.kt index ae87951f..f4f7243e 100644 --- a/src/test/kotlin/com/github/avrokotlin/avro4k/RecordBuilderForTest.kt +++ b/src/test/kotlin/com/github/avrokotlin/avro4k/RecordBuilderForTest.kt @@ -94,6 +94,6 @@ internal fun recordWithSchema( private val Schema.nonNull: Schema get() = when { - type == Schema.Type.UNION && isNullable -> this.types.filter { it.type != Schema.Type.NULL }.let { if (it.size > 1) Schema.createUnion(it) else it[0] } + isUnion && isNullable -> this.types.filter { it.type != Schema.Type.NULL }.let { if (it.size > 1) Schema.createUnion(it) else it[0] } else -> this } \ No newline at end of file