summaryrefslogtreecommitdiff
path: root/core/jvmMain/src/kotlinx/serialization/internal/Caching.kt
blob: 191b30c17cb8f56e3e8d7d82c86a4b46694fab07 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
/*
 * 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.lang.ref.SoftReference
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
import kotlin.reflect.KClassifier
import kotlin.reflect.KType
import kotlin.reflect.KTypeProjection

/*
 * 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 = try {
    Class.forName("java.lang.ClassValue")
    true
} catch (_: Throwable) {
    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 <T> createCache(factory: (KClass<*>) -> KSerializer<T>?): SerializerCache<T> {
    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 <T> createParametrizedCache(factory: (KClass<Any>, List<KType>) -> KSerializer<T>?): ParametrizedSerializerCache<T> {
    return if (useClassValue) ClassValueParametrizedCache(factory) else ConcurrentHashMapParametrizedCache(factory)
}

private class ClassValueCache<T>(val compute: (KClass<*>) -> KSerializer<T>?) : SerializerCache<T> {
    private val classValue = ClassValueReferences<CacheEntry<T>>()

    override fun get(key: KClass<Any>): KSerializer<T>? {
        return classValue
            .getOrSet(key.java) { CacheEntry(compute(key)) }
            .serializer
    }
}

/**
 * A class that combines the capabilities of ClassValue and SoftReference.
 * Softly binds the calculated value to the specified class.
 *
 * [SoftReference] used to prevent class loaders from leaking,
 * since the value can transitively refer to an instance of type [Class], this may prevent the loader from
 * being collected during garbage collection.
 *
 * In the first calculation the value is cached, every time [getOrSet] is called, a pre-calculated value is returned.
 *
 * However, the value can be collected during garbage collection (thanks to [SoftReference])
 * - in this case, when trying to call the [getOrSet] function, the value will be calculated again and placed in the cache.
 *
 * An important requirement for a function generating a value is that it must be stable, so that each time it is called for the same class, the function returns similar values.
 * In the case of serializers, these should be instances of the same class filled with equivalent values.
 */
@SuppressAnimalSniffer
private class ClassValueReferences<T> : ClassValue<MutableSoftReference<T>>() {
    override fun computeValue(type: Class<*>): MutableSoftReference<T> {
        return MutableSoftReference()
    }

    inline fun getOrSet(key: Class<*>, crossinline factory: () -> T): T {
        val ref: MutableSoftReference<T> = get(key)

        ref.reference.get()?.let { return it }

        // go to the slow path and create serializer with blocking, also wrap factory block
        return ref.getOrSetWithLock { factory() }
    }

}

/**
 * Wrapper over `SoftReference`, used  to store a mutable value.
 */
private class MutableSoftReference<T> {
    // volatile because of situations like https://stackoverflow.com/a/7855774
    @JvmField
    @Volatile
    var reference: SoftReference<T> = SoftReference(null)

    /*
    It is important that the monitor for synchronized is the `MutableSoftReference` of a specific class
    This way access to reference is blocked only for one serializable class, and not for all
     */
    @Synchronized
    fun getOrSetWithLock(factory: () -> T): T {
        // exit function if another thread has already filled in the `reference` with non-null value
        reference.get()?.let { return it }

        val value = factory()
        reference = SoftReference(value)
        return value
    }
}

private class ClassValueParametrizedCache<T>(private val compute: (KClass<Any>, List<KType>) -> KSerializer<T>?) :
    ParametrizedSerializerCache<T> {
    private val classValue = ClassValueReferences<ParametrizedCacheEntry<T>>()

    override fun get(key: KClass<Any>, types: List<KType>): Result<KSerializer<T>?> {
        return classValue.getOrSet(key.java) { ParametrizedCacheEntry() }
            .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<T>(private val compute: (KClass<*>) -> KSerializer<T>?) : SerializerCache<T> {
    private val cache = ConcurrentHashMap<Class<*>, CacheEntry<T>>()

    override fun get(key: KClass<Any>): KSerializer<T>? {
        return cache.getOrPut(key.java) {
            CacheEntry(compute(key))
        }.serializer
    }
}


private class ConcurrentHashMapParametrizedCache<T>(private val compute: (KClass<Any>, List<KType>) -> KSerializer<T>?) :
    ParametrizedSerializerCache<T> {
    private val cache = ConcurrentHashMap<Class<*>, ParametrizedCacheEntry<T>>()

    override fun get(key: KClass<Any>, types: List<KType>): Result<KSerializer<T>?> {
        return cache.getOrPut(key.java) { ParametrizedCacheEntry() }
            .computeIfAbsent(types) { compute(key, types) }
    }
}

/**
 * Wrapper for cacheable serializer of some type.
 * Used to store cached serializer or indicates that the serializer is not cacheable.
 *
 * If serializer for type is not cacheable then value of [serializer] is `null`.
 */
private class CacheEntry<T>(@JvmField val serializer: KSerializer<T>?)

/**
 * Workaround of https://youtrack.jetbrains.com/issue/KT-54611 and https://github.com/Kotlin/kotlinx.serialization/issues/2065
 */
private class KTypeWrapper(private val origin: KType) : KType {
    override val annotations: List<Annotation>
        get() = origin.annotations
    override val arguments: List<KTypeProjection>
        get() = origin.arguments
    override val classifier: KClassifier?
        get() = origin.classifier
    override val isMarkedNullable: Boolean
        get() = origin.isMarkedNullable

    override fun equals(other: Any?): Boolean {
        if (other == null) return false
        if (origin != (other as? KTypeWrapper)?.origin) return false

        val kClassifier = classifier
        if (kClassifier is KClass<*>) {
            val otherClassifier = (other as? KType)?.classifier
            if (otherClassifier == null || otherClassifier !is KClass<*>) {
                return false
            }
            return kClassifier.java == otherClassifier.java
        } else {
            return false
        }
    }

    override fun hashCode(): Int {
        return origin.hashCode()
    }

    override fun toString(): String {
        return "KTypeWrapper: $origin"
    }
}

private class ParametrizedCacheEntry<T> {
    private val serializers: ConcurrentHashMap<List<KTypeWrapper>, Result<KSerializer<T>?>> = ConcurrentHashMap()
    inline fun computeIfAbsent(types: List<KType>, producer: () -> KSerializer<T>?): Result<KSerializer<T>?> {
        val wrappedTypes = types.map { KTypeWrapper(it) }
        return serializers.getOrPut(wrappedTypes) {
            kotlin.runCatching { producer() }
        }
    }
}