From 9bfdad52648df1cc8b0c130847bb84144e92e1e3 Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Thu, 18 Aug 2022 16:51:11 +0200 Subject: [PATCH 1/7] Implemented non-parametrized serializers caching for lookup --- .../json/LookupOverheadBenchmark.kt | 45 ++++++++++++ build.gradle | 2 +- .../src/kotlinx/serialization/Serializers.kt | 70 +++++++++++-------- .../serialization/internal/Platform.common.kt | 13 ++++ .../serialization/internal/Platform.kt | 6 ++ .../kotlinx/serialization/internal/Caching.kt | 68 ++++++++++++++++++ .../internal/SuppressAnimalSniffer.kt | 13 ++++ .../serialization/internal/Platform.kt | 6 ++ 8 files changed, 194 insertions(+), 29 deletions(-) create mode 100644 benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt create mode 100644 core/jvmMain/src/kotlinx/serialization/internal/Caching.kt create mode 100644 core/jvmMain/src/kotlinx/serialization/internal/SuppressAnimalSniffer.kt diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt new file mode 100644 index 000000000..689fe3ceb --- /dev/null +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt @@ -0,0 +1,45 @@ +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(val a: T) + + private val data = """{"a":""}""" + + @Serializable + object Object + + @Benchmark + fun dataReified() = Json.decodeFromString(data) + + @Benchmark + fun dataPlain() = Json.decodeFromString(Holder.serializer(), data) + + @Benchmark + fun genericReified() = Json.decodeFromString>(data) + + @Benchmark + fun genericPlain() = Json.decodeFromString(Generic.serializer(String.serializer()), data) + + @Benchmark + fun objectReified() = Json.decodeFromString("{}") + + @Benchmark + fun objectPlain() = Json.decodeFromString(Object.serializer(), "{}") +} diff --git a/build.gradle b/build.gradle index 236943b8b..a7c19b85f 100644 --- a/build.gradle +++ b/build.gradle @@ -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' diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index ff35a9f15..66e318ed0 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -16,6 +16,11 @@ import kotlinx.serialization.modules.* import kotlin.jvm.* import kotlin.reflect.* +/** + * A wrapper for a factory of serializers of non-parameterized types that can cache them. + */ +private val CACHED_SERIALIZER_FACTORY = createCachedFactoryWrapper { it.serializerOrNull()?.cast() } + /** * Retrieves a serializer for the given type [T]. * This method is a reified version of `serializer(KType)`. @@ -79,26 +84,44 @@ 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? = when { - typeArguments.isEmpty() -> rootClass.serializerOrNull() ?: getContextual(rootClass) - else -> builtinSerializer(typeArguments, rootClass, failOnMissingTypeArgSerializer) - }?.cast() - return result?.nullable(isNullable) + + val serializer: KSerializer? = if (typeArguments.isEmpty()) { + // if serializer cached - return it immediately because nullable already processed + CACHED_SERIALIZER_FACTORY.get(rootClass, isNullable)?.let { return it } + getContextual(rootClass) + } else { + val serializers = if (failOnMissingTypeArgSerializer) + typeArguments.map(::serializer) + else { + typeArguments.map { serializerOrNull(it) ?: return null } + } + rootClass.parametrizedSerializerOrNull(typeArguments, serializers) ?: getContextual(rootClass, serializers) + } + + return serializer?.cast()?.nullable(isNullable) +} + +private fun KClass.parametrizedSerializerOrNull(types: List, serializers: List>): KSerializer? = + // builtin first because some standard parametrized interfaces (e.g. Map) must use builtin serializer but not polymorphic + builtinParametrizedSerializer(types, serializers) ?: compiledParametrizedSerializerImpl(serializers) + + +private fun KClass.compiledParametrizedSerializerImpl(serializers: List>): KSerializer? { + // TODO remove after review! + // useless because same constructSerializerForGivenTypeArgs actually wil be called below? + // rootClass.compiledSerializerImpl() ?: + // useless because BUILTIN_SERIALIZERS types are non-paramtrized + // rootClass.builtinSerializerOrNull() + return constructSerializerForGivenTypeArgs(*serializers.toTypedArray()) } @OptIn(ExperimentalSerializationApi::class) -private fun SerializersModule.builtinSerializer( +private fun KClass.builtinParametrizedSerializer( typeArguments: List, - rootClass: KClass, - failOnMissingTypeArgSerializer: Boolean + serializers: List>, ): KSerializer? { - val serializers = if (failOnMissingTypeArgSerializer) - typeArguments.map(::serializer) - else { - typeArguments.map { serializerOrNull(it) ?: return null } - } // Array is not supported, see KT-32839 - return when (rootClass) { + 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]) @@ -111,24 +134,15 @@ private fun SerializersModule.builtinSerializer( Pair::class -> PairSerializer(serializers[0], serializers[1]) Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2]) else -> { - if (isReferenceArray(rootClass)) { - return ArraySerializer(typeArguments[0].classifier as KClass, serializers[0]).cast() + if (isReferenceArray(this)) { + return ArraySerializer(typeArguments[0].classifier as KClass, serializers[0]).cast() + } else { + return null } - val args = serializers.toTypedArray() - rootClass.constructSerializerForGivenTypeArgs(*args) - ?: reflectiveOrContextual(rootClass, serializers) } } } -@OptIn(ExperimentalSerializationApi::class) -internal fun SerializersModule.reflectiveOrContextual( - kClass: KClass, - typeArgumentsSerializers: List> -): KSerializer? { - return kClass.serializerOrNull() ?: getContextual(kClass, typeArgumentsSerializers) -} - /** * Retrieves a [KSerializer] for the given [KClass]. * The given class must be annotated with [Serializable] or be one of the built-in types. @@ -179,7 +193,7 @@ public fun KClass.serializer(): KSerializer = serializerOrNull() public fun KClass.serializerOrNull(): KSerializer? = compiledSerializerImpl() ?: builtinSerializerOrNull() -private fun KSerializer.nullable(shouldBeNullable: Boolean): KSerializer { +internal fun KSerializer.nullable(shouldBeNullable: Boolean): KSerializer { if (shouldBeNullable) return nullable return this as KSerializer } diff --git a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt index fde2c36a8..cd68118b0 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt @@ -130,6 +130,12 @@ internal expect fun BooleanArray.getChecked(index: Int): Boolean internal expect fun KClass.compiledSerializerImpl(): KSerializer? +/** + * Create caching wrapping over non-parametrized serializer factory. + * The activity and type of cache is determined for a specific platform and a specific environment. + */ +internal expect fun createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer?): CachedSerializerFactory + internal expect fun ArrayList.toNativeArrayImpl(eClass: KClass): Array /** @@ -145,3 +151,10 @@ internal expect fun Any.isInstanceOf(kclass: KClass<*>): Boolean internal inline fun Iterable.elementsHashCodeBy(selector: (T) -> K): Int { return fold(1) { hash, element -> 31 * hash + selector(element).hashCode() } } + +/** + * Wrapper over non-parametrized serializer factory. + */ +internal interface CachedSerializerFactory { + fun get(key: KClass<*>, isNullable: Boolean): KSerializer? +} diff --git a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt index 25c481465..bc654a999 100644 --- a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt @@ -20,6 +20,12 @@ internal actual fun BooleanArray.getChecked(index: Int): Boolean { internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() ?: this.js.asDynamic().Companion?.serializer() as? KSerializer +internal actual fun createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer?): CachedSerializerFactory { + return object : CachedSerializerFactory { + override fun get(key: KClass<*>, isNullable: Boolean): KSerializer? = factory(key)?.nullable(isNullable) + } +} + internal actual fun ArrayList.toNativeArrayImpl(eClass: KClass): Array = toTypedArray() internal actual fun Any.isInstanceOf(kclass: KClass<*>): Boolean = kclass.isInstance(this) diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt b/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt new file mode 100644 index 000000000..68e66c7f4 --- /dev/null +++ b/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.internal + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.nullable +import java.util.concurrent.ConcurrentHashMap +import kotlin.reflect.KClass +/* + * By default, we use ClassValue-based caches to avoid classloader leaks, + * but ClassValue is not available on Android, thus we attempt to check it dynamically + * and fallback to ConcurrentHashMap-based cache. + */ +private val useClassValue = runCatching { + Class.forName("java.lang.ClassValue") +}.map { true }.getOrDefault(false) + +/** + * Creates a **strongly referenced** cache of values associated with [Class]. + * Values are computed using provided [factory] function. + * + * `null` values are not supported, though there aren't any technical limitations. + */ +internal actual fun createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer?): CachedSerializerFactory { + return if (useClassValue) ClassValueCache(factory) else ConcurrentHashMapCache(factory) +} + +@SuppressAnimalSniffer +private class ClassValueCache(private val compute: (KClass<*>) -> KSerializer?) : CachedSerializerFactory { + private val classValue = initClassValue() + + private fun initClassValue() = object : ClassValue() { + override fun computeValue(type: Class<*>): CacheEntry { + val pair = compute(type.kotlin)?.let { SerializerPair(it, it.nullable) } + return CacheEntry(pair) + } + } + + override fun get(key: KClass<*>, isNullable: Boolean): KSerializer? = + classValue[key.java].serializer(isNullable) +} + +/** + * We no longer support Java 6, so the only place we use this cache is Android, where there + * are no classloader leaks issue, thus we can safely use strong references and do not bother + * with WeakReference wrapping. + */ +private class ConcurrentHashMapCache(private val compute: (KClass<*>) -> KSerializer?) : CachedSerializerFactory { + private val cache = ConcurrentHashMap, CacheEntry>() + + override fun get(key: KClass<*>, isNullable: Boolean): KSerializer? { + return cache.getOrPut(key.java) { + val pair = compute(key)?.let { SerializerPair(it, it.nullable) } + CacheEntry(pair) + }.serializer(isNullable) + } +} + +private class SerializerPair(val nonNull: KSerializer, val nullable: KSerializer) + +private class CacheEntry(private val serializers: SerializerPair?) { + fun serializer(isNullable: Boolean): KSerializer? { + return serializers?.let { if (isNullable) it.nullable else it.nonNull }?.cast() + } +} + diff --git a/core/jvmMain/src/kotlinx/serialization/internal/SuppressAnimalSniffer.kt b/core/jvmMain/src/kotlinx/serialization/internal/SuppressAnimalSniffer.kt new file mode 100644 index 000000000..7b3cc3104 --- /dev/null +++ b/core/jvmMain/src/kotlinx/serialization/internal/SuppressAnimalSniffer.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2017-2022 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.serialization.internal + +/** + * Suppresses Animal Sniffer plugin errors for certain classes. + * Such classes are not available in Android API, but used only for JVM. + */ +@Retention(AnnotationRetention.BINARY) +@Target(AnnotationTarget.CLASS) +internal annotation class SuppressAnimalSniffer diff --git a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt index e24c18204..f254f0fee 100644 --- a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt @@ -45,6 +45,12 @@ internal actual fun KClass.constructSerializerForGivenTypeArgs(vara internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() +internal actual fun createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer?): CachedSerializerFactory { + return object : CachedSerializerFactory { + override fun get(key: KClass<*>, isNullable: Boolean): KSerializer? = factory(key)?.nullable(isNullable) + } +} + internal actual fun ArrayList.toNativeArrayImpl(eClass: KClass): Array { val result = arrayOfAnyNulls(size) var index = 0 From 6360cfab8527ff15b4a85cd60bbfbd3a807605dd Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Thu, 18 Aug 2022 17:14:13 +0200 Subject: [PATCH 2/7] Marked common val as @SharedImmutable --- core/commonMain/src/kotlinx/serialization/Serializers.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index 66e318ed0..fb39b75f7 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -14,11 +14,13 @@ import kotlinx.serialization.builtins.TripleSerializer import kotlinx.serialization.internal.* import kotlinx.serialization.modules.* import kotlin.jvm.* +import kotlin.native.concurrent.SharedImmutable import kotlin.reflect.* /** * A wrapper for a factory of serializers of non-parameterized types that can cache them. */ +@SharedImmutable private val CACHED_SERIALIZER_FACTORY = createCachedFactoryWrapper { it.serializerOrNull()?.cast() } /** From 0c6a0a53269332b94f69e9121c902290b78031f8 Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Wed, 24 Aug 2022 21:56:25 +0200 Subject: [PATCH 3/7] Parametrized serializers caching --- .../src/kotlinx/serialization/Serializers.kt | 152 +++++++++++------- .../serialization/internal/Platform.common.kt | 30 +++- .../serialization/internal/Platform.kt | 16 +- .../kotlinx/serialization/internal/Caching.kt | 86 ++++++++-- .../serialization/internal/Platform.kt | 18 ++- .../serialization/internal/Platform.kt | 17 +- 6 files changed, 231 insertions(+), 88 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index fb39b75f7..6c0e8e8b7 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -18,10 +18,19 @@ import kotlin.native.concurrent.SharedImmutable import kotlin.reflect.* /** - * A wrapper for a factory of serializers of non-parameterized types that can cache them. + * Cache class for non-parametrized and non-contextual serializers. */ @SharedImmutable -private val CACHED_SERIALIZER_FACTORY = createCachedFactoryWrapper { it.serializerOrNull()?.cast() } +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]. @@ -62,9 +71,7 @@ public fun serializerOrNull(type: KType): KSerializer? = 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 = - serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass() - .platformSpecificSerializerNotRegistered() +public fun SerializersModule.serializer(type: KType): KSerializer = serializerCommon(type) /** * Attempts to create a serializer for the given [type] and fallbacks to [contextual][SerializersModule.getContextual] @@ -73,12 +80,20 @@ public fun SerializersModule.serializer(type: KType): KSerializer = * 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? { +public fun SerializersModule.serializerOrNull(type: KType): KSerializer? = serializerOrNullCommon(type) + + +private fun SerializersModule?.serializerCommon(type: KType): KSerializer = + serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass() + .platformSpecificSerializerNotRegistered() + +private fun SerializersModule?.serializerOrNullCommon(type: KType): KSerializer? { return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false) } +// the `this` is empty if there is no need to search for a contextual serializer @OptIn(ExperimentalSerializationApi::class) -private fun SerializersModule.serializerByKTypeImpl( +private fun SerializersModule?.serializerByKTypeImpl( type: KType, failOnMissingTypeArgSerializer: Boolean ): KSerializer? { @@ -87,64 +102,50 @@ private fun SerializersModule.serializerByKTypeImpl( val typeArguments = type.arguments .map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } } - val serializer: KSerializer? = if (typeArguments.isEmpty()) { - // if serializer cached - return it immediately because nullable already processed - CACHED_SERIALIZER_FACTORY.get(rootClass, isNullable)?.let { return it } - getContextual(rootClass) + val cachedSerializer = if (typeArguments.isEmpty()) { + SERIALIZERS_CACHE.get(rootClass, isNullable) } else { - val serializers = if (failOnMissingTypeArgSerializer) - typeArguments.map(::serializer) - else { - typeArguments.map { serializerOrNull(it) ?: return null } + 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 } } - rootClass.parametrizedSerializerOrNull(typeArguments, serializers) ?: getContextual(rootClass, serializers) } + cachedSerializer?.let { return it } - return serializer?.cast()?.nullable(isNullable) -} - -private fun KClass.parametrizedSerializerOrNull(types: List, serializers: List>): KSerializer? = - // builtin first because some standard parametrized interfaces (e.g. Map) must use builtin serializer but not polymorphic - builtinParametrizedSerializer(types, serializers) ?: compiledParametrizedSerializerImpl(serializers) - - -private fun KClass.compiledParametrizedSerializerImpl(serializers: List>): KSerializer? { - // TODO remove after review! - // useless because same constructSerializerForGivenTypeArgs actually wil be called below? - // rootClass.compiledSerializerImpl() ?: - // useless because BUILTIN_SERIALIZERS types are non-paramtrized - // rootClass.builtinSerializerOrNull() - return constructSerializerForGivenTypeArgs(*serializers.toTypedArray()) + // slow path to find contextual serializers in serializers module + val contextualSerializer: KSerializer? = 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 + ) + } + return contextualSerializer?.cast()?.nullable(isNullable) } -@OptIn(ExperimentalSerializationApi::class) -private fun KClass.builtinParametrizedSerializer( +/** + * Returns null only if `failOnMissingTypeArgSerializer == false` and at least one parameter serializer not found. + */ +private fun SerializersModule?.serializersForParameters( typeArguments: List, - serializers: List>, -): KSerializer? { - // 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, serializers[0]).cast() - } else { - return null - } - } + failOnMissingTypeArgSerializer: Boolean +): List>? { + val serializers = if (failOnMissingTypeArgSerializer) { + typeArguments.map { this.serializerCommon(it) } + } else { + typeArguments.map { this.serializerOrNullCommon(it) ?: return null } } + return serializers } + + + /** * Retrieves a [KSerializer] for the given [KClass]. * The given class must be annotated with [Serializable] or be one of the built-in types. @@ -195,6 +196,47 @@ public fun KClass.serializer(): KSerializer = serializerOrNull() public fun KClass.serializerOrNull(): KSerializer? = compiledSerializerImpl() ?: builtinSerializerOrNull() +private fun KClass.parametrizedSerializerOrNull( + types: List, + serializers: List> +): KSerializer? { + // 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.compiledParametrizedSerializer(serializers: List>): KSerializer? { + return constructSerializerForGivenTypeArgs(*serializers.toTypedArray()) +} + +@OptIn(ExperimentalSerializationApi::class) +private fun KClass.builtinParametrizedSerializer( + typeArguments: List, + serializers: List>, +): KSerializer? { + // 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, serializers[0]) + } else { + return null + } + } + } +} + internal fun KSerializer.nullable(shouldBeNullable: Boolean): KSerializer { if (shouldBeNullable) return nullable return this as KSerializer diff --git a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt index cd68118b0..cc735a42a 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt @@ -131,10 +131,16 @@ internal expect fun BooleanArray.getChecked(index: Int): Boolean internal expect fun KClass.compiledSerializerImpl(): KSerializer? /** - * Create caching wrapping over non-parametrized serializer factory. + * 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 createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer?): CachedSerializerFactory +internal expect fun createCache(factory: (KClass<*>) -> KSerializer?): 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, List) -> KSerializer?): ParametrizedSerializerCache internal expect fun ArrayList.toNativeArrayImpl(eClass: KClass): Array @@ -153,8 +159,22 @@ internal inline fun Iterable.elementsHashCodeBy(selector: (T) -> K): I } /** - * Wrapper over non-parametrized serializer factory. + * 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, isNullable: Boolean): KSerializer? +} + +/** + * Cache class for parametrized and non-contextual serializers. */ -internal interface CachedSerializerFactory { - fun get(key: KClass<*>, isNullable: Boolean): KSerializer? +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, isNullable: Boolean, types: List = emptyList()): Result?> } diff --git a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt index bc654a999..d74edae17 100644 --- a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt @@ -20,9 +20,19 @@ internal actual fun BooleanArray.getChecked(index: Int): Boolean { internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() ?: this.js.asDynamic().Companion?.serializer() as? KSerializer -internal actual fun createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer?): CachedSerializerFactory { - return object : CachedSerializerFactory { - override fun get(key: KClass<*>, isNullable: Boolean): KSerializer? = factory(key)?.nullable(isNullable) +internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { + return object: SerializerCache { + override fun get(key: KClass, isNullable: Boolean): KSerializer? { + return factory(key)?.nullable(isNullable) + } + } +} + +internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { + return object: ParametrizedSerializerCache { + override fun get(key: KClass, isNullable: Boolean, types: List): Result?> { + return kotlin.runCatching { factory(key, types)?.nullable(isNullable) } + } } } diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt b/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt index 68e66c7f4..9a481a32f 100644 --- a/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt +++ b/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt @@ -8,6 +8,8 @@ import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.nullable import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass +import kotlin.reflect.KType + /* * By default, we use ClassValue-based caches to avoid classloader leaks, * but ClassValue is not available on Android, thus we attempt to check it dynamically @@ -19,27 +21,53 @@ private val useClassValue = runCatching { /** * Creates a **strongly referenced** cache of values associated with [Class]. - * Values are computed using provided [factory] function. + * Serializers are computed using provided [factory] function. * * `null` values are not supported, though there aren't any technical limitations. */ -internal actual fun createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer?): CachedSerializerFactory { +internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { return if (useClassValue) ClassValueCache(factory) else ConcurrentHashMapCache(factory) } +/** + * Creates a **strongly referenced** cache of values associated with [Class]. + * Serializers are computed using provided [factory] function. + * + * `null` values are not supported, though there aren't any technical limitations. + */ +internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { + return if (useClassValue) ClassValueParametrizedCache(factory) else ConcurrentHashMapParametrizedCache(factory) +} + @SuppressAnimalSniffer -private class ClassValueCache(private val compute: (KClass<*>) -> KSerializer?) : CachedSerializerFactory { +private class ClassValueCache(private val compute: (KClass<*>) -> KSerializer?) : SerializerCache { private val classValue = initClassValue() private fun initClassValue() = object : ClassValue() { override fun computeValue(type: Class<*>): CacheEntry { - val pair = compute(type.kotlin)?.let { SerializerPair(it, it.nullable) } + val pair = SerializerPair.from(compute(type.kotlin)) return CacheEntry(pair) } } - override fun get(key: KClass<*>, isNullable: Boolean): KSerializer? = - classValue[key.java].serializer(isNullable) + override fun get(key: KClass, isNullable: Boolean): KSerializer? = + classValue[key.java].serializers?.get(isNullable) +} + +@SuppressAnimalSniffer +private class ClassValueParametrizedCache(private val compute: (KClass, List) -> KSerializer?) : ParametrizedSerializerCache { + private val classValue = initClassValue() + + private fun initClassValue() = object : ClassValue() { + override fun computeValue(type: Class<*>): ParametrizedCacheEntry { + return ParametrizedCacheEntry() + } + } + + override fun get(key: KClass, isNullable: Boolean, types: List): Result?> = + classValue[key.java] + .computeIfAbsent(types) { compute(key, types) } + .map { it?.get(isNullable) } } /** @@ -47,22 +75,50 @@ private class ClassValueCache(private val compute: (KClass<*>) -> KSerializer) -> KSerializer?) : CachedSerializerFactory { +private class ConcurrentHashMapCache(private val compute: (KClass<*>) -> KSerializer?) : SerializerCache { private val cache = ConcurrentHashMap, CacheEntry>() - override fun get(key: KClass<*>, isNullable: Boolean): KSerializer? { + override fun get(key: KClass, isNullable: Boolean): KSerializer? { return cache.getOrPut(key.java) { - val pair = compute(key)?.let { SerializerPair(it, it.nullable) } - CacheEntry(pair) - }.serializer(isNullable) + CacheEntry(SerializerPair.from(compute(key))) + }.serializers?.get(isNullable) } } -private class SerializerPair(val nonNull: KSerializer, val nullable: KSerializer) -private class CacheEntry(private val serializers: SerializerPair?) { - fun serializer(isNullable: Boolean): KSerializer? { - return serializers?.let { if (isNullable) it.nullable else it.nonNull }?.cast() + +private class ConcurrentHashMapParametrizedCache(private val compute: (KClass, List) -> KSerializer?) : ParametrizedSerializerCache { + private val cache = ConcurrentHashMap, ParametrizedCacheEntry>() + + override fun get(key: KClass, isNullable: Boolean, types: List): Result?> { + return cache.getOrPut(key.java) { ParametrizedCacheEntry() } + .computeIfAbsent(types) { compute(key, types) } + .map { it?.get(isNullable) } + } +} + + + +private class SerializerPair(private val nonNull: KSerializer, private val nullable: KSerializer) { + fun get(isNullable: Boolean): KSerializer { + return (if (isNullable) nullable else nonNull).cast() + } + + companion object { + fun from(serializer: KSerializer?): SerializerPair? { + return serializer?.let { SerializerPair(it, it.nullable) } + } + } +} + +private class CacheEntry(@JvmField val serializers: SerializerPair?) + +private class ParametrizedCacheEntry { + private val serializers: ConcurrentHashMap, Result> = ConcurrentHashMap() + inline fun computeIfAbsent(types: List, producer: () -> KSerializer?): Result { + return serializers.getOrPut(types) { + kotlin.runCatching { SerializerPair.from(producer()) } + } } } diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt index 7c16650b8..1feb842e5 100644 --- a/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jvmMain/src/kotlinx/serialization/internal/Platform.kt @@ -61,7 +61,11 @@ internal fun Class.constructSerializerForGivenTypeArgs(vararg args: } if (fromNamedCompanion != null) return fromNamedCompanion // Check for polymorphic - return polymorphicSerializer() + return if (isPolymorphicSerializer()) { + PolymorphicSerializer(this.kotlin) + } else { + null + } } private fun Class.isNotAnnotated(): Boolean { @@ -72,19 +76,19 @@ private fun Class.isNotAnnotated(): Boolean { getAnnotation(Polymorphic::class.java) == null } -private fun Class.polymorphicSerializer(): KSerializer? { +private fun Class.isPolymorphicSerializer(): Boolean { /* * Last resort: check for @Polymorphic or Serializable(with = PolymorphicSerializer::class) * annotations. */ if (getAnnotation(Polymorphic::class.java) != null) { - return PolymorphicSerializer(this.kotlin) + return true } val serializable = getAnnotation(Serializable::class.java) if (serializable != null && serializable.with == PolymorphicSerializer::class) { - return PolymorphicSerializer(this.kotlin) + return true } - return null + return false } private fun Class.interfaceSerializer(): KSerializer? { @@ -125,9 +129,9 @@ private fun Class<*>.companionOrNull() = } @Suppress("UNCHECKED_CAST") -private fun Class.createEnumSerializer(): KSerializer? { +private fun Class.createEnumSerializer(): KSerializer { val constants = enumConstants - return EnumSerializer(canonicalName, constants as Array>) as? KSerializer + return EnumSerializer(canonicalName, constants as Array>) as KSerializer } private fun Class.findObjectSerializer(): KSerializer? { diff --git a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt index f254f0fee..af7404c2f 100644 --- a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt @@ -45,9 +45,20 @@ internal actual fun KClass.constructSerializerForGivenTypeArgs(vara internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() -internal actual fun createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer?): CachedSerializerFactory { - return object : CachedSerializerFactory { - override fun get(key: KClass<*>, isNullable: Boolean): KSerializer? = factory(key)?.nullable(isNullable) + +internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { + return object: SerializerCache { + override fun get(key: KClass, isNullable: Boolean): KSerializer? { + return factory(key)?.nullable(isNullable) + } + } +} + +internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { + return object: ParametrizedSerializerCache { + override fun get(key: KClass, isNullable: Boolean, types: List): Result?> { + return kotlin.runCatching { factory(key, types)?.nullable(isNullable) } + } } } From f6eb35f4727d20fc8c8599da1a514cb46043f417 Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Wed, 24 Aug 2022 22:00:08 +0200 Subject: [PATCH 4/7] review fixes --- .../kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt | 4 ++++ core/commonMain/src/kotlinx/serialization/Serializers.kt | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt index 689fe3ceb..04bf64424 100644 --- a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt @@ -1,3 +1,7 @@ +/* + * 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.* diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index 6c0e8e8b7..9d3077743 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -209,7 +209,6 @@ private fun KClass.compiledParametrizedSerializer(serializers: List.builtinParametrizedSerializer( typeArguments: List, serializers: List>, From 9655d85d8a2bc007b572a5963595aaafd635c58a Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Wed, 24 Aug 2022 23:02:34 +0200 Subject: [PATCH 5/7] ~fix --- core/commonMain/src/kotlinx/serialization/Serializers.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index 9d3077743..6c0e8e8b7 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -209,6 +209,7 @@ private fun KClass.compiledParametrizedSerializer(serializers: List.builtinParametrizedSerializer( typeArguments: List, serializers: List>, From fb14eafd1e2e12d8a2e7abdde94db0997855d6bf Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Fri, 26 Aug 2022 13:02:08 +0200 Subject: [PATCH 6/7] ~remove nullability of serializers module --- .../src/kotlinx/serialization/Serializers.kt | 33 +++++++------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index 6c0e8e8b7..6a9436776 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -28,7 +28,7 @@ private val SERIALIZERS_CACHE = createCache { it.serializerOrNull()?.cast() } */ @SharedImmutable private val PARAMETRIZED_SERIALIZERS_CACHE = createParametrizedCache { clazz, types -> - val serializers = null.serializersForParameters(types, true)!! + val serializers = EmptySerializersModule().serializersForParameters(types, true)!! clazz.parametrizedSerializerOrNull(types, serializers)?.cast() } @@ -71,7 +71,9 @@ public fun serializerOrNull(type: KType): KSerializer? = 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 = serializerCommon(type) +public fun SerializersModule.serializer(type: KType): KSerializer = + serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass() + .platformSpecificSerializerNotRegistered() /** * Attempts to create a serializer for the given [type] and fallbacks to [contextual][SerializersModule.getContextual] @@ -80,20 +82,12 @@ public fun SerializersModule.serializer(type: KType): KSerializer = serial * 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? = serializerOrNullCommon(type) - - -private fun SerializersModule?.serializerCommon(type: KType): KSerializer = - serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = true) ?: type.kclass() - .platformSpecificSerializerNotRegistered() - -private fun SerializersModule?.serializerOrNullCommon(type: KType): KSerializer? { - return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false) -} +public fun SerializersModule.serializerOrNull(type: KType): KSerializer? = + serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false) // the `this` is empty if there is no need to search for a contextual serializer @OptIn(ExperimentalSerializationApi::class) -private fun SerializersModule?.serializerByKTypeImpl( +private fun SerializersModule.serializerByKTypeImpl( type: KType, failOnMissingTypeArgSerializer: Boolean ): KSerializer? { @@ -116,11 +110,11 @@ private fun SerializersModule?.serializerByKTypeImpl( // slow path to find contextual serializers in serializers module val contextualSerializer: KSerializer? = if (typeArguments.isEmpty()) { - this?.getContextual(rootClass) + 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.parametrizedSerializerOrNull(typeArguments, serializers) ?: getContextual( rootClass, serializers ) @@ -131,21 +125,18 @@ private fun SerializersModule?.serializerByKTypeImpl( /** * Returns null only if `failOnMissingTypeArgSerializer == false` and at least one parameter serializer not found. */ -private fun SerializersModule?.serializersForParameters( +private fun SerializersModule.serializersForParameters( typeArguments: List, failOnMissingTypeArgSerializer: Boolean ): List>? { val serializers = if (failOnMissingTypeArgSerializer) { - typeArguments.map { this.serializerCommon(it) } + typeArguments.map { serializer(it) } } else { - typeArguments.map { this.serializerOrNullCommon(it) ?: return null } + typeArguments.map { serializerOrNull(it) ?: return null } } return serializers } - - - /** * Retrieves a [KSerializer] for the given [KClass]. * The given class must be annotated with [Serializable] or be one of the built-in types. From 3c86e420306c1708e703dbde89c7e44caf707753 Mon Sep 17 00:00:00 2001 From: "Sergey.Shanshin" Date: Fri, 2 Sep 2022 20:50:00 +0200 Subject: [PATCH 7/7] ~review fixes --- .../json/LookupOverheadBenchmark.kt | 20 +++++ .../src/kotlinx/serialization/Serializers.kt | 34 +++----- .../kotlinx/serialization/SerializersCache.kt | 74 ++++++++++++++++++ .../serialization/internal/Platform.common.kt | 12 +-- .../serialization/internal/Platform.kt | 16 ++-- .../kotlinx/serialization/internal/Caching.kt | 78 ++++++++----------- .../serialization/internal/Platform.kt | 16 ++-- 7 files changed, 158 insertions(+), 92 deletions(-) create mode 100644 core/commonMain/src/kotlinx/serialization/SerializersCache.kt diff --git a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt index 04bf64424..ddfc57922 100644 --- a/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt @@ -24,7 +24,15 @@ open class LookupOverheadBenchmark { @Serializable class Generic(val a: T) + @Serializable + class DoubleGeneric(val a: T1, val b: T2) + + @Serializable + class PentaGeneric(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 @@ -41,6 +49,18 @@ open class LookupOverheadBenchmark { @Benchmark fun genericPlain() = Json.decodeFromString(Generic.serializer(String.serializer()), data) + @Benchmark + fun doubleGenericReified() = Json.decodeFromString>(doubleData) + + @Benchmark + fun doubleGenericPlain() = Json.decodeFromString(DoubleGeneric.serializer(String.serializer(), Int.serializer()), doubleData) + + @Benchmark + fun pentaGenericReified() = Json.decodeFromString>(pentaData) + + @Benchmark + fun pentaGenericPlain() = Json.decodeFromString(PentaGeneric.serializer(String.serializer(), Int.serializer(), Long.serializer(), Boolean.serializer(), Char.serializer()), pentaData) + @Benchmark fun objectReified() = Json.decodeFromString("{}") diff --git a/core/commonMain/src/kotlinx/serialization/Serializers.kt b/core/commonMain/src/kotlinx/serialization/Serializers.kt index 6a9436776..8063e1a80 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -14,24 +14,8 @@ 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 -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 = EmptySerializersModule().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)`. @@ -85,7 +69,6 @@ public fun SerializersModule.serializer(type: KType): KSerializer = public fun SerializersModule.serializerOrNull(type: KType): KSerializer? = serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false) -// the `this` is empty if there is no need to search for a contextual serializer @OptIn(ExperimentalSerializationApi::class) private fun SerializersModule.serializerByKTypeImpl( type: KType, @@ -97,13 +80,14 @@ private fun SerializersModule.serializerByKTypeImpl( .map { requireNotNull(it.type) { "Star projections in type arguments are not allowed, but had $type" } } val cachedSerializer = if (typeArguments.isEmpty()) { - SERIALIZERS_CACHE.get(rootClass, isNullable) + findCachedSerializer(rootClass, isNullable) } else { + val cachedResult = findParametrizedCachedSerializer(rootClass, typeArguments, isNullable) if (failOnMissingTypeArgSerializer) { - PARAMETRIZED_SERIALIZERS_CACHE.get(rootClass, isNullable, typeArguments).getOrNull() + cachedResult.getOrNull() } else { // return null if error occurred - serializer for parameter(s) was not found - PARAMETRIZED_SERIALIZERS_CACHE.get(rootClass, isNullable, typeArguments).getOrElse { return null } + cachedResult.getOrElse { return null } } } cachedSerializer?.let { return it } @@ -125,7 +109,7 @@ private fun SerializersModule.serializerByKTypeImpl( /** * Returns null only if `failOnMissingTypeArgSerializer == false` and at least one parameter serializer not found. */ -private fun SerializersModule.serializersForParameters( +internal fun SerializersModule.serializersForParameters( typeArguments: List, failOnMissingTypeArgSerializer: Boolean ): List>? { @@ -187,7 +171,7 @@ public fun KClass.serializer(): KSerializer = serializerOrNull() public fun KClass.serializerOrNull(): KSerializer? = compiledSerializerImpl() ?: builtinSerializerOrNull() -private fun KClass.parametrizedSerializerOrNull( +internal fun KClass.parametrizedSerializerOrNull( types: List, serializers: List> ): KSerializer? { @@ -220,15 +204,15 @@ private fun KClass.builtinParametrizedSerializer( Triple::class -> TripleSerializer(serializers[0], serializers[1], serializers[2]) else -> { if (isReferenceArray(this)) { - return ArraySerializer(typeArguments[0].classifier as KClass, serializers[0]) + ArraySerializer(typeArguments[0].classifier as KClass, serializers[0]) } else { - return null + null } } } } -internal fun KSerializer.nullable(shouldBeNullable: Boolean): KSerializer { +private fun KSerializer.nullable(shouldBeNullable: Boolean): KSerializer { if (shouldBeNullable) return nullable return this as KSerializer } diff --git a/core/commonMain/src/kotlinx/serialization/SerializersCache.kt b/core/commonMain/src/kotlinx/serialization/SerializersCache.kt new file mode 100644 index 000000000..b1481884d --- /dev/null +++ b/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 { 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 { 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, isNullable: Boolean): KSerializer? { + 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, + types: List, + isNullable: Boolean +): Result?> { + return if (!isNullable) { + @Suppress("UNCHECKED_CAST") + PARAMETRIZED_SERIALIZERS_CACHE.get(clazz, types) as Result?> + } else { + PARAMETRIZED_SERIALIZERS_CACHE_NULLABLE.get(clazz, types) + } +} diff --git a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt index cc735a42a..9c4bad7cb 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt @@ -134,13 +134,13 @@ internal expect fun KClass.compiledSerializerImpl(): KSerializer * 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?): SerializerCache +internal expect fun createCache(factory: (KClass<*>) -> KSerializer?): 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, List) -> KSerializer?): ParametrizedSerializerCache +internal expect fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache internal expect fun ArrayList.toNativeArrayImpl(eClass: KClass): Array @@ -161,20 +161,20 @@ internal inline fun Iterable.elementsHashCodeBy(selector: (T) -> K): I /** * Cache class for non-parametrized and non-contextual serializers. */ -internal interface SerializerCache { +internal interface SerializerCache { /** * Returns cached serializer or `null` if serializer not found. */ - fun get(key: KClass, isNullable: Boolean): KSerializer? + fun get(key: KClass): KSerializer? } /** * Cache class for parametrized and non-contextual serializers. */ -internal interface ParametrizedSerializerCache { +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, isNullable: Boolean, types: List = emptyList()): Result?> + fun get(key: KClass, types: List = emptyList()): Result?> } diff --git a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt index d74edae17..a9534ddab 100644 --- a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt @@ -20,18 +20,18 @@ internal actual fun BooleanArray.getChecked(index: Int): Boolean { internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() ?: this.js.asDynamic().Companion?.serializer() as? KSerializer -internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { - return object: SerializerCache { - override fun get(key: KClass, isNullable: Boolean): KSerializer? { - return factory(key)?.nullable(isNullable) +internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { + return object: SerializerCache { + override fun get(key: KClass): KSerializer? { + return factory(key) } } } -internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { - return object: ParametrizedSerializerCache { - override fun get(key: KClass, isNullable: Boolean, types: List): Result?> { - return kotlin.runCatching { factory(key, types)?.nullable(isNullable) } +internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { + return object: ParametrizedSerializerCache { + override fun get(key: KClass, types: List): Result?> { + return kotlin.runCatching { factory(key, types) } } } } diff --git a/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt b/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt index 9a481a32f..39cec9369 100644 --- a/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt +++ b/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt @@ -5,7 +5,6 @@ package kotlinx.serialization.internal import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.nullable import java.util.concurrent.ConcurrentHashMap import kotlin.reflect.KClass import kotlin.reflect.KType @@ -25,7 +24,7 @@ private val useClassValue = runCatching { * * `null` values are not supported, though there aren't any technical limitations. */ -internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { +internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { return if (useClassValue) ClassValueCache(factory) else ConcurrentHashMapCache(factory) } @@ -35,39 +34,43 @@ internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): Seri * * `null` values are not supported, though there aren't any technical limitations. */ -internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { +internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { return if (useClassValue) ClassValueParametrizedCache(factory) else ConcurrentHashMapParametrizedCache(factory) } @SuppressAnimalSniffer -private class ClassValueCache(private val compute: (KClass<*>) -> KSerializer?) : SerializerCache { +private class ClassValueCache(private val compute: (KClass<*>) -> KSerializer?) : SerializerCache { private val classValue = initClassValue() - private fun initClassValue() = object : ClassValue() { - override fun computeValue(type: Class<*>): CacheEntry { - val pair = SerializerPair.from(compute(type.kotlin)) - return CacheEntry(pair) + private fun initClassValue() = object : ClassValue>() { + /* + * Since during the computing of the value for the `ClassValue` entry, we do not know whether a nullable + * serializer is needed, so we may need to differentiate nullable/non-null caches by a level higher + */ + override fun computeValue(type: Class<*>): CacheEntry { + return CacheEntry(compute(type.kotlin)) } } - override fun get(key: KClass, isNullable: Boolean): KSerializer? = - classValue[key.java].serializers?.get(isNullable) + override fun get(key: KClass): KSerializer? = classValue[key.java].serializer } @SuppressAnimalSniffer -private class ClassValueParametrizedCache(private val compute: (KClass, List) -> KSerializer?) : ParametrizedSerializerCache { +private class ClassValueParametrizedCache(private val compute: (KClass, List) -> KSerializer?) : ParametrizedSerializerCache { private val classValue = initClassValue() - private fun initClassValue() = object : ClassValue() { - override fun computeValue(type: Class<*>): ParametrizedCacheEntry { + private fun initClassValue() = object : ClassValue>() { + /* + * Since during the computing of the value for the `ClassValue` entry, we do not know whether a nullable + * serializer is needed, so we may need to differentiate nullable/non-null caches by a level higher + */ + override fun computeValue(type: Class<*>): ParametrizedCacheEntry { return ParametrizedCacheEntry() } } - override fun get(key: KClass, isNullable: Boolean, types: List): Result?> = - classValue[key.java] - .computeIfAbsent(types) { compute(key, types) } - .map { it?.get(isNullable) } + override fun get(key: KClass, types: List): Result?> = + classValue[key.java].computeIfAbsent(types) { compute(key, types) } } /** @@ -75,49 +78,34 @@ private class ClassValueParametrizedCache(private val compute: (KClass, Lis * are no classloader leaks issue, thus we can safely use strong references and do not bother * with WeakReference wrapping. */ -private class ConcurrentHashMapCache(private val compute: (KClass<*>) -> KSerializer?) : SerializerCache { - private val cache = ConcurrentHashMap, CacheEntry>() +private class ConcurrentHashMapCache(private val compute: (KClass<*>) -> KSerializer?) : SerializerCache { + private val cache = ConcurrentHashMap, CacheEntry>() - override fun get(key: KClass, isNullable: Boolean): KSerializer? { + override fun get(key: KClass): KSerializer? { return cache.getOrPut(key.java) { - CacheEntry(SerializerPair.from(compute(key))) - }.serializers?.get(isNullable) + CacheEntry(compute(key)) + }.serializer } } -private class ConcurrentHashMapParametrizedCache(private val compute: (KClass, List) -> KSerializer?) : ParametrizedSerializerCache { - private val cache = ConcurrentHashMap, ParametrizedCacheEntry>() +private class ConcurrentHashMapParametrizedCache(private val compute: (KClass, List) -> KSerializer?) : ParametrizedSerializerCache { + private val cache = ConcurrentHashMap, ParametrizedCacheEntry>() - override fun get(key: KClass, isNullable: Boolean, types: List): Result?> { + override fun get(key: KClass, types: List): Result?> { return cache.getOrPut(key.java) { ParametrizedCacheEntry() } .computeIfAbsent(types) { compute(key, types) } - .map { it?.get(isNullable) } - } -} - - - -private class SerializerPair(private val nonNull: KSerializer, private val nullable: KSerializer) { - fun get(isNullable: Boolean): KSerializer { - return (if (isNullable) nullable else nonNull).cast() - } - - companion object { - fun from(serializer: KSerializer?): SerializerPair? { - return serializer?.let { SerializerPair(it, it.nullable) } - } } } -private class CacheEntry(@JvmField val serializers: SerializerPair?) +private class CacheEntry(@JvmField val serializer: KSerializer?) -private class ParametrizedCacheEntry { - private val serializers: ConcurrentHashMap, Result> = ConcurrentHashMap() - inline fun computeIfAbsent(types: List, producer: () -> KSerializer?): Result { +private class ParametrizedCacheEntry { + private val serializers: ConcurrentHashMap, Result?>> = ConcurrentHashMap() + inline fun computeIfAbsent(types: List, producer: () -> KSerializer?): Result?> { return serializers.getOrPut(types) { - kotlin.runCatching { SerializerPair.from(producer()) } + kotlin.runCatching { producer() } } } } diff --git a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt index af7404c2f..1c0d5ab3e 100644 --- a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt @@ -46,18 +46,18 @@ internal actual fun KClass.compiledSerializerImpl(): KSerializer this.constructSerializerForGivenTypeArgs() -internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { - return object: SerializerCache { - override fun get(key: KClass, isNullable: Boolean): KSerializer? { - return factory(key)?.nullable(isNullable) +internal actual fun createCache(factory: (KClass<*>) -> KSerializer?): SerializerCache { + return object: SerializerCache { + override fun get(key: KClass): KSerializer? { + return factory(key) } } } -internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { - return object: ParametrizedSerializerCache { - override fun get(key: KClass, isNullable: Boolean, types: List): Result?> { - return kotlin.runCatching { factory(key, types)?.nullable(isNullable) } +internal actual fun createParametrizedCache(factory: (KClass, List) -> KSerializer?): ParametrizedSerializerCache { + return object: ParametrizedSerializerCache { + override fun get(key: KClass, types: List): Result?> { + return kotlin.runCatching { factory(key, types) } } } }