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 2 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,45 @@
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
72 changes: 44 additions & 28 deletions core/commonMain/src/kotlinx/serialization/Serializers.kt
Expand Up @@ -14,8 +14,15 @@ 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
shanshin marked this conversation as resolved.
Show resolved Hide resolved
private val CACHED_SERIALIZER_FACTORY = createCachedFactoryWrapper { it.serializerOrNull()?.cast() }
shanshin marked this conversation as resolved.
Show resolved Hide resolved

/**
* Retrieves a serializer for the given type [T].
* This method is a reified version of `serializer(KType)`.
Expand Down Expand Up @@ -79,26 +86,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<Any>? = when {
typeArguments.isEmpty() -> rootClass.serializerOrNull() ?: getContextual(rootClass)
else -> builtinSerializer(typeArguments, rootClass, failOnMissingTypeArgSerializer)
}?.cast()
return result?.nullable(isNullable)

val serializer: KSerializer<out Any>? = if (typeArguments.isEmpty()) {
// if serializer cached - return it immediately because nullable already processed
shanshin marked this conversation as resolved.
Show resolved Hide resolved
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)
shanshin marked this conversation as resolved.
Show resolved Hide resolved
}

return serializer?.cast<Any>()?.nullable(isNullable)
}

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
builtinParametrizedSerializer(types, serializers) ?: compiledParametrizedSerializerImpl(serializers)


private fun KClass<Any>.compiledParametrizedSerializerImpl(serializers: List<KSerializer<Any?>>): KSerializer<out Any>? {
// 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)
shanshin marked this conversation as resolved.
Show resolved Hide resolved
private fun SerializersModule.builtinSerializer(
private fun KClass<Any>.builtinParametrizedSerializer(
typeArguments: List<KType>,
rootClass: KClass<Any>,
failOnMissingTypeArgSerializer: Boolean
serializers: List<KSerializer<Any?>>,
): KSerializer<out Any>? {
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])
Expand All @@ -111,24 +136,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<Any, Any?>(typeArguments[0].classifier as KClass<Any>, serializers[0]).cast()
if (isReferenceArray(this)) {
return ArraySerializer(typeArguments[0].classifier as KClass<Any>, serializers[0]).cast()
} else {
return null
}
val args = serializers.toTypedArray()
rootClass.constructSerializerForGivenTypeArgs(*args)
?: reflectiveOrContextual(rootClass, serializers)
}
}
}

shanshin marked this conversation as resolved.
Show resolved Hide resolved
@OptIn(ExperimentalSerializationApi::class)
internal fun <T : Any> SerializersModule.reflectiveOrContextual(
kClass: KClass<T>,
typeArgumentsSerializers: List<KSerializer<Any?>>
): KSerializer<T>? {
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.
Expand Down Expand Up @@ -179,7 +195,7 @@ 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?> {
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,12 @@ internal expect fun BooleanArray.getChecked(index: Int): Boolean

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

/**
* 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<Any>?): CachedSerializerFactory

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

/**
Expand All @@ -145,3 +151,10 @@ 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() }
}

/**
* Wrapper over non-parametrized serializer factory.
*/
internal interface CachedSerializerFactory {
fun get(key: KClass<*>, isNullable: Boolean): KSerializer<Any?>?
}
6 changes: 6 additions & 0 deletions core/jsMain/src/kotlinx/serialization/internal/Platform.kt
Expand Up @@ -20,6 +20,12 @@ 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 createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer<Any>?): CachedSerializerFactory {
return object : CachedSerializerFactory {
override fun get(key: KClass<*>, isNullable: Boolean): KSerializer<Any?>? = factory(key)?.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
68 changes: 68 additions & 0 deletions 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<Any>?): CachedSerializerFactory {
return if (useClassValue) ClassValueCache(factory) else ConcurrentHashMapCache(factory)
}

@SuppressAnimalSniffer
private class ClassValueCache(private val compute: (KClass<*>) -> KSerializer<Any>?) : CachedSerializerFactory {
private val classValue = initClassValue()

private fun initClassValue() = object : ClassValue<CacheEntry>() {
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<Any?>? =
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<Any>?) : CachedSerializerFactory {
private val cache = ConcurrentHashMap<Class<*>, CacheEntry>()

override fun get(key: KClass<*>, isNullable: Boolean): KSerializer<Any?>? {
return cache.getOrPut(key.java) {
val pair = compute(key)?.let { SerializerPair(it, it.nullable) }
CacheEntry(pair)
}.serializer(isNullable)
}
}

private class SerializerPair(val nonNull: KSerializer<out Any>, val nullable: KSerializer<out Any?>)
shanshin marked this conversation as resolved.
Show resolved Hide resolved

private class CacheEntry(private val serializers: SerializerPair?) {
fun serializer(isNullable: Boolean): KSerializer<Any?>? {
return serializers?.let { if (isNullable) it.nullable else it.nonNull }?.cast()
}
}

@@ -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
Expand Up @@ -45,6 +45,12 @@ internal actual fun <T : Any> KClass<T>.constructSerializerForGivenTypeArgs(vara
internal actual fun <T : Any> KClass<T>.compiledSerializerImpl(): KSerializer<T>? =
this.constructSerializerForGivenTypeArgs()

internal actual fun createCachedFactoryWrapper(factory: (KClass<*>) -> KSerializer<Any>?): CachedSerializerFactory {
return object : CachedSerializerFactory {
override fun get(key: KClass<*>, isNullable: Boolean): KSerializer<Any?>? = factory(key)?.nullable(isNullable)
}
}

internal actual fun <T : Any, E : T?> ArrayList<E>.toNativeArrayImpl(eClass: KClass<T>): Array<E> {
val result = arrayOfAnyNulls<E>(size)
var index = 0
Expand Down