diff --git a/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/TagArraySerializerBenchmark.kt b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/TagArraySerializerBenchmark.kt new file mode 100644 index 000000000..d8abe9be2 --- /dev/null +++ b/benchmark/src/androidTest/java/com/vitorpamplona/quartz/benchmark/TagArraySerializerBenchmark.kt @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.benchmark + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vitorpamplona.quartz.nip01Core.core.OptimizedJsonMapper +import com.vitorpamplona.quartz.nip01Core.jackson.JacksonMapper +import junit.framework.TestCase.assertNotNull +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class TagArraySerializerBenchmark { + @get:Rule val benchmarkRule = BenchmarkRule() + + val tags = + arrayOf( + arrayOf("title", "Retro Computer Fans"), + arrayOf("d", "xmbspe8rddsq"), + arrayOf("image", "https://blog.johnnovak.net/2022/04/15/achieving-period-correct-graphics-in-personal-computer-emulators-part-1-the-amiga/img/dream-setup.jpg"), + arrayOf("p", "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da"), + arrayOf("p", "9a9a4aa0e43e57873380ab22e8a3df12f3c4cf5bb3a804c6e3fed0069a6e2740"), + arrayOf("p", "4f5dd82517b11088ce00f23d99f06fe8f3e2e45ecf47bc9c2f90f34d5c6f7382"), + arrayOf("p", "ac92102a2ecb873c488e0125354ef5a97075a16198668c360eda050007ed42cd"), + arrayOf("p", "47f54409a4620eb35208a3bc1b53555bf3d0656b246bf0471a93208e20672f6f"), + arrayOf("p", "2624911545afb7a2b440cf10f5c69308afa33aae26fca664d8c94623dc0f1baf"), + arrayOf("p", "6641f26f5c59f7010dbe3e42e4593398e27c087497cb7d20e0e7633a17e48a94"), + arrayOf("description", "Retro computer fans and enthusiasts "), + ) + + @Test + fun serializeCommonTagArray() { + benchmarkRule.measureRepeated { assertNotNull(OptimizedJsonMapper.toJson(tags)) } + } + + @Test + fun deserializeCommonTagArray() { + val serialized = JacksonMapper.toJson(tags) + benchmarkRule.measureRepeated { assertNotNull(OptimizedJsonMapper.fromJsonToTagArray(serialized)) } + } +} diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt index 958bd7381..d1111304c 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapper.kt @@ -102,7 +102,7 @@ class JacksonMapper { fun fromJson(json: JsonNode): Event = EventManualDeserializer.fromJson(json) - fun fromJsonToTagArray(json: String): Array> = mapper.readValue>>(json) + fun fromJsonToTagArray(json: String): TagArray = mapper.readValue(json) inline fun fromJsonTo(json: String): T = mapper.readValue(json) @@ -116,6 +116,6 @@ class JacksonMapper { fun toJson(value: OptimizedSerializable): String = mapper.writeValueAsString(value) - fun toJson(tags: Array>): String = mapper.writeValueAsString(tags) + fun toJson(tags: TagArray): String = mapper.writeValueAsString(tags) } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayDeserializer.kt index fbc44abe3..c907d773d 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayDeserializer.kt @@ -21,12 +21,31 @@ package com.vitorpamplona.quartz.nip01Core.jackson import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.databind.DeserializationContext import com.fasterxml.jackson.databind.deser.std.StdDeserializer +import com.vitorpamplona.quartz.nip01Core.core.TagArray -class TagArrayDeserializer : StdDeserializer>>(Array>::class.java) { +class TagArrayDeserializer : StdDeserializer(TagArray::class.java) { + // Needs to be very fast. override fun deserialize( - jp: JsonParser, + p: JsonParser, ctxt: DeserializationContext, - ): Array> = TagArrayManualDeserializer.fromJson(jp.codec.readTree(jp)) + ): TagArray { + // doesn't check for weird payloads on purpose. + val outerList = ArrayList>() + while (p.nextToken() != JsonToken.END_ARRAY) { + val innerList = ArrayList(5) + while (p.nextToken() != JsonToken.END_ARRAY) { + if (p.currentToken == JsonToken.VALUE_NULL) { + innerList.add("") + } else { + innerList.add(p.text.intern()) + } + } + + outerList.add(innerList.toArray(arrayOfNulls(innerList.size))) + } + return outerList.toArray(arrayOfNulls>(outerList.size)) + } } diff --git a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayManualDeserializer.kt b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayManualDeserializer.kt index b4ec39807..1aa2c4b30 100644 --- a/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayManualDeserializer.kt +++ b/quartz/src/jvmAndroid/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/TagArrayManualDeserializer.kt @@ -21,12 +21,13 @@ package com.vitorpamplona.quartz.nip01Core.jackson import com.fasterxml.jackson.databind.JsonNode +import com.vitorpamplona.quartz.nip01Core.core.TagArray class TagArrayManualDeserializer { companion object { - fun fromJson(jsonObject: JsonNode): Array> = - jsonObject.toTypedArray { - it.toTypedArray { s -> + fun fromJson(tagArray: JsonNode): TagArray = + tagArray.toTypedArray { tag -> + tag.toTypedArray { s -> if (s.isNull) { "" } else { diff --git a/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt new file mode 100644 index 000000000..c454fc648 --- /dev/null +++ b/quartz/src/jvmAndroidTest/kotlin/com/vitorpamplona/quartz/nip01Core/jackson/JacksonMapperTest.kt @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.nip01Core.jackson + +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals + +class JacksonMapperTest { + @Test + fun serializeTagArray() { + val tags = + arrayOf( + arrayOf("title", "Retro Computer Fans"), + arrayOf("d", "xmbspe8rddsq"), + arrayOf("image", "https://blog.johnnovak.net/2022/04/15/achieving-period-correct-graphics-in-personal-computer-emulators-part-1-the-amiga/img/dream-setup.jpg"), + arrayOf("p", "3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da"), + arrayOf("p", "9a9a4aa0e43e57873380ab22e8a3df12f3c4cf5bb3a804c6e3fed0069a6e2740"), + arrayOf("p", "4f5dd82517b11088ce00f23d99f06fe8f3e2e45ecf47bc9c2f90f34d5c6f7382"), + arrayOf("p", "ac92102a2ecb873c488e0125354ef5a97075a16198668c360eda050007ed42cd"), + arrayOf("p", "47f54409a4620eb35208a3bc1b53555bf3d0656b246bf0471a93208e20672f6f"), + arrayOf("p", "2624911545afb7a2b440cf10f5c69308afa33aae26fca664d8c94623dc0f1baf"), + arrayOf("p", "6641f26f5c59f7010dbe3e42e4593398e27c087497cb7d20e0e7633a17e48a94"), + arrayOf("description", "Retro computer fans and enthusiasts "), + ) + + val serialized = JacksonMapper.toJson(tags) + + assertEquals( + "[[\"title\",\"Retro Computer Fans\"],[\"d\",\"xmbspe8rddsq\"],[\"image\",\"https://blog.johnnovak.net/2022/04/15/achieving-period-correct-graphics-in-personal-computer-emulators-part-1-the-amiga/img/dream-setup.jpg\"],[\"p\",\"3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da\"],[\"p\",\"9a9a4aa0e43e57873380ab22e8a3df12f3c4cf5bb3a804c6e3fed0069a6e2740\"],[\"p\",\"4f5dd82517b11088ce00f23d99f06fe8f3e2e45ecf47bc9c2f90f34d5c6f7382\"],[\"p\",\"ac92102a2ecb873c488e0125354ef5a97075a16198668c360eda050007ed42cd\"],[\"p\",\"47f54409a4620eb35208a3bc1b53555bf3d0656b246bf0471a93208e20672f6f\"],[\"p\",\"2624911545afb7a2b440cf10f5c69308afa33aae26fca664d8c94623dc0f1baf\"],[\"p\",\"6641f26f5c59f7010dbe3e42e4593398e27c087497cb7d20e0e7633a17e48a94\"],[\"description\",\"Retro computer fans and enthusiasts \"]]", + serialized, + ) + + val deserialized = JacksonMapper.fromJsonToTagArray(serialized) + + tags.forEachIndexed { index, tag -> + assertContentEquals(tag, deserialized[index]) + } + } +}