Skip to content

Commit

Permalink
feat: kotlinx serialization for GraphQLServerRequest (#1937)
Browse files Browse the repository at this point in the history
Switch from `jackson` to `kotlinx.serialization` for
serialization/deserialization of `GraphQLServerRequest` types.

After running benchmarks we where able to identify that deserializing
`GraphQLServerRequest` with `kotlinx.serialization` is quite faster than
doing it with `jackson`, the reason ? possibly because jackson relies on
reflections to identify deserialization process.

On the other hand, serialization/deserialization of
`GraphQLServerReponse` type is still faster if done with `jackson`,
possibly because of how `kotlinx.serialization` library was designed and
the poor support for serializing `Any` type:
Kotlin/kotlinx.serialization#296, which causes
a lot of memory comsumption.

As part of this PR also including the benchmarks. For that, i created a
separate set of types that are marked with both `jackson` and
`kotlinx.serialization` annotations.

Benchmarks results:
Executed on a MacBookPro 2.6 GHz 6-Core Intel Core i7.

`GraphQLBatchRequest`
4 batched operations, each operation is aprox: 30kb
<img width="1260" alt="image"
src="https://github.com/ExpediaGroup/graphql-kotlin/assets/6611331/06e5b218-a35e-4baa-a25e-2be1b3c27a95">

`GraphQLRequest`
<img width="1231" alt="image"
src="https://github.com/ExpediaGroup/graphql-kotlin/assets/6611331/e5ecba01-fd41-4872-b3e8-5519414cc918">

`GraphQLBatchResponse`
<img width="1240" alt="image"
src="https://github.com/ExpediaGroup/graphql-kotlin/assets/6611331/ee84bfa4-d7d1-46b4-b4a8-b3c220998a03">

`GraphQLResponse`
<img width="1197" alt="image"
src="https://github.com/ExpediaGroup/graphql-kotlin/assets/6611331/c217e05f-45fc-460e-a059-7667975ee49f">
  • Loading branch information
samuelAndalon committed Apr 1, 2024
1 parent 2fb8447 commit e65437a
Show file tree
Hide file tree
Showing 14 changed files with 2,354 additions and 23 deletions.
9 changes: 4 additions & 5 deletions servers/graphql-kotlin-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,17 @@ val kotlinxBenchmarkVersion: String by project

plugins {
id("org.jetbrains.kotlinx.benchmark")
kotlin("plugin.serialization")
}

val jacksonVersion: String by project
val kotlinxSerializationVersion: String by project
dependencies {
api(project(path = ":graphql-kotlin-schema-generator"))
api(project(path = ":graphql-kotlin-dataloader-instrumentation"))
api(project(path = ":graphql-kotlin-automatic-persisted-queries"))
api("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
api("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion")
}

Expand Down Expand Up @@ -43,14 +46,10 @@ tasks {
jacocoTestCoverageVerification {
violationRules {
rule {
excludes = listOf("com.expediagroup.graphql.server.testtypes.*")
limit {
counter = "INSTRUCTION"
value = "COVEREDRATIO"
minimum = "0.95".toBigDecimal()
}
limit {
counter = "BRANCH"
value = "COVEREDRATIO"
minimum = "0.84".toBigDecimal()
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.server

import com.expediagroup.graphql.server.testtypes.GraphQLServerRequest
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.serialization.json.Json
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Measurement
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.Setup
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.Warmup
import java.util.concurrent.TimeUnit

@State(Scope.Benchmark)
@Fork(5)
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
open class GraphQLServerRequestDeserializationBenchmark {
private val mapper = jacksonObjectMapper()
private lateinit var request: String
private lateinit var batchRequest: String

@Setup
fun setUp() {
val loader = this::class.java.classLoader
val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n")
val variables = loader.getResource("StarWarsDetailsVariables.json")!!.readText()
request = """
{
"operationName": "StarWarsDetails",
"query": "$operation",
"variables": $variables
}
""".trimIndent()
batchRequest = """
[
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }
]
""".trimIndent()
}

@Benchmark
fun JacksonDeserializeGraphQLRequest(): GraphQLServerRequest = mapper.readValue(request)

@Benchmark
fun JacksonDeserializeGraphQLBatchRequest(): GraphQLServerRequest = mapper.readValue(batchRequest)

@Benchmark
fun KSerializationDeserializeGraphQLRequest(): GraphQLServerRequest = Json.decodeFromString(request)

@Benchmark
fun KSerializationDeserializeGraphQLBatchRequest(): GraphQLServerRequest = Json.decodeFromString(batchRequest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.server

import com.expediagroup.graphql.server.testtypes.GraphQLBatchRequest
import com.expediagroup.graphql.server.testtypes.GraphQLRequest
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Measurement
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.Setup
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.Warmup
import java.util.concurrent.TimeUnit

@State(Scope.Benchmark)
@Fork(5)
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
open class GraphQLServerRequestSerializationBenchmark {
private val mapper = jacksonObjectMapper()
private lateinit var request: GraphQLRequest
private lateinit var batchRequest: GraphQLBatchRequest

@Setup
fun setUp() {
val loader = this::class.java.classLoader
val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n")
val variables = mapper.readValue<Map<String, Any?>>(
loader.getResourceAsStream("StarWarsDetailsVariables.json")!!
)
request = GraphQLRequest(operation, "StarWarsDetails", variables)
batchRequest = GraphQLBatchRequest(
GraphQLRequest(operation, "StarWarsDetails", variables),
GraphQLRequest(operation, "StarWarsDetails", variables),
GraphQLRequest(operation, "StarWarsDetails", variables),
GraphQLRequest(operation, "StarWarsDetails", variables)
)
}

@Benchmark
fun JacksonSerializeGraphQLRequest(): String = mapper.writeValueAsString(request)

@Benchmark
fun JacksonSerializeGraphQLBatchRequest(): String = mapper.writeValueAsString(batchRequest)

@Benchmark
fun KSerializationSerializeGraphQLRequest(): String = Json.encodeToString(request)

@Benchmark
fun KSerializationSerializeGraphQLBatchRequest(): String = Json.encodeToString(batchRequest)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.server

import com.expediagroup.graphql.server.testtypes.GraphQLServerResponse
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.serialization.json.Json
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Measurement
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.Setup
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.Warmup
import java.util.concurrent.TimeUnit

@State(Scope.Benchmark)
@Fork(5)
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
open class GraphQLServerResponseDeserializationBenchmark {
private val mapper = jacksonObjectMapper()
private lateinit var response: String
private lateinit var batchResponse: String

@Setup
fun setUp() {
response = this::class.java.classLoader.getResource("StarWarsDetailsResponse.json")!!.readText()
batchResponse = """
[
$response,
$response,
$response,
$response
]
""".trimIndent()
}

@Benchmark
fun JacksonDeserializeGraphQLResponse(): GraphQLServerResponse = mapper.readValue(response)

@Benchmark
fun JacksonDeserializeGraphQLBatchResponse(): GraphQLServerResponse = mapper.readValue(batchResponse)

@Benchmark
fun KSerializationDeserializeGraphQLResponse(): GraphQLServerResponse = Json.decodeFromString(response)

@Benchmark
fun KSerializationDeserializeGraphQLBatchResponse(): GraphQLServerResponse = Json.decodeFromString(batchResponse)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.expediagroup.graphql.server

import com.expediagroup.graphql.server.testtypes.GraphQLBatchResponse
import com.expediagroup.graphql.server.testtypes.GraphQLResponse
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.Fork
import org.openjdk.jmh.annotations.Measurement
import org.openjdk.jmh.annotations.Scope
import org.openjdk.jmh.annotations.Setup
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.Warmup
import java.util.concurrent.TimeUnit

@State(Scope.Benchmark)
@Fork(5)
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
open class GraphQLServerResponseSerializationBenchmark {
private val mapper = jacksonObjectMapper()
private lateinit var response: GraphQLResponse
private lateinit var batchResponse: GraphQLBatchResponse

@Setup
fun setUp() {
val data = mapper.readValue<Map<String, Any?>>(
this::class.java.classLoader.getResourceAsStream("StarWarsDetailsResponse.json")!!
)
response = GraphQLResponse(
mapper.readValue<Map<String, Any?>>(
this::class.java.classLoader.getResourceAsStream("StarWarsDetailsResponse.json")!!
)
)
batchResponse = GraphQLBatchResponse(
GraphQLResponse(data),
GraphQLResponse(data),
GraphQLResponse(data),
GraphQLResponse(data)
)
}

@Benchmark
fun JacksonSerializeGraphQLResponse(): String = mapper.writeValueAsString(response)

@Benchmark
fun JacksonSerializeGraphQLBatchResponse(): String = mapper.writeValueAsString(batchResponse)

@Benchmark
fun KSerializationSerializeGraphQLResponse(): String = Json.encodeToString(response)

@Benchmark
fun KSerializationSerializeGraphQLBatchResponse(): String = Json.encodeToString(batchResponse)
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2022 Expedia, Inc
* Copyright 2024 Expedia, Inc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,7 +18,6 @@ package com.expediagroup.graphql.server

import com.expediagroup.graphql.server.extensions.isMutation
import com.expediagroup.graphql.server.types.GraphQLRequest
import org.openjdk.jmh.annotations.Benchmark
import org.openjdk.jmh.annotations.Setup
import org.openjdk.jmh.annotations.State
import org.openjdk.jmh.annotations.Scope
Expand All @@ -32,7 +31,7 @@ import kotlin.random.Random
@Fork(1)
@Warmup(iterations = 2)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
open class GraphQLRequestBenchmark {
open class IsMutationBenchmark {
private val requests = mutableListOf<GraphQLRequest>()

@Setup
Expand Down Expand Up @@ -68,7 +67,7 @@ open class GraphQLRequestBenchmark {
requests.add(GraphQLRequest(mutation))
}

@Benchmark
// @Benchmark
fun isMutationBenchmark(): Boolean {
return requests.any(GraphQLRequest::isMutation)
}
Expand Down
Loading

0 comments on commit e65437a

Please sign in to comment.