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..ddfc57922 --- /dev/null +++ b/benchmark/src/jmh/kotlin/kotlinx/benchmarks/json/LookupOverheadBenchmark.kt @@ -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(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 + + @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 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("{}") + + @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..8063e1a80 100644 --- a/core/commonMain/src/kotlinx/serialization/Serializers.kt +++ b/core/commonMain/src/kotlinx/serialization/Serializers.kt @@ -66,9 +66,8 @@ 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? { - return serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false) -} +public fun SerializersModule.serializerOrNull(type: KType): KSerializer? = + serializerByKTypeImpl(type, failOnMissingTypeArgSerializer = false) @OptIn(ExperimentalSerializationApi::class) private fun SerializersModule.serializerByKTypeImpl( @@ -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? = 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, - rootClass: KClass, - failOnMissingTypeArgSerializer: Boolean -): KSerializer? { - 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? = 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(typeArguments[0].classifier as KClass, serializers[0]).cast() - } - val args = serializers.toTypedArray() - rootClass.constructSerializerForGivenTypeArgs(*args) - ?: reflectiveOrContextual(rootClass, serializers) - } } + return contextualSerializer?.cast()?.nullable(isNullable) } -@OptIn(ExperimentalSerializationApi::class) -internal fun SerializersModule.reflectiveOrContextual( - kClass: KClass, - typeArgumentsSerializers: List> -): KSerializer? { - 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, + failOnMissingTypeArgSerializer: Boolean +): List>? { + val serializers = if (failOnMissingTypeArgSerializer) { + typeArguments.map { serializer(it) } + } else { + typeArguments.map { serializerOrNull(it) ?: return null } + } + return serializers } /** @@ -179,6 +171,47 @@ public fun KClass.serializer(): KSerializer = serializerOrNull() public fun KClass.serializerOrNull(): KSerializer? = compiledSerializerImpl() ?: builtinSerializerOrNull() +internal 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)) { + ArraySerializer(typeArguments[0].classifier as KClass, serializers[0]) + } else { + null + } + } + } +} + 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 fde2c36a8..9c4bad7cb 100644 --- a/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt +++ b/core/commonMain/src/kotlinx/serialization/internal/Platform.common.kt @@ -130,6 +130,18 @@ internal expect fun BooleanArray.getChecked(index: Int): Boolean 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 + +/** + * 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 /** @@ -145,3 +157,24 @@ 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() } } + +/** + * 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): KSerializer? +} + +/** + * 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, 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 25c481465..a9534ddab 100644 --- a/core/jsMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/jsMain/src/kotlinx/serialization/internal/Platform.kt @@ -20,6 +20,22 @@ 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): KSerializer? { + return factory(key) + } + } +} + +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) } + } + } +} + 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..39cec9369 --- /dev/null +++ b/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt @@ -0,0 +1,112 @@ +/* + * 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 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 + * 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]. + * Serializers are computed using provided [factory] function. + * + * `null` values are not supported, though there aren't any technical limitations. + */ +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?) : SerializerCache { + private val classValue = initClassValue() + + 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): KSerializer? = classValue[key.java].serializer +} + +@SuppressAnimalSniffer +private class ClassValueParametrizedCache(private val compute: (KClass, List) -> KSerializer?) : ParametrizedSerializerCache { + private val classValue = initClassValue() + + 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, types: List): Result?> = + classValue[key.java].computeIfAbsent(types) { compute(key, types) } +} + +/** + * 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?) : SerializerCache { + private val cache = ConcurrentHashMap, CacheEntry>() + + override fun get(key: KClass): KSerializer? { + return cache.getOrPut(key.java) { + CacheEntry(compute(key)) + }.serializer + } +} + + + +private class ConcurrentHashMapParametrizedCache(private val compute: (KClass, List) -> KSerializer?) : ParametrizedSerializerCache { + private val cache = ConcurrentHashMap, ParametrizedCacheEntry>() + + override fun get(key: KClass, types: List): Result?> { + return cache.getOrPut(key.java) { ParametrizedCacheEntry() } + .computeIfAbsent(types) { compute(key, types) } + } +} + +private class CacheEntry(@JvmField val serializer: KSerializer?) + +private class ParametrizedCacheEntry { + private val serializers: ConcurrentHashMap, Result?>> = ConcurrentHashMap() + inline fun computeIfAbsent(types: List, producer: () -> KSerializer?): Result?> { + return serializers.getOrPut(types) { + kotlin.runCatching { 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/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..1c0d5ab3e 100644 --- a/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt +++ b/core/nativeMain/src/kotlinx/serialization/internal/Platform.kt @@ -45,6 +45,23 @@ internal actual fun KClass.constructSerializerForGivenTypeArgs(vara internal actual fun KClass.compiledSerializerImpl(): KSerializer? = this.constructSerializerForGivenTypeArgs() + +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, types: List): Result?> { + return kotlin.runCatching { factory(key, types) } + } + } +} + internal actual fun ArrayList.toNativeArrayImpl(eClass: KClass): Array { val result = arrayOfAnyNulls(size) var index = 0