Skip to content

Commit

Permalink
JVM integration with InputStream and OutputStream (#1569)
Browse files Browse the repository at this point in the history
Co-authored-by: Vsevolod Tolstopyatov <[email protected]>
  • Loading branch information
sandwwraith and qwwdfsad authored Sep 3, 2021
1 parent 0e75d25 commit c0c60a6
Show file tree
Hide file tree
Showing 49 changed files with 1,221 additions and 500 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package kotlinx.benchmarks.json

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import kotlinx.benchmarks.model.MacroTwitterFeed
import kotlinx.benchmarks.model.MicroTwitterFeed
import kotlinx.serialization.json.*
import org.openjdk.jmh.annotations.*
import java.io.*
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.TimeUnit
import kotlin.io.path.deleteIfExists
import kotlin.io.path.outputStream

@Warmup(iterations = 7, time = 1)
@Measurement(iterations = 7, time = 1)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@State(Scope.Benchmark)
@Fork(2)
open class TwitterFeedStreamBenchmark {
val resource = TwitterFeedBenchmark::class.java.getResource("/twitter_macro.json")!!
val bytes = resource.readBytes()
private val twitter = Json.decodeFromString(MacroTwitterFeed.serializer(), resource.readText())

private val jsonIgnoreUnknwn = Json { ignoreUnknownKeys = true }
private val objectMapper: ObjectMapper =
jacksonObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)


private val inputStream: InputStream
get() = ByteArrayInputStream(bytes)
private val outputStream: OutputStream
get() = ByteArrayOutputStream()

@Benchmark
fun encodeTwitterWriteText(): OutputStream {
return outputStream.use {
it.bufferedWriter().write(Json.encodeToString(MacroTwitterFeed.serializer(), twitter))
it
}
}

@Benchmark
fun encodeTwitterWriteStream(): OutputStream {
return outputStream.use {
Json.encodeToStream(MacroTwitterFeed.serializer(), twitter, it)
it
}
}

@Benchmark
fun encodeTwitterJacksonStream(): OutputStream {
return outputStream.use {
objectMapper.writeValue(it, twitter)
it
}
}

@Benchmark
fun decodeMicroTwitterReadText(): MicroTwitterFeed {
return inputStream.use {
jsonIgnoreUnknwn.decodeFromString(MicroTwitterFeed.serializer(), it.bufferedReader().readText())
}
}

@Benchmark
fun decodeMicroTwitterStream(): MicroTwitterFeed {
return inputStream.use {
jsonIgnoreUnknwn.decodeFromStream(MicroTwitterFeed.serializer(), it.buffered())
}
}

@Benchmark
fun decodeMicroTwitterJacksonStream(): MicroTwitterFeed {
return inputStream.use {
objectMapper.readValue(it, MicroTwitterFeed::class.java)
}
}
}
5 changes: 5 additions & 0 deletions formats/json/api/kotlinx-serialization-json.api
Original file line number Diff line number Diff line change
Expand Up @@ -340,3 +340,8 @@ public abstract class kotlinx/serialization/json/JsonTransformingSerializer : ko
protected fun transformSerialize (Lkotlinx/serialization/json/JsonElement;)Lkotlinx/serialization/json/JsonElement;
}

