Skip to content

Commit

Permalink
Implemented serializers caching for lookup
Browse files Browse the repository at this point in the history
  • Loading branch information
shanshin committed Sep 7, 2022
1 parent 49dad86 commit 2825e21
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 53 deletions.
@@ -0,0 +1,69 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.benchmarks.json

import kotlinx.serialization.*
import kotlinx.serialization.builtins.*
import kotlinx.serialization.json.*
import org.openjdk.jmh.annotations.*
import java.util.concurrent.*

@Warmup(iterations = 7, time = 1)
@Measurement(iterations = 7, time = 1)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
@Fork(2)
open class LookupOverheadBenchmark {

@Serializable
class Holder(val a: String)

@Serializable
class Generic<T>(val a: T)

@Serializable
class DoubleGeneric<T1, T2>(val a: T1, val b: T2)

@Serializable
class PentaGeneric<T1, T2, T3, T4, T5>(val a: T1, val b: T2, val c: T3, val d: T4, val e: T5)

private val data = """{"a":""}"""
private val doubleData = """{"a":"","b":0}"""
private val pentaData = """{"a":"","b":0,"c":1,"d":true,"e":" "}"""

@Serializable
object Object

@Benchmark
fun dataReified() = Json.decodeFromString<Holder>(data)

@Benchmark
fun dataPlain() = Json.decodeFromString(Holder.serializer(), data)

@Benchmark
fun genericReified() = Json.decodeFromString<Generic<String>>(data)

@Benchmark
fun genericPlain() = Json.decodeFromString(Generic.serializer(String.serializer()), data)

@Benchmark
fun doubleGenericReified() = Json.decodeFromString<DoubleGeneric<String, Int>>(doubleData)

@Benchmark
fun doubleGenericPlain() = Json.decodeFromString(DoubleGeneric.serializer(String.serializer(), Int.serializer()), doubleData)

@Benchmark
fun pentaGenericReified() = Json.decodeFromString<PentaGeneric<String, Int, Long, Boolean, Char>>(pentaData)

@Benchmark
fun pentaGenericPlain() = Json.decodeFromString(PentaGeneric.serializer(String.serializer(), Int.serializer(), Long.serializer(), Boolean.serializer(), Char.serializer()), pentaData)

@Benchmark
fun objectReified() = Json.decodeFromString<Object>("{}")

@Benchmark
fun objectPlain() = Json.decodeFromString(Object.serializer(), "{}")
}
2 changes: 1 addition & 1 deletion build.gradle
Expand Up @@ -173,7 +173,7 @@ subprojects {
afterEvaluate { // Can be applied only when the project is evaluated
animalsniffer {
sourceSets = [sourceSets.main]
annotation = "kotlinx.serialization.json.internal.SuppressAnimalSniffer"
annotation = (name == "kotlinx-serialization-core")? "kotlinx.serialization.internal.SuppressAnimalSniffer" : "kotlinx.serialization.json.internal.SuppressAnimalSniffer"
}
dependencies {
signature 'net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature'
Expand Down
123 changes: 78 additions & 45 deletions core/commonMain/src/kotlinx/serialization/Serializers.kt
Expand Up @@ -66,9 +66,8 @@ public fun SerializersModule.serializer(type: KType): KSerializer<Any?> =
* Returns `null` if serializer cannot be created (provided [type] or its type argument is not serializable and is not registered in [this] module).
*/
@OptIn(ExperimentalSerializationApi::class)
public fun SerializersModule.serializerOrNull(type: KType): KSerializer<Any?>? {
return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false)
}
public fun SerializersModule.serializerOrNull(type: KType): KSerializer<Any?>? =
serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false)

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.serializerByKTypeImpl(
Expand All @@ -79,54 +78,47 @@ private fun SerializersModule.serializerByKTypeImpl(
val isNullable = type.isMarkedNullable
val typeArguments = type.arguments
.map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } }
val result: KSerializer<Any>? = when {
typeArguments.isEmpty() -> rootClass.serializerOrNull() ?: getContextual(rootClass)
else -> builtinSerializer(typeArguments, rootClass, failOnMissingTypeArgSerializer)
}?.cast()
return result?.nullable(isNullable)
}

