diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt index 81bfc56317..4af1045326 100644 --- a/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt +++ b/formats/json/jvmMain/src/kotlinx/serialization/json/JvmStreams.kt @@ -56,7 +56,12 @@ public fun Json.decodeFromStream( deserializer: DeserializationStrategy, stream: InputStream ): T { - return decodeByReader(deserializer, JavaStreamSerialReader(stream)) + val reader = JavaStreamSerialReader(stream) + try { + return decodeByReader(deserializer, reader) + } finally { + reader.release() + } } /** diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharArrayPool.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/ArrayPools.kt similarity index 51% rename from formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharArrayPool.kt rename to formats/json/jvmMain/src/kotlinx/serialization/json/internal/ArrayPools.kt index 08d22ec83d..da0692d1e2 100644 --- a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharArrayPool.kt +++ b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/ArrayPools.kt @@ -3,20 +3,18 @@ */ package kotlinx.serialization.json.internal -import java.util.concurrent.* +/* + * Not really documented kill switch as a workaround for potential + * (unlikely) problems with memory consumptions. + */ +private val MAX_CHARS_IN_POOL = runCatching { + System.getProperty("kotlinx.serialization.json.pool.size").toIntOrNull() +}.getOrNull() ?: 1024 * 1024 internal open class CharArrayPoolBase { private val arrays = ArrayDeque() private var charsTotal = 0 - /* - * Not really documented kill switch as a workaround for potential - * (unlikely) problems with memory consumptions. - */ - private val MAX_CHARS_IN_POOL = runCatching { - System.getProperty("kotlinx.serialization.json.pool.size").toIntOrNull() - }.getOrNull() ?: 1024 * 1024 // 2 MB seems to be a reasonable constraint, (1M of chars) - protected fun take(size: Int): CharArray { /* * Initially the pool is empty, so an instance will be allocated @@ -52,3 +50,40 @@ internal actual object CharArrayPoolBatchSize : CharArrayPoolBase() { releaseImpl(array) } } + +// Byte array pool + +internal open class ByteArrayPoolBase { + private val arrays = ArrayDeque() + private var bytesTotal = 0 + + protected fun take(size: Int): ByteArray { + /* + * Initially the pool is empty, so an instance will be allocated + * and the pool will be populated in the 'release' + */ + val candidate = synchronized(this) { + arrays.removeLastOrNull()?.also { bytesTotal -= it.size / 2 } + } + return candidate ?: ByteArray(size) + } + + protected fun releaseImpl(array: ByteArray): Unit = synchronized(this) { + if (bytesTotal + array.size >= MAX_CHARS_IN_POOL) return@synchronized + bytesTotal += array.size / 2 + arrays.addLast(array) + } +} + +internal object ByteArrayPool8k : ByteArrayPoolBase() { + fun take(): ByteArray = super.take(8196) + + fun release(array: ByteArray) = releaseImpl(array) +} + + +internal object ByteArrayPool : ByteArrayPoolBase() { + fun take(): ByteArray = super.take(128) + + fun release(array: ByteArray) = releaseImpl(array) +} diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/ByteArrayPool.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/ByteArrayPool.kt deleted file mode 100644 index a1ab1bc0b8..0000000000 --- a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/ByteArrayPool.kt +++ /dev/null @@ -1,30 +0,0 @@ -package kotlinx.serialization.json.internal - -internal object ByteArrayPool { - private val arrays = ArrayDeque() - private var charsTotal = 0 - /* - * Not really documented kill switch as a workaround for potential - * (unlikely) problems with memory consumptions. - */ - private val MAX_CHARS_IN_POOL = runCatching { - System.getProperty("kotlinx.serialization.json.pool.size").toIntOrNull() - }.getOrNull() ?: 2 * 1024 * 1024 // 2 MB seems to be a reasonable constraint, (1M of chars) - - fun take(): ByteArray { - /* - * Initially the pool is empty, so an instance will be allocated - * and the pool will be populated in the 'release' - */ - val candidate = synchronized(this) { - arrays.removeLastOrNull()?.also { charsTotal -= it.size } - } - return candidate ?: ByteArray(512) - } - - fun release(array: ByteArray) = synchronized(this) { - if (charsTotal + array.size >= MAX_CHARS_IN_POOL) return@synchronized - charsTotal += array.size - arrays.addLast(array) - } -} diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharsetReader.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharsetReader.kt index d2ba1afc27..f5bf0cf6ba 100644 --- a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharsetReader.kt +++ b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/CharsetReader.kt @@ -19,7 +19,7 @@ internal class CharsetReader( decoder = charset.newDecoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE) - byteBuffer = ByteBuffer.allocate(32) + byteBuffer = ByteBuffer.wrap(ByteArrayPool8k.take()) byteBuffer.flip() // Make empty } @@ -117,4 +117,8 @@ internal class CharsetReader( else -> error("Unreachable state: $bytesRead") } } + + public fun release() { + ByteArrayPool8k.release(byteBuffer.array()) + } } diff --git a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JvmJsonStreams.kt b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JvmJsonStreams.kt index 79e668b160..274dd4dc2d 100644 --- a/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JvmJsonStreams.kt +++ b/formats/json/jvmMain/src/kotlinx/serialization/json/internal/JvmJsonStreams.kt @@ -254,10 +254,14 @@ internal class JsonToJavaStreamWriter(private val stream: OutputStream) : JsonWr } internal class JavaStreamSerialReader(stream: InputStream) : SerialReader { - // NB: not closed on purpose, it is responsibility of the caller + // NB: not closed on purpose, it is the responsibility of the caller private val reader = CharsetReader(stream, Charsets.UTF_8) override fun read(buffer: CharArray, bufferOffset: Int, count: Int): Int { return reader.read(buffer, bufferOffset, count) } + + fun release() { + reader.release() + } }