public final class kotlinx/serialization/json/JvmStreamsKt {
public static final fun decodeFromStream (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/DeserializationStrategy;Ljava/io/InputStream;)Ljava/lang/Object;
public static final fun encodeToStream (Lkotlinx/serialization/json/Json;Lkotlinx/serialization/SerializationStrategy;Ljava/lang/Object;Ljava/io/OutputStream;)V
}

Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public sealed class Json(
* @throws [SerializationException] if the given JSON string cannot be deserialized to the value of type [T].
*/
public final override fun <T> decodeFromString(deserializer: DeserializationStrategy<T>, string: String): T {
val lexer = JsonLexer(string)
val lexer = StringJsonLexer(string)
val input = StreamingJsonDecoder(this, WriteMode.OBJ, lexer, deserializer.descriptor)
val result = input.decodeSerializableValue(deserializer)
lexer.expectEof()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,32 @@
* Copyright 2017-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
*/

@file:OptIn(ExperimentalSerializationApi::class)
package kotlinx.serialization.json.internal

import kotlinx.serialization.*
import kotlinx.serialization.json.*
import kotlin.jvm.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlin.jvm.JvmField

internal fun Composer(sb: JsonStringBuilder, json: Json): Composer =
if (json.configuration.prettyPrint) ComposerWithPrettyPrint(sb, json) else Composer(sb)

@OptIn(ExperimentalSerializationApi::class)
internal open class Composer(@JvmField internal val sb: JsonStringBuilder, @JvmField internal val json: Json) {
private var level = 0
internal open class Composer(@JvmField internal val sb: JsonStringBuilder) {
var writingFirst = true
private set
protected set

fun indent() {
open fun indent() {
writingFirst = true
level++
}

fun unIndent() {
level--
}
open fun unIndent() = Unit

fun nextItem() {
open fun nextItem() {
writingFirst = false
if (json.configuration.prettyPrint) {
print("\n")
repeat(level) { print(json.configuration.prettyPrintIndent) }
}
}

fun space() {
if (json.configuration.prettyPrint)
print(' ')
}
open fun space() = Unit

fun print(v: Char) = sb.append(v)
fun print(v: String) = sb.append(v)
Expand All @@ -49,7 +42,7 @@ internal open class Composer(@JvmField internal val sb: JsonStringBuilder, @JvmF
}

@ExperimentalUnsignedTypes
internal class ComposerForUnsignedNumbers(sb: JsonStringBuilder, json: Json) : Composer(sb, json) {
internal class ComposerForUnsignedNumbers(sb: JsonStringBuilder) : Composer(sb) {
override fun print(v: Int) {
return super.print(v.toUInt().toString())
}
Expand All @@ -66,3 +59,29 @@ internal class ComposerForUnsignedNumbers(sb: JsonStringBuilder, json: Json) : C
return super.print(v.toUShort().toString())
}
}

internal class ComposerWithPrettyPrint(
sb: JsonStringBuilder,
private val json: Json
) : Composer(sb) {
private var level = 0

override fun indent() {
writingFirst = true
level++
}

override fun unIndent() {
level--
}

override fun nextItem() {
writingFirst = false
print("\n")
repeat(level) { print(json.configuration.prettyPrintIndent) }
}

override fun space() {
print(' ')
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal fun JsonDecodingException(offset: Int, message: String) =
*/
internal class JsonEncodingException(message: String) : JsonException(message)

internal fun JsonDecodingException(offset: Int, message: String, input: String) =
internal fun JsonDecodingException(offset: Int, message: String, input: CharSequence) =
JsonDecodingException(offset, "$message\nJSON input: ${input.minify(offset)}")

internal fun InvalidFloatingPointEncoded(value: Number, output: String) = JsonEncodingException(
Expand All @@ -45,7 +45,7 @@ internal fun InvalidFloatingPointDecoded(value: Number, key: String, output: Str
JsonDecodingException(-1, unexpectedFpErrorMessage(value, key, output))

// Extension on JSON reader and fail immediately
internal fun JsonLexer.throwInvalidFloatingPointDecoded(result: Number): Nothing {
internal fun AbstractJsonLexer.throwInvalidFloatingPointDecoded(result: Number): Nothing {
fail("Unexpected special floating-point value $result. By default, " +
"non-finite floating point values are prohibited because they do not conform JSON specification. " +
specialFlowingValuesHint
Expand Down Expand Up @@ -73,7 +73,7 @@ internal fun InvalidKeyKindException(keyDescriptor: SerialDescriptor) = JsonEnco
allowStructuredMapKeysHint
)

private fun String.minify(offset: Int = -1): String {
private fun CharSequence.minify(offset: Int = -1): CharSequence {
if (length < 200) return this
if (offset == -1) {
val start = this.length - 60
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlinx.serialization.json.*
@OptIn(ExperimentalSerializationApi::class)
internal class JsonTreeReader(
configuration: JsonConfiguration,
private val lexer: JsonLexer
private val lexer: AbstractJsonLexer
) {
private val isLenient = configuration.isLenient
private var stackDepth = 0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ import kotlinx.serialization.modules.*
import kotlin.jvm.*

/**
* [JsonDecoder] which reads given JSON from [JsonLexer] field by field.
* [JsonDecoder] which reads given JSON from [AbstractJsonLexer] field by field.
*/
@OptIn(ExperimentalSerializationApi::class, ExperimentalUnsignedTypes::class)
internal open class StreamingJsonDecoder(
final override val json: Json,
private val mode: WriteMode,
@JvmField internal val lexer: JsonLexer,
@JvmField internal val lexer: AbstractJsonLexer,
descriptor: SerialDescriptor
) : JsonDecoder, AbstractDecoder() {

Expand Down Expand Up @@ -256,7 +256,7 @@ internal open class StreamingJsonDecoder(
@OptIn(ExperimentalSerializationApi::class)
@ExperimentalUnsignedTypes
internal class JsonDecoderForUnsignedTypes(
private val lexer: JsonLexer,
private val lexer: AbstractJsonLexer,
json: Json
) : AbstractDecoder() {
override val serializersModule: SerializersModule = json.serializersModule
Expand All @@ -268,7 +268,7 @@ internal class JsonDecoderForUnsignedTypes(
override fun decodeShort(): Short = lexer.parseString("UShort") { toUShort().toShort() }
}

private inline fun <T> JsonLexer.parseString(expectedType: String, block: String.() -> T): T {
private inline fun <T> AbstractJsonLexer.parseString(expectedType: String, block: String.() -> T): T {
val input = consumeStringLenient()
try {
return input.block()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,10 +160,7 @@ internal class StreamingJsonEncoder(

override fun encodeInline(inlineDescriptor: SerialDescriptor): Encoder =
if (inlineDescriptor.isUnsignedNumber) StreamingJsonEncoder(
ComposerForUnsignedNumbers(
composer.sb,
json
), json, mode, null
ComposerForUnsignedNumbers(composer.sb), json, mode, null
)
else super.encodeInline(inlineDescriptor)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ private sealed class AbstractJsonTreeDecoder(

@OptIn(ExperimentalUnsignedTypes::class)
override fun decodeTaggedInline(tag: String, inlineDescriptor: SerialDescriptor): Decoder =
if (inlineDescriptor.isUnsignedNumber) JsonDecoderForUnsignedTypes(JsonLexer(getPrimitiveValue(tag).content), json)
if (inlineDescriptor.isUnsignedNumber) JsonDecoderForUnsignedTypes(StringJsonLexer(getPrimitiveValue(tag).content), json)
else super.decodeTaggedInline(tag, inlineDescriptor)
}

Expand Down
Loading

0 comments on commit c0c60a6

Please sign in to comment.