Skip to content

Commit

Permalink
feat: fastjson2 for serialization of GraphQLResponse and deserializat…
Browse files Browse the repository at this point in the history
…ion of GraphQLRequest (#2040)

### 📝 Description
[the previous integration of
kotlinx-serialization](#1937)
represented a performance improvement when deserializing requests,
however, responses are still being deserialized with jackson, recently
we found that fastjson2 is the [fastest serialization library for the
jvm](https://github.com/fabienrenaud/java-json-benchmark).

this PR adds an opt-in support for fastjson2 which provides an easy
integration with springboot codecs, the serializer relies on the same
jackson annotation, and just had to write a deserializer because of the
polymorphic nature of GraphQLRequest and GraphQLReponse

####  benchmark results

GraphQLRequest deserialization
<img width="1156" alt="image"
src="https://github.com/user-attachments/assets/8a1e56a2-0d6f-4118-904a-a5d584caac19">

GraphQLResponse serialization
<img width="1161" alt="image"
src="https://github.com/user-attachments/assets/cfa6c843-50de-42e2-8f8e-7722c722b048">

---------

Co-authored-by: Samuel Vazquez <samvazquez@expediagroup.com>
  • Loading branch information
samuelAndalon and Samuel Vazquez authored Oct 3, 2024
1 parent 1dbdf28 commit cca3202
Show file tree
Hide file tree
Showing 25 changed files with 320 additions and 621 deletions.
3 changes: 3 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ kotlinx-coroutines = "1.8.1"
# TODO kotlin 1.9 upgrade: fix GraphQLTestUtils and GenerateKotlinxClientIT
kotlinx-serialization = "1.6.3"
ktor = "2.3.12"
fastjson2 = "2.0.53"
maven-plugin-annotation = "3.13.1"
maven-plugin-api = "3.9.8"
maven-project = "2.2.1"
Expand Down Expand Up @@ -86,6 +87,8 @@ spring-boot-netty = { group = "org.springframework.boot", name = "spring-boot-st
spring-boot-webflux = { group = "org.springframework.boot", name = "spring-boot-starter-webflux", version.ref = "spring-boot" }
spring-webflux = { group = "org.springframework", name = "spring-webflux", version.ref = "spring" }
spring-context = { group = "org.springframework", name = "spring-context", version.ref = "spring" }
fastjson2 = { group = "com.alibaba.fastjson2", name = "fastjson2-kotlin", version.ref = "fastjson2" }
fastjson2-spring = { group = "com.alibaba.fastjson2", name = "fastjson2-extension-spring6", version.ref = "fastjson2" }

# security vulnerabilities overrides
commons-codec = { group = "commons-codec", name = "commons-codec", version.ref = "commons-codec" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,22 +120,6 @@
"allDeclaredFields": true,
"queryAllDeclaredMethods": true
},
{
"name":"com.expediagroup.graphql.server.types.GraphQLRequest",
"fields":[{"name":"Companion"}]
},
{
"name":"com.expediagroup.graphql.server.types.GraphQLRequest$Companion",
"methods":[{"name":"serializer","parameterTypes":[] }]
},
{
"name":"com.expediagroup.graphql.server.types.GraphQLBatchRequest",
"fields":[{"name":"Companion"}]
},
{
"name":"com.expediagroup.graphql.server.types.GraphQLBatchRequest$Companion",
"methods":[{"name":"serializer","parameterTypes":[] }]
},
{
"name": "com.expediagroup.graphql.server.types.GraphQLServerRequestDeserializer",
"methods": [
Expand Down
11 changes: 9 additions & 2 deletions servers/graphql-kotlin-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ description = "Common code for running a GraphQL server in any HTTP server frame
plugins {
id("com.expediagroup.graphql.conventions")
alias(libs.plugins.benchmark)
alias(libs.plugins.kotlin.serialization)
}

dependencies {
api(projects.graphqlKotlinSchemaGenerator)
api(projects.graphqlKotlinDataloaderInstrumentation)
api(projects.graphqlKotlinAutomaticPersistedQueries)
api(libs.jackson)
api(libs.kotlinx.serialization.json)
api(libs.fastjson2)
testImplementation(libs.kotlinx.coroutines.test)
testImplementation(libs.logback)
}
Expand All @@ -32,6 +31,14 @@ kotlin.sourceSets.getByName("benchmarks") {
}

benchmark {
configurations {
register("graphQLRequest") {
include("com.expediagroup.graphql.server.GraphQLServerRequest*")
}
register("graphQLResponse") {
include("com.expediagroup.graphql.server.GraphQLServerResponse*")
}
}
targets {
register("benchmarks") {
this as JvmBenchmarkTarget
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* 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.alibaba.fastjson2.JSON
import com.alibaba.fastjson2.JSONWriter
import com.alibaba.fastjson2.to
import com.expediagroup.graphql.server.types.GraphQLServerRequest
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
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(value = 5, jvmArgsAppend = ["--add-modules=jdk.incubator.vector", "-Dfastjson2.readerVector=true"])
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS)
open class GraphQLServerRequestBatchDeserializationBenchmark {
private val mapper = jacksonObjectMapper()
private lateinit var request: String
private lateinit var batchRequest: String

@Setup
fun setUp() {
JSON.config(JSONWriter.Feature.WriteNulls)
val loader = this::class.java.classLoader
val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n")
val variables = loader.getResource("StarWarsDetailsVariables.json")!!.readText()
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 JacksonDeserializeGraphQLBatchRequest(): GraphQLServerRequest = mapper.readValue(batchRequest)

@Benchmark
fun FastJsonDeserializeGraphQLBatchRequest(): GraphQLServerRequest = batchRequest.to<GraphQLServerRequest>()
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

package com.expediagroup.graphql.server

import com.expediagroup.graphql.server.testtypes.GraphQLServerRequest
import com.alibaba.fastjson2.JSON
import com.alibaba.fastjson2.JSONWriter
import com.alibaba.fastjson2.to
import com.expediagroup.graphql.server.types.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
Expand All @@ -30,16 +32,16 @@ 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)
@Fork(value = 5, jvmArgsAppend = ["--add-modules=jdk.incubator.vector", "-Dfastjson2.readerVector=true"])
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS)
open class GraphQLServerRequestDeserializationBenchmark {
private val mapper = jacksonObjectMapper()
private lateinit var request: String
private lateinit var batchRequest: String

@Setup
fun setUp() {
JSON.config(JSONWriter.Feature.WriteNulls)
val loader = this::class.java.classLoader
val operation = loader.getResource("StarWarsDetails.graphql")!!.readText().replace("\n", "\\n")
val variables = loader.getResource("StarWarsDetailsVariables.json")!!.readText()
Expand All @@ -50,25 +52,11 @@ open class GraphQLServerRequestDeserializationBenchmark {
"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)
fun FastJsonDeserializeGraphQLRequest(): GraphQLServerRequest = request.to()
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@

package com.expediagroup.graphql.server

import com.expediagroup.graphql.server.testtypes.GraphQLServerResponse
import com.alibaba.fastjson2.JSON
import com.alibaba.fastjson2.JSONWriter
import com.expediagroup.graphql.server.types.GraphQLBatchResponse
import com.expediagroup.graphql.server.types.GraphQLResponse
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
Expand All @@ -30,36 +32,32 @@ 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 {
@Fork(value = 5, jvmArgsAppend = ["--add-modules=jdk.incubator.vector", "-Dfastjson2.readerVector=true"])
@Warmup(iterations = 1, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 4, time = 5, timeUnit = TimeUnit.SECONDS)
open class GraphQLServerResponseBatchSerializationBenchmark {
private val mapper = jacksonObjectMapper()
private lateinit var response: String
private lateinit var batchResponse: String
private lateinit var batchResponse: GraphQLBatchResponse

@Setup
fun setUp() {
response = this::class.java.classLoader.getResource("StarWarsDetailsResponse.json")!!.readText()
batchResponse = """
[
$response,
$response,
$response,
$response
]
""".trimIndent()
JSON.config(JSONWriter.Feature.WriteNulls)
val data = mapper.readValue<Map<String, Any?>>(
this::class.java.classLoader.getResourceAsStream("StarWarsDetailsResponse.json")!!
)
batchResponse = GraphQLBatchResponse(
listOf(
GraphQLResponse(data),
GraphQLResponse(data),
GraphQLResponse(data),
GraphQLResponse(data)
)
)
}

@Benchmark
fun JacksonDeserializeGraphQLResponse(): GraphQLServerResponse = mapper.readValue(response)
fun JacksonSerializeGraphQLBatchResponse(): String = mapper.writeValueAsString(batchResponse)

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

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

@Benchmark
fun KSerializationDeserializeGraphQLBatchResponse(): GraphQLServerResponse = Json.decodeFromString(batchResponse)
fun FastJsonSerializeGraphQLBatchResponse(): String = JSON.toJSONString(batchResponse)
}
Loading

0 comments on commit cca3202

Please sign in to comment.