Skip to content

Commit 06435c9

Browse files
feat: kotlinx serialization for GraphQLServerRequest (#1937)
### 📝 Description 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. #### GraphQLServerRequest Deserialization `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"> #### GraphQLServerResponse Serialization `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">
1 parent d2470ec commit 06435c9

File tree

16 files changed

+2372
-22
lines changed

16 files changed

+2372
-22
lines changed

integration/graalvm/spring-graalvm-server/src/test/kotlin/com/expediagroup/graalvm/spring/schema/TypesQueryTest.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package com.expediagroup.graalvm.spring.schema
1717

18-
import com.expediagroup.graalvm.schema.model.InputOnly
1918
import com.expediagroup.graphql.server.types.GraphQLRequest
2019
import org.junit.jupiter.api.Test
2120
import org.junit.jupiter.api.TestInstance
@@ -35,7 +34,7 @@ class TypesQueryTest(@Autowired private val testClient: WebTestClient) {
3534
val request = GraphQLRequest(
3635
query = "query InputOnlyQuery(\$inputArg: InputOnlyInput){ inputTypeQuery(arg: \$inputArg) }",
3736
operationName = "InputOnlyQuery",
38-
variables = mapOf("inputArg" to InputOnly(id = 123))
37+
variables = mapOf("inputArg" to mapOf("id" to 123))
3938
)
4039

4140
testClient.post()

plugins/server/graphql-kotlin-graalvm-metadata-generator/src/main/resources/default-reflect-config.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,22 @@
120120
"allDeclaredFields": true,
121121
"queryAllDeclaredMethods": true
122122
},
123+
{
124+
"name":"com.expediagroup.graphql.server.types.GraphQLRequest",
125+
"fields":[{"name":"Companion"}]
126+
},
127+
{
128+
"name":"com.expediagroup.graphql.server.types.GraphQLRequest$Companion",
129+
"methods":[{"name":"serializer","parameterTypes":[] }]
130+
},
131+
{
132+
"name":"com.expediagroup.graphql.server.types.GraphQLBatchRequest",
133+
"fields":[{"name":"Companion"}]
134+
},
135+
{
136+
"name":"com.expediagroup.graphql.server.types.GraphQLBatchRequest$Companion",
137+
"methods":[{"name":"serializer","parameterTypes":[] }]
138+
},
123139
{
124140
"name": "com.expediagroup.graphql.server.types.GraphQLServerRequestDeserializer",
125141
"methods": [

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ description = "Common code for running a GraphQL server in any HTTP server frame
55
plugins {
66
id("com.expediagroup.graphql.conventions")
77
alias(libs.plugins.benchmark)
8+
alias(libs.plugins.kotlin.serialization)
89
}
910

1011
dependencies {
1112
api(projects.graphqlKotlinSchemaGenerator)
1213
api(projects.graphqlKotlinDataloaderInstrumentation)
1314
api(projects.graphqlKotlinAutomaticPersistedQueries)
1415
api(libs.jackson)
16+
api(libs.kotlinx.serialization.json)
1517
testImplementation(libs.kotlinx.coroutines.test)
1618
testImplementation(libs.logback)
1719
}
@@ -41,15 +43,16 @@ tasks {
4143
jacocoTestCoverageVerification {
4244
violationRules {
4345
rule {
46+
excludes = listOf("com.expediagroup.graphql.server.testtypes.*")
4447
limit {
4548
counter = "INSTRUCTION"
4649
value = "COVEREDRATIO"
47-
minimum = "0.88".toBigDecimal()
50+
minimum = "0.84".toBigDecimal()
4851
}
4952
limit {
5053
counter = "BRANCH"
5154
value = "COVEREDRATIO"
52-
minimum = "0.84".toBigDecimal()
55+
minimum = "0.72".toBigDecimal()
5356
}
5457
}
5558
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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.fasterxml.jackson.module.kotlin.jacksonObjectMapper
21+
import com.fasterxml.jackson.module.kotlin.readValue
22+
import kotlinx.serialization.json.Json
23+
import org.openjdk.jmh.annotations.Benchmark
24+
import org.openjdk.jmh.annotations.Fork
25+
import org.openjdk.jmh.annotations.Measurement
26+
import org.openjdk.jmh.annotations.Scope
27+
import org.openjdk.jmh.annotations.Setup
28+
import org.openjdk.jmh.annotations.State
29+
import org.openjdk.jmh.annotations.Warmup
30+
import java.util.concurrent.TimeUnit
31+
32+
@State(Scope.Benchmark)
33+
@Fork(5)
34+
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
35+
@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
36+
open class GraphQLServerRequestDeserializationBenchmark {
37+
private val mapper = jacksonObjectMapper()
38+
private lateinit var request: String
39+
private lateinit var batchRequest: String
40+
41+
@Setup
42+
fun setUp() {
43+
val loader = this::class.java.classLoader
44+
val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n")
45+
val variables = loader.getResource("StarWarsDetailsVariables.json")!!.readText()
46+
request = """
47+
{
48+
"operationName": "StarWarsDetails",
49+
"query": "$operation",
50+
"variables": $variables
51+
}
52+
""".trimIndent()
53+
batchRequest = """
54+
[
55+
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
56+
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
57+
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables },
58+
{ "operationName": "StarWarsDetails", "query": "$operation", "variables": $variables }
59+
]
60+
""".trimIndent()
61+
}
62+
63+
@Benchmark
64+
fun JacksonDeserializeGraphQLRequest(): GraphQLServerRequest = mapper.readValue(request)
65+
66+
@Benchmark
67+
fun JacksonDeserializeGraphQLBatchRequest(): GraphQLServerRequest = mapper.readValue(batchRequest)
68+
69+
@Benchmark
70+
fun KSerializationDeserializeGraphQLRequest(): GraphQLServerRequest = Json.decodeFromString(request)
71+
72+
@Benchmark
73+
fun KSerializationDeserializeGraphQLBatchRequest(): GraphQLServerRequest = Json.decodeFromString(batchRequest)
74+
}
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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
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.fasterxml.jackson.module.kotlin.jacksonObjectMapper
21+
import com.fasterxml.jackson.module.kotlin.readValue
22+
import kotlinx.serialization.json.Json
23+
import org.openjdk.jmh.annotations.Benchmark
24+
import org.openjdk.jmh.annotations.Fork
25+
import org.openjdk.jmh.annotations.Measurement
26+
import org.openjdk.jmh.annotations.Scope
27+
import org.openjdk.jmh.annotations.Setup
28+
import org.openjdk.jmh.annotations.State
29+
import org.openjdk.jmh.annotations.Warmup
30+
import java.util.concurrent.TimeUnit
31+
32+
@State(Scope.Benchmark)
33+
@Fork(5)
34+
@Warmup(iterations = 1, time = 5, timeUnit = TimeUnit.SECONDS)
35+
@Measurement(iterations = 5, time = 10, timeUnit = TimeUnit.SECONDS)
36+
open class GraphQLServerResponseDeserializationBenchmark {
37+
private val mapper = jacksonObjectMapper()
38+
private lateinit var response: String
39+
private lateinit var batchResponse: String
40+
41+
@Setup
42+
fun setUp() {
43+
response = this::class.java.classLoader.getResource("StarWarsDetailsResponse.json")!!.readText()
44+
batchResponse = """
45+
[
46+
$response,
47+
$response,
48+
$response,
49+
$response
50+
]
51+
""".trimIndent()
52+
}
53+
54+
@Benchmark
55+
fun JacksonDeserializeGraphQLResponse(): GraphQLServerResponse = mapper.readValue(response)
56+
57+
@Benchmark
58+
fun JacksonDeserializeGraphQLBatchResponse(): GraphQLServerResponse = mapper.readValue(batchResponse)
59+
60+
@Benchmark
61+
fun KSerializationDeserializeGraphQLResponse(): GraphQLServerResponse = Json.decodeFromString(response)
62+
63+
@Benchmark
64+
fun KSerializationDeserializeGraphQLBatchResponse(): GraphQLServerResponse = Json.decodeFromString(batchResponse)
65+
}
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: 3 additions & 4 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.
@@ -18,7 +18,6 @@ package com.expediagroup.graphql.server
1818

1919
import com.expediagroup.graphql.server.extensions.isMutation
2020
import com.expediagroup.graphql.server.types.GraphQLRequest
21-
import org.openjdk.jmh.annotations.Benchmark
2221
import org.openjdk.jmh.annotations.Setup
2322
import org.openjdk.jmh.annotations.State
2423
import org.openjdk.jmh.annotations.Scope
@@ -32,7 +31,7 @@ import kotlin.random.Random
3231
@Fork(1)
3332
@Warmup(iterations = 2)
3433
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
35-
open class GraphQLRequestBenchmark {
34+
open class IsMutationBenchmark {
3635
private val requests = mutableListOf<GraphQLRequest>()
3736

3837
@Setup
@@ -68,7 +67,7 @@ open class GraphQLRequestBenchmark {
6867
requests.add(GraphQLRequest(mutation))
6968
}
7069

71-
@Benchmark
70+
// @Benchmark
7271
fun isMutationBenchmark(): Boolean {
7372
return requests.any(GraphQLRequest::isMutation)
7473
}

0 commit comments

Comments
 (0)