Skip to content

Commit 65aa3f0

Browse files
feat: kotlinx serialization for GraphQLServerRequest -- cherry pick (#1937) (#1944)
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"> ### 📝 Description ### 🔗 Related Issues
1 parent 2fb8447 commit 65aa3f0

14 files changed

+2357
-18
lines changed

servers/graphql-kotlin-server/build.gradle.kts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ val kotlinxBenchmarkVersion: String by project
77

88
plugins {
99
id("org.jetbrains.kotlinx.benchmark")
10+
kotlin("plugin.serialization")
1011
}
1112

1213
val jacksonVersion: String by project
14+
val kotlinxSerializationVersion: String by project
1315
dependencies {
1416
api(project(path = ":graphql-kotlin-schema-generator"))
1517
api(project(path = ":graphql-kotlin-dataloader-instrumentation"))
1618
api(project(path = ":graphql-kotlin-automatic-persisted-queries"))
1719
api("com.fasterxml.jackson.module:jackson-module-kotlin:$jacksonVersion")
20+
api("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
1821
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlinCoroutinesVersion")
1922
}
2023

@@ -43,15 +46,16 @@ tasks {
4346
jacocoTestCoverageVerification {
4447
violationRules {
4548
rule {
49+
excludes = listOf("com.expediagroup.graphql.server.testtypes.*")
4650
limit {
4751
counter = "INSTRUCTION"
4852
value = "COVEREDRATIO"
49-
minimum = "0.95".toBigDecimal()
53+
minimum = "0.81".toBigDecimal()
5054
}
5155
limit {
5256
counter = "BRANCH"
5357
value = "COVEREDRATIO"
54-
minimum = "0.84".toBigDecimal()
58+
minimum = "0.67".toBigDecimal()
5559
}
5660
}
5761
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2024 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.server
18+
19+
import com.expediagroup.graphql.server.testtypes.GraphQLServerRequest
20+
import com.expediagroup.graphql.server.testtypes.GraphQLServerRequestKSerializer
21+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
22+
import com.fasterxml.jackson.module.kotlin.readValue
23+
import kotlinx.serialization.json.Json
24+
import org.openjdk.jmh.annotations.Benchmark
25+
import org.openjdk.jmh.annotations.Fork
26+
import org.openjdk.jmh.annotations.Measurement
27+
import org.openjdk.jmh.annotations.Scope
28+
import org.openjdk.jmh.annotations.Setup
29+
import org.openjdk.jmh.annotations.State
30+
import org.openjdk.jmh.annotations.Warmup
31+
import java.util.concurrent.TimeUnit
32+
33+
@State(Scope.Benchmark)
34+
@Fork(5)
35+
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
36+
@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
37+
open class GraphQLServerRequestDeserializationBenchmark {
38+
private val mapper = jacksonObjectMapper()
39+
private lateinit var request: String
40+
private lateinit var batchRequest: String
41+
42+
@Setup
43+
fun setUp() {
44+
val loader = this::class.java.classLoader
45+
val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n")
46+
val variables = loader.getResource("StarWarsDetailsVariables.json")!!.readText()
47+
request = """
48+
{
49+
"operationName": "StarWarsDetails",
50+
"query": "$operation",
51+
"variables": $variables
52+
}
53+
""".trimIndent()
54+
batchRequest = """
55+
[
56+
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
57+
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
58+
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
59+
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }
60+
]
61+
""".trimIndent()
62+
}
63+
64+
@Benchmark
65+
fun JacksonDeserializeGraphQLRequest(): GraphQLServerRequest = mapper.readValue(request)
66+
67+
@Benchmark
68+
fun JacksonDeserializeGraphQLBatchRequest(): GraphQLServerRequest = mapper.readValue(batchRequest)
69+
70+
@Benchmark
71+
fun KSerializationDeserializeGraphQLRequest(): GraphQLServerRequest = Json.decodeFromString(GraphQLServerRequestKSerializer, request)
72+
73+
@Benchmark
74+
fun KSerializationDeserializeGraphQLBatchRequest(): GraphQLServerRequest = Json.decodeFromString(GraphQLServerRequestKSerializer, batchRequest)
75+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Copyright 2024 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.server
18+
19+
import com.expediagroup.graphql.server.testtypes.GraphQLBatchRequest
20+
import com.expediagroup.graphql.server.testtypes.GraphQLRequest
21+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
22+
import com.fasterxml.jackson.module.kotlin.readValue
23+
import kotlinx.serialization.encodeToString
24+
import kotlinx.serialization.json.Json
25+
import org.openjdk.jmh.annotations.Benchmark
26+
import org.openjdk.jmh.annotations.Fork
27+
import org.openjdk.jmh.annotations.Measurement
28+
import org.openjdk.jmh.annotations.Scope
29+
import org.openjdk.jmh.annotations.Setup
30+
import org.openjdk.jmh.annotations.State
31+
import org.openjdk.jmh.annotations.Warmup
32+
import java.util.concurrent.TimeUnit
33+
34+
@State(Scope.Benchmark)
35+
@Fork(5)
36+
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
37+
@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
38+
open class GraphQLServerRequestSerializationBenchmark {
39+
private val mapper = jacksonObjectMapper()
40+
private lateinit var request: GraphQLRequest
41+
private lateinit var batchRequest: GraphQLBatchRequest
42+
43+
@Setup
44+
fun setUp() {
45+
val loader = this::class.java.classLoader
46+
val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n")
47+
val variables = mapper.readValue<Map<String, Any?>>(
48+
loader.getResourceAsStream("StarWarsDetailsVariables.json")!!
49+
)
50+
request = GraphQLRequest(operation, "StarWarsDetails", variables)
51+
batchRequest = GraphQLBatchRequest(
52+
GraphQLRequest(operation, "StarWarsDetails", variables),
53+
GraphQLRequest(operation, "StarWarsDetails", variables),
54+
GraphQLRequest(operation, "StarWarsDetails", variables),
55+
GraphQLRequest(operation, "StarWarsDetails", variables)
56+
)
57+
}
58+
59+
@Benchmark
60+
fun JacksonSerializeGraphQLRequest(): String = mapper.writeValueAsString(request)
61+
62+
@Benchmark
63+
fun JacksonSerializeGraphQLBatchRequest(): String = mapper.writeValueAsString(batchRequest)
64+
65+
@Benchmark
66+
fun KSerializationSerializeGraphQLRequest(): String = Json.encodeToString(request)
67+
68+
@Benchmark
69+
fun KSerializationSerializeGraphQLBatchRequest(): String = Json.encodeToString(batchRequest)
70+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2024 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.server
18+
19+
import com.expediagroup.graphql.server.testtypes.GraphQLServerResponse
20+
import com.expediagroup.graphql.server.testtypes.GraphQLServerResponseKSerializer
21+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
22+
import com.fasterxml.jackson.module.kotlin.readValue
23+
import kotlinx.serialization.json.Json
24+
import org.openjdk.jmh.annotations.Benchmark
25+
import org.openjdk.jmh.annotations.Fork
26+
import org.openjdk.jmh.annotations.Measurement
27+
import org.openjdk.jmh.annotations.Scope
28+
import org.openjdk.jmh.annotations.Setup
29+
import org.openjdk.jmh.annotations.State
30+
import org.openjdk.jmh.annotations.Warmup
31+
import java.util.concurrent.TimeUnit
32+
33+
@State(Scope.Benchmark)
34+
@Fork(5)
35+
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
36+
@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
37+
open class GraphQLServerResponseDeserializationBenchmark {
38+
private val mapper = jacksonObjectMapper()
39+
private lateinit var response: String
40+
private lateinit var batchResponse: String
41+
42+
@Setup
43+
fun setUp() {
44+
response = this::class.java.classLoader.getResource("StarWarsDetailsResponse.json")!!.readText()
45+
batchResponse = """
46+
[
47+
$response,
48+
$response,
49+
$response,
50+
$response
51+
]
52+
""".trimIndent()
53+
}
54+
55+
@Benchmark
56+
fun JacksonDeserializeGraphQLResponse(): GraphQLServerResponse = mapper.readValue(response)
57+
58+
@Benchmark
59+
fun JacksonDeserializeGraphQLBatchResponse(): GraphQLServerResponse = mapper.readValue(batchResponse)
60+
61+
@Benchmark
62+
fun KSerializationDeserializeGraphQLResponse(): GraphQLServerResponse = Json.decodeFromString(GraphQLServerResponseKSerializer, response)
63+
64+
@Benchmark
65+
fun KSerializationDeserializeGraphQLBatchResponse(): GraphQLServerResponse = Json.decodeFromString(GraphQLServerResponseKSerializer, batchResponse)
66+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2024 Expedia, Inc
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.expediagroup.graphql.server
18+
19+
import com.expediagroup.graphql.server.testtypes.GraphQLBatchResponse
20+
import com.expediagroup.graphql.server.testtypes.GraphQLResponse
21+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
22+
import com.fasterxml.jackson.module.kotlin.readValue
23+
import kotlinx.serialization.encodeToString
24+
import kotlinx.serialization.json.Json
25+
import org.openjdk.jmh.annotations.Benchmark
26+
import org.openjdk.jmh.annotations.Fork
27+
import org.openjdk.jmh.annotations.Measurement
28+
import org.openjdk.jmh.annotations.Scope
29+
import org.openjdk.jmh.annotations.Setup
30+
import org.openjdk.jmh.annotations.State
31+
import org.openjdk.jmh.annotations.Warmup
32+
import java.util.concurrent.TimeUnit
33+
34+
@State(Scope.Benchmark)
35+
@Fork(5)
36+
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
37+
@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
38+
open class GraphQLServerResponseSerializationBenchmark {
39+
private val mapper = jacksonObjectMapper()
40+
private lateinit var response: GraphQLResponse
41+
private lateinit var batchResponse: GraphQLBatchResponse
42+
43+
@Setup
44+
fun setUp() {
45+
val data = mapper.readValue<Map<String, Any?>>(
46+
this::class.java.classLoader.getResourceAsStream("StarWarsDetailsResponse.json")!!
47+
)
48+
response = GraphQLResponse(
49+
mapper.readValue<Map<String, Any?>>(
50+
this::class.java.classLoader.getResourceAsStream("StarWarsDetailsResponse.json")!!
51+
)
52+
)
53+
batchResponse = GraphQLBatchResponse(
54+
GraphQLResponse(data),
55+
GraphQLResponse(data),
56+
GraphQLResponse(data),
57+
GraphQLResponse(data)
58+
)
59+
}
60+
61+
@Benchmark
62+
fun JacksonSerializeGraphQLResponse(): String = mapper.writeValueAsString(response)
63+
64+
@Benchmark
65+
fun JacksonSerializeGraphQLBatchResponse(): String = mapper.writeValueAsString(batchResponse)
66+
67+
@Benchmark
68+
fun KSerializationSerializeGraphQLResponse(): String = Json.encodeToString(response)
69+
70+
@Benchmark
71+
fun KSerializationSerializeGraphQLBatchResponse(): String = Json.encodeToString(batchResponse)
72+
}

servers/graphql-kotlin-server/src/benchmarks/kotlin/GraphQLRequestBenchmark.kt renamed to servers/graphql-kotlin-server/src/benchmarks/kotlin/IsMutationBenchmark.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022 Expedia, Inc
2+
* Copyright 2024 Expedia, Inc
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -32,7 +32,7 @@ import kotlin.random.Random
3232
@Fork(1)
3333
@Warmup(iterations = 2)
3434
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
35-
open class GraphQLRequestBenchmark {
35+
open class IsMutationBenchmark {
3636
private val requests = mutableListOf<GraphQLRequest>()
3737

3838
@Setup

0 commit comments

Comments
 (0)