Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented serializers caching for lookup #2015

Merged
merged 7 commits into from Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,49 @@
/*
* Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

package kotlinx.benchmarks.json
shanshin marked this conversation as resolved.
Show resolved Hide resolved

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)

private val data = """{"a":""}"""

@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 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"
shanshin marked this conversation as resolved.
Show resolved Hide resolved
}
dependencies {
signature 'net.sf.androidscents.signature:android-api-level-14:4.0_r4@signature'
Expand Down
154 changes: 106 additions & 48 deletions core/commonMain/src/kotlinx/serialization/Serializers.kt
Expand Up @@ -14,8 +14,24 @@ import kotlinx.serialization.builtins.TripleSerializer
import kotlinx.serialization.internal.*
import kotlinx.serialization.modules.*
import kotlin.jvm.*
import kotlin.native.concurrent.SharedImmutable
import kotlin.reflect.*

/**
* Cache class for non-parametrized and non-contextual serializers.
*/
@SharedImmutable
shanshin marked this conversation as resolved.
Show resolved Hide resolved
private val SERIALIZERS_CACHE = createCache { it.serializerOrNull()?.cast() }

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

/**
* Retrieves a serializer for the given type [T].
* This method is a reified version of `serializer(KType)`.
Expand Down Expand Up @@ -55,9 +71,7 @@ public fun serializerOrNull(type: KType): KSerializer<Any?>? = EmptySerializersM
* @throws SerializationException 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.serializer(type: KType): KSerializer<Any?> =
serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass()
.platformSpecificSerializerNotRegistered()
public fun SerializersModule.serializer(type: KType): KSerializer<Any?> = serializerCommon(type)

/**
* Attempts to create a serializer for the given [type] and fallbacks to [contextual][SerializersModule.getContextual]
Expand All @@ -66,69 +80,72 @@ 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?>? {
public fun SerializersModule.serializerOrNull(type: KType): KSerializer<Any?>? = serializerOrNullCommon(type)


private fun SerializersModule?.serializerCommon(type: KType): KSerializer<Any?> =
serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass()
.platformSpecificSerializerNotRegistered()

private fun SerializersModule?.serializerOrNullCommon(type: KType): KSerializer<Any?>? {
return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false)
}

// the `this` is empty if there is no need to search for a contextual serializer
shanshin marked this conversation as resolved.
Show resolved Hide resolved
@OptIn(ExperimentalSerializationApi::class)
private fun SerializersModule.serializerByKTypeImpl(
private fun SerializersModule?.serializerByKTypeImpl(
shanshin marked this conversation as resolved.
Show resolved Hide resolved
type: KType,
failOnMissingTypeArgSerializer: Boolean
): KSerializer<Any?>? {
val rootClass = type.kclass()
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()) {
SERIALIZERS_CACHE.get(rootClass, isNullable)
} else {
if (failOnMissingTypeArgSerializer) {
PARAMETRIZED_SERIALIZERS_CACHE.get(rootClass, isNullable, typeArguments).getOrNull()
} else {
// return null if error occurred - serializer for parameter(s) was not found
PARAMETRIZED_SERIALIZERS_CACHE.get(rootClass, isNullable, typeArguments).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()) {
this?.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) ?: this?.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.
*/
private fun SerializersModule?.serializersForParameters(
shanshin marked this conversation as resolved.
Show resolved Hide resolved
typeArguments: List<KType>,
failOnMissingTypeArgSerializer: Boolean
): List<KSerializer<Any?>>? {
val serializers = if (failOnMissingTypeArgSerializer) {
typeArguments.map { this.serializerCommon(it) }
} else {
typeArguments.map { this.serializerOrNullCommon(it) ?: return null }
}
return serializers
}




shanshin marked this conversation as resolved.
Show resolved Hide resolved
/**
* Retrieves a [KSerializer] for the given [KClass].
* The given class must be annotated with [Serializable] or be one of the built-in types.
Expand Down Expand Up @@ -179,7 +196,48 @@ public fun <T : Any> KClass<T>.serializer(): KSerializer<T> = serializerOrNull()
public fun <T : Any> KClass<T>.serializerOrNull(): KSerializer<T>? =
compiledSerializerImpl() ?: builtinSerializerOrNull()

private fun <T : Any> KSerializer<T>.nullable(shouldBeNullable: Boolean): KSerializer<T?> {
private 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)) {
return ArraySerializer(typeArguments[0].classifier as KClass<Any>, serializers[0])
shanshin marked this conversation as resolved.
Show resolved Hide resolved
} else {
return null
}
}
}
}

internal fun <T : Any> KSerializer<T>.nullable(shouldBeNullable: Boolean): KSerializer<T?> {
if (shouldBeNullable) return nullable
return this as KSerializer<T?>
}
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 createCache(factory: (KClass<*>) -> KSerializer<Any>?): SerializerCache

/**
* 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 createParametrizedCache(factory: (KClass<Any>, List<KType>) -> KSerializer<Any>?): ParametrizedSerializerCache

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 {
/**
* Returns cached serializer or `null` if serializer not found.
*/
fun get(key: KClass<Any>, isNullable: Boolean): KSerializer<Any?>?
}

/**
* Cache class for parametrized and non-contextual serializers.
*/
internal interface ParametrizedSerializerCache {
/**
* 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>, isNullable: Boolean, types: List<KType> = emptyList()): Result<KSerializer<Any?>?>
}
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 createCache(factory: (KClass<*>) -> KSerializer<Any>?): SerializerCache {
return object: SerializerCache {
override fun get(key: KClass<Any>, isNullable: Boolean): KSerializer<Any?>? {
return factory(key)?.nullable(isNullable)
}
}
}

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

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