@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.builtinSerializer(
typeArguments: List<KType>,
rootClass: KClass<Any>,
failOnMissingTypeArgSerializer: Boolean
): KSerializer<out Any>? {
val serializers = if (failOnMissingTypeArgSerializer)
typeArguments.map(::serializer)
else {
typeArguments.map { serializerOrNull(it) ?: return null }
val cachedSerializer = if (typeArguments.isEmpty()) {
findCachedSerializer(rootClass, isNullable)
} else {
val cachedResult = findParametrizedCachedSerializer(rootClass, typeArguments, isNullable)
if (failOnMissingTypeArgSerializer) {
cachedResult.getOrNull()
} else {
// return null if error occurred - serializer for parameter(s) was not found
cachedResult.getOrElse { return null }
}
}
// Array is not supported, see KT-32839
return when (rootClass) {
Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0])
HashSet::class -> HashSetSerializer(serializers[0])
Set::class, MutableSet::class, LinkedHashSet::class -> LinkedHashSetSerializer(serializers[0])
HashMap::class -> HashMapSerializer(serializers[0], serializers[1])
Map::class, MutableMap::class, LinkedHashMap::class -> LinkedHashMapSerializer(
serializers[0],
serializers[1]
cachedSerializer?.let { return it }

// slow path to find contextual serializers in serializers module
val contextualSerializer: KSerializer<out Any?>? = if (typeArguments.isEmpty()) {
getContextual(rootClass)
} else {
val serializers = serializersForParameters(typeArguments, failOnMissingTypeArgSerializer) ?: return null
// first, we look among the built-in serializers, because the parameter could be contextual
rootClass.parametrizedSerializerOrNull(typeArguments, serializers) ?: getContextual(
rootClass,
serializers
)
Map.Entry::class -> MapEntrySerializer(serializers[0], serializers[1])
Pair::class -> PairSerializer(serializers[0], serializers[1])
Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2])
else -> {
if (isReferenceArray(rootClass)) {
return ArraySerializer<Any, Any?>(typeArguments[0].classifier as KClass<Any>, serializers[0]).cast()
}
val args = serializers.toTypedArray()
rootClass.constructSerializerForGivenTypeArgs(*args)
?: reflectiveOrContextual(rootClass, serializers)
}
}
return contextualSerializer?.cast<Any>()?.nullable(isNullable)
}

@OptIn(ExperimentalSerializationApi::class)
internal fun <T : Any> SerializersModule.reflectiveOrContextual(
kClass: KClass<T>,
typeArgumentsSerializers: List<KSerializer<Any?>>
): KSerializer<T>? {
return kClass.serializerOrNull() ?: getContextual(kClass, typeArgumentsSerializers)
/**
* Returns null only if `failOnMissingTypeArgSerializer == false` and at least one parameter serializer not found.
*/
internal fun SerializersModule.serializersForParameters(
typeArguments: List<KType>,
failOnMissingTypeArgSerializer: Boolean
): List<KSerializer<Any?>>? {
val serializers = if (failOnMissingTypeArgSerializer) {
typeArguments.map { serializer(it) }
} else {
typeArguments.map { serializerOrNull(it) ?: return null }
}
return serializers
}

/**
Expand Down Expand Up @@ -179,6 +171,47 @@ public fun <T : Any> KClass<T>.serializer(): KSerializer<T> = serializerOrNull()
public fun <T : Any> KClass<T>.serializerOrNull(): KSerializer<T>? =
compiledSerializerImpl() ?: builtinSerializerOrNull()

internal fun KClass<Any>.parametrizedSerializerOrNull(
types: List<KType>,
serializers: List<KSerializer<Any?>>
): KSerializer<out Any>? {
// builtin first because some standard parametrized interfaces (e.g. Map) must use builtin serializer but not polymorphic
return builtinParametrizedSerializer(types, serializers) ?: compiledParametrizedSerializer(serializers)
}


private fun KClass<Any>.compiledParametrizedSerializer(serializers: List<KSerializer<Any?>>): KSerializer<out Any>? {
return constructSerializerForGivenTypeArgs(*serializers.toTypedArray())
}

@OptIn(ExperimentalSerializationApi::class)
private fun KClass<Any>.builtinParametrizedSerializer(
typeArguments: List<KType>,
serializers: List<KSerializer<Any?>>,
): KSerializer<out Any>? {
// Array is not supported, see KT-32839
return when (this) {
Collection::class, List::class, MutableList::class, ArrayList::class -> ArrayListSerializer(serializers[0])
HashSet::class -> HashSetSerializer(serializers[0])
Set::class, MutableSet::class, LinkedHashSet::class -> LinkedHashSetSerializer(serializers[0])
HashMap::class -> HashMapSerializer(serializers[0], serializers[1])
Map::class, MutableMap::class, LinkedHashMap::class -> LinkedHashMapSerializer(
serializers[0],
serializers[1]
)
Map.Entry::class -> MapEntrySerializer(serializers[0], serializers[1])
Pair::class -> PairSerializer(serializers[0], serializers[1])
Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2])
else -> {
if (isReferenceArray(this)) {
ArraySerializer(typeArguments[0].classifier as KClass<Any>, serializers[0])
} else {
null
}
}
}
}

private fun <T : Any> KSerializer<T>.nullable(shouldBeNullable: Boolean): KSerializer<T?> {
if (shouldBeNullable) return nullable
return this as KSerializer<T?>
Expand Down
74 changes: 74 additions & 0 deletions core/commonMain/src/kotlinx/serialization/SerializersCache.kt
@@ -0,0 +1,74 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.serialization

import kotlinx.serialization.builtins.nullable
import kotlinx.serialization.internal.cast
import kotlinx.serialization.internal.createCache
import kotlinx.serialization.internal.createParametrizedCache
import kotlinx.serialization.modules.EmptySerializersModule
import kotlin.native.concurrent.ThreadLocal
import kotlin.reflect.KClass
import kotlin.reflect.KType


/**
* Cache for non-null non-parametrized and non-contextual serializers.
*/
@ThreadLocal
private val SERIALIZERS_CACHE = createCache { it.serializerOrNull() }

/**
* Cache for nullable non-parametrized and non-contextual serializers.
*/
@ThreadLocal
private val SERIALIZERS_CACHE_NULLABLE = createCache<Any?> { it.serializerOrNull()?.nullable?.cast() }

/**
* Cache for non-null parametrized and non-contextual serializers.
*/
@ThreadLocal
private val PARAMETRIZED_SERIALIZERS_CACHE = createParametrizedCache { clazz, types ->
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
clazz.parametrizedSerializerOrNull(types, serializers)
}

/**
* Cache for nullable parametrized and non-contextual serializers.
*/
@ThreadLocal
private val PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE = createParametrizedCache<Any?> { clazz, types ->
val serializers = EmptySerializersModule().serializersForParameters(types, true)!!
clazz.parametrizedSerializerOrNull(types, serializers)?.nullable?.cast()
}

/**
* Find cacheable serializer in the cache.
* If serializer is cacheable but missed in cache - it will be created, placed into the cache and returned.
*/
internal fun findCachedSerializer(clazz: KClass<Any>, isNullable: Boolean): KSerializer<Any?>? {
return if (!isNullable) {
SERIALIZERS_CACHE.get(clazz)?.cast()
} else {
SERIALIZERS_CACHE_NULLABLE.get(clazz)
}
}

/**
* Find cacheable parametrized serializer in the cache.
* If serializer is cacheable but missed in cache - it will be created, placed into the cache and returned.
*/
internal fun findParametrizedCachedSerializer(
clazz: KClass<Any>,
types: List<KType>,
isNullable: Boolean
): Result<KSerializer<Any?>?> {
return if (!isNullable) {
@Suppress("UNCHECKED_CAST")
PARAMETRIZED_SERIALIZERS_CACHE.get(clazz, types) as Result<KSerializer<Any?>?>
} else {
PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE.get(clazz, types)
}
}
Expand Up @@ -130,6 +130,18 @@ internal expect fun BooleanArray.getChecked(index: Int): Boolean

internal expect fun <T : Any> KClass<T>.compiledSerializerImpl(): KSerializer<T>?

/**
* Create serializers cache for non-parametrized and non-contextual serializers.
* The activity and type of cache is determined for a specific platform and a specific environment.
*/
internal expect fun <T> createCache(factory: (KClass<*>) -> KSerializer<T>?): SerializerCache<T>

/**
* Create serializers cache for parametrized and non-contextual serializers. Parameters also non-contextual.
* The activity and type of cache is determined for a specific platform and a specific environment.
*/
internal expect fun <T> createParametrizedCache(factory: (KClass<Any>, List<KType>) -> KSerializer<T>?): ParametrizedSerializerCache<T>

internal expect fun <T : Any, E : T?> ArrayList<E>.toNativeArrayImpl(eClass: KClass<T>): Array<E>

/**
Expand All @@ -145,3 +157,24 @@ internal expect fun Any.isInstanceOf(kclass: KClass<*>): Boolean
internal inline fun <T, K> Iterable<T>.elementsHashCodeBy(selector: (T) -> K): Int {
return fold(1) { hash, element -> 31 * hash + selector(element).hashCode() }
}

/**
* Cache class for non-parametrized and non-contextual serializers.
*/
internal interface SerializerCache<T> {
/**
* Returns cached serializer or `null` if serializer not found.
*/
fun get(key: KClass<Any>): KSerializer<T>?
}

/**
* Cache class for parametrized and non-contextual serializers.
*/
internal interface ParametrizedSerializerCache<T> {
/**
* Returns successful result with cached serializer or `null` if root serializer not found.
* If no serializer was found for the parameters, then result contains an exception.
*/
fun get(key: KClass<Any>, types: List<KType> = emptyList()): Result<KSerializer<T>?>
}
16 changes: 16 additions & 0 deletions core/jsMain/src/kotlinx/serialization/internal/Platform.kt
Expand Up @@ -20,6 +20,22 @@ internal actual fun BooleanArray.getChecked(index: Int): Boolean {
internal actual fun <T : Any> KClass<T>.compiledSerializerImpl(): KSerializer<T>? =
this.constructSerializerForGivenTypeArgs() ?: this.js.asDynamic().Companion?.serializer() as? KSerializer<T>

internal actual fun <T> createCache(factory: (KClass<*>) -> KSerializer<T>?): SerializerCache<T> {
return object: SerializerCache<T> {
override fun get(key: KClass<Any>): KSerializer<T>? {
return factory(key)
}
}
}

internal actual fun <T> createParametrizedCache(factory: (KClass<Any>, List<KType>) -> KSerializer<T>?): ParametrizedSerializerCache<T> {
return object: ParametrizedSerializerCache<T> {
override fun get(key: KClass<Any>, types: List<KType>): Result<KSerializer<T>?> {
return kotlin.runCatching { factory(key, types) }
}
}
}

internal actual fun <T : Any, E : T?> ArrayList<E>.toNativeArrayImpl(eClass: KClass<T>): Array<E> = toTypedArray()

internal actual fun Any.isInstanceOf(kclass: KClass<*>): Boolean = kclass.isInstance(this)
Expand Down

0 comments on commit 2825e21

Please sign in to comment.