Skip to content

Commit 4f46d7d

Browse files
authored
Improve code and fix some bugs in the Ktor benchmarks (#7983)
* Improve and simplify the code in the benchmark portions "ktor" and "ktor-pgclient" In both projects: 1. Remove the manually created serializer variables as they don't improve performance as tested. In "ktor": 1. Extract a common `selectWorlds` function to simplify the code. 1. Separate the Single Database Query test from the Multiple Database Queries so that the `queries` parameter doesn't need to be parsed in the Single Database Query test. 1. Simplify a for loop and remove some redundant while loops. 1. Remove the redundant explicit `HttpStatusCode.OK` arguments. In "ktor-pgclient": 1. Set `pipeliningLimit = 100000` as in other Vert.x benchmarks. 1. Refactor `toBoxedInt` to not catch exceptions because catching exceptions is expensive. 1. Simplify `(1..queries).map { db.getWorld() }` to `List(queries) { db.getWorld() }`. * Fix the issues that Database Updates fails and Plaintext yields poor performance in the "ktor-pgclient" portion Database Updates fails due to 2 reasons: the data are not sorted before batch-updating which causes data race and deadlocks, and the prepared statement arguments in wrong order in the `Tuple`. Poor Plaintext performance especially with a lot of connections, is caused by a too low memory limit (`-Xmx1G`).
1 parent 1847278 commit 4f46d7d

File tree

4 files changed

+67
-83
lines changed

4 files changed

+67
-83
lines changed

frameworks/Kotlin/ktor/benchmark_config.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"plaintext_url": "/plaintext",
77
"json_url": "/json",
88
"db_url": "/db",
9-
"query_url": "/db?queries=",
9+
"query_url": "/queries?queries=",
1010
"update_url": "/updates?queries=",
1111
"fortune_url": "/fortunes",
1212

@@ -29,7 +29,7 @@
2929
"plaintext_url": "/plaintext",
3030
"json_url": "/json",
3131
"db_url": "/db",
32-
"query_url": "/db?queries=",
32+
"query_url": "/queries?queries=",
3333
"update_url": "/updates?queries=",
3434
"fortune_url": "/fortunes",
3535

@@ -52,7 +52,7 @@
5252
"plaintext_url": "/plaintext",
5353
"json_url": "/json",
5454
"db_url": "/db",
55-
"query_url": "/db?queries=",
55+
"query_url": "/queries?queries=",
5656
"update_url": "/updates?queries=",
5757
"fortune_url": "/fortunes",
5858

frameworks/Kotlin/ktor/ktor-pgclient.dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ COPY --from=build /app/build/libs/ktor-pgclient.jar ktor-pgclient.jar
1212

1313
EXPOSE 8080
1414

15-
CMD ["java", "-server", "-Xms1G", "-Xmx1G", "-XX:-UseBiasedLocking", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "ktor-pgclient.jar"]
15+
CMD ["java", "-server", "-XX:MaxRAMFraction=1", "-XX:-UseBiasedLocking", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "ktor-pgclient.jar"]

frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/main.kt

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import io.vertx.sqlclient.PoolOptions
1313
import io.vertx.sqlclient.Tuple
1414
import kotlinx.html.*
1515
import kotlinx.serialization.Serializable
16-
import kotlinx.serialization.builtins.ListSerializer
16+
import kotlinx.serialization.encodeToString
1717
import kotlinx.serialization.json.Json
1818
import java.util.concurrent.ThreadLocalRandom
1919

@@ -36,15 +36,15 @@ interface Repository {
3636

3737
class PgclientRepository : Repository {
3838
private val connectOptions =
39-
PgConnectOptions()
40-
.setPort(5432)
41-
.setHost("tfb-database")
42-
.setDatabase("hello_world")
43-
.setUser("benchmarkdbuser")
44-
.setPassword("benchmarkdbpass")
45-
.apply {
46-
cachePreparedStatements = true
47-
}
39+
PgConnectOptions().apply {
40+
port = 5432
41+
host = "tfb-database"
42+
database = "hello_world"
43+
user = "benchmarkdbuser"
44+
password = "benchmarkdbpass"
45+
cachePreparedStatements = true
46+
pipeliningLimit = 100000
47+
}
4848

4949
private val poolOptions = PoolOptions()
5050
private val client = ThreadLocal.withInitial { PgPool.client(connectOptions, poolOptions) }
@@ -67,20 +67,17 @@ class PgclientRepository : Repository {
6767
}
6868

6969
override suspend fun updateWorlds(worlds: List<World>) {
70-
val batch = worlds.map { Tuple.of(it.id, it.randomNumber) }
70+
// Worlds should be sorted before being batch-updated with to avoid data race and deadlocks.
71+
val batch = worlds.sortedBy { it.id }.map { Tuple.of(it.randomNumber, it.id) }
7172
client()
7273
.preparedQuery("update world set randomNumber = $1 where id = $2")
7374
.executeBatch(batch)
7475
.await()
7576
}
7677
}
7778

78-
fun String.toBoxedInt(range: IntRange): Int =
79-
try {
80-
this.toInt().coerceIn(range)
81-
} catch (e: NumberFormatException) {
82-
1
83-
}
79+
fun String.toBoxedInt(range: IntRange): Int? =
80+
toIntOrNull()?.coerceIn(range)
8481

8582
class MainTemplate : Template<HTML> {
8683
val content = Placeholder<HtmlBlockTag>()
@@ -121,10 +118,6 @@ class FortuneTemplate(
121118
fun main() {
122119
val db = PgclientRepository()
123120

124-
val messageSerializer = Message.serializer()
125-
val worldSerializer = World.serializer()
126-
val worldListSerializer = ListSerializer(World.serializer())
127-
128121
val server = embeddedServer(Netty, 8080, configure = {
129122
shareWorkGroup = true
130123
}) {
@@ -136,19 +129,19 @@ fun main() {
136129

137130
get("/json") {
138131
call.respondText(
139-
Json.encodeToString(messageSerializer, Message("Hello, World!")),
132+
Json.encodeToString(Message("Hello, World!")),
140133
ContentType.Application.Json
141134
)
142135
}
143136

144137
get("/db") {
145-
call.respondText(Json.encodeToString(worldSerializer, db.getWorld()), ContentType.Application.Json)
138+
call.respondText(Json.encodeToString(db.getWorld()), ContentType.Application.Json)
146139
}
147140

148141
get("/query") {
149142
val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1
150-
val worlds = (1..queries).map { db.getWorld() }
151-
call.respondText(Json.encodeToString(worldListSerializer, worlds), ContentType.Application.Json)
143+
val worlds = List(queries) { db.getWorld() }
144+
call.respondText(Json.encodeToString(worlds), ContentType.Application.Json)
152145
}
153146

154147
get("/fortunes") {
@@ -161,12 +154,12 @@ fun main() {
161154

162155
get("/updates") {
163156
val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1
164-
val worlds = (1..queries).map { db.getWorld() }
157+
val worlds = List(queries) { db.getWorld() }
165158
val newWorlds = worlds.map { it.copy(randomNumber = rand.nextInt(1, 10001)) }
166159

167160
db.updateWorlds(newWorlds)
168161

169-
call.respondText(Json.encodeToString(worldListSerializer, newWorlds), ContentType.Application.Json)
162+
call.respondText(Json.encodeToString(newWorlds), ContentType.Application.Json)
170163
}
171164
}
172165
}

frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt

Lines changed: 43 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ import kotlinx.coroutines.Dispatchers
1313
import kotlinx.coroutines.withContext
1414
import kotlinx.html.*
1515
import kotlinx.serialization.Serializable
16-
import kotlinx.serialization.builtins.ListSerializer
1716
import kotlinx.serialization.encodeToString
1817
import kotlinx.serialization.json.Json
18+
import java.sql.Connection
1919
import java.util.concurrent.ThreadLocalRandom
2020

2121
@Serializable
@@ -28,58 +28,68 @@ data class World(val id: Int, var randomNumber: Int)
2828
data class Fortune(val id: Int, var message: String)
2929

3030
fun Application.main() {
31-
val worldSerializer = World.serializer()
32-
val worldListSerializer = ListSerializer(World.serializer())
33-
3431
val dbRows = 10000
3532
val poolSize = 48
3633
val pool by lazy { HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) }) }
3734
val databaseDispatcher = Dispatchers.IO
3835

3936
install(DefaultHeaders)
4037

41-
val okContent = TextContent("Hello, World!", ContentType.Text.Plain, HttpStatusCode.OK).also { it.contentLength }
38+
val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain).also { it.contentLength }
4239

4340
routing {
4441
get("/plaintext") {
45-
call.respond(okContent)
42+
call.respond(helloWorldContent)
4643
}
4744

4845
get("/json") {
49-
call.respondText(
50-
Json.encodeToString(Message("Hello, world!")),
51-
ContentType.Application.Json,
52-
HttpStatusCode.OK
53-
)
46+
call.respondText(Json.encodeToString(Message("Hello, world!")), ContentType.Application.Json)
5447
}
5548

5649
get("/db") {
5750
val random = ThreadLocalRandom.current()
58-
val queries = call.queries()
59-
val result = ArrayList<World>(queries ?: 1)
6051

61-
withContext(databaseDispatcher) {
52+
val world = withContext(databaseDispatcher) {
6253
pool.connection.use { connection ->
6354
connection.prepareStatement("SELECT id, randomNumber FROM World WHERE id = ?").use { statement ->
64-
for (i in 1..(queries ?: 1)) {
65-
statement.setInt(1, random.nextInt(dbRows) + 1)
66-
statement.executeQuery().use { rs ->
67-
while (rs.next()) {
68-
result += World(rs.getInt(1), rs.getInt(2))
69-
}
70-
}
55+
statement.setInt(1, random.nextInt(dbRows) + 1)
56+
57+
statement.executeQuery().use { rs ->
58+
rs.next()
59+
World(rs.getInt(1), rs.getInt(2))
7160
}
7261
}
7362
}
7463
}
7564

76-
call.respondText(
77-
when (queries) {
78-
null -> Json.encodeToString(worldSerializer, result.single())
79-
else -> Json.encodeToString(worldListSerializer, result)
80-
},
81-
ContentType.Application.Json, HttpStatusCode.OK
82-
)
65+
call.respondText(Json.encodeToString(world), ContentType.Application.Json)
66+
}
67+
68+
fun Connection.selectWorlds(queries: Int, random: ThreadLocalRandom): List<World> {
69+
val result = ArrayList<World>(queries)
70+
prepareStatement("SELECT id, randomNumber FROM World WHERE id = ?").use { statement ->
71+
repeat(queries) {
72+
statement.setInt(1, random.nextInt(dbRows) + 1)
73+
74+
statement.executeQuery().use { rs ->
75+
rs.next()
76+
result += World(rs.getInt(1), rs.getInt(2))
77+
}
78+
}
79+
}
80+
81+
return result
82+
}
83+
84+
get("/queries") {
85+
val queries = call.queries()
86+
val random = ThreadLocalRandom.current()
87+
88+
val result = withContext(databaseDispatcher) {
89+
pool.connection.use { it.selectWorlds(queries, random) }
90+
}
91+
92+
call.respondText(Json.encodeToString(result), ContentType.Application.Json)
8393
}
8494

8595
get("/fortunes") {
@@ -119,21 +129,11 @@ fun Application.main() {
119129
get("/updates") {
120130
val queries = call.queries()
121131
val random = ThreadLocalRandom.current()
122-
val result = ArrayList<World>(queries ?: 1)
132+
val result: List<World>
123133

124134
withContext(databaseDispatcher) {
125135
pool.connection.use { connection ->
126-
connection.prepareStatement("SELECT id, randomNumber FROM World WHERE id = ?").use { statement ->
127-
for (i in 1..(queries ?: 1)) {
128-
statement.setInt(1, random.nextInt(dbRows) + 1)
129-
130-
statement.executeQuery().use { rs ->
131-
while (rs.next()) {
132-
result += World(rs.getInt(1), rs.getInt(2))
133-
}
134-
}
135-
}
136-
}
136+
result = connection.selectWorlds(queries, random)
137137

138138
result.forEach { it.randomNumber = random.nextInt(dbRows) + 1 }
139139

@@ -149,13 +149,7 @@ fun Application.main() {
149149
}
150150
}
151151

152-
call.respondText(
153-
when (queries) {
154-
null -> Json.encodeToString(worldSerializer, result.single())
155-
else -> Json.encodeToString(worldListSerializer, result)
156-
},
157-
ContentType.Application.Json, HttpStatusCode.OK
158-
)
152+
call.respondText(Json.encodeToString(result), ContentType.Application.Json)
159153
}
160154
}
161155
}
@@ -186,8 +180,5 @@ fun HikariConfig.configureMySql(poolSize: Int) {
186180
configureCommon(poolSize)
187181
}
188182

189-
fun ApplicationCall.queries() = try {
190-
request.queryParameters["queries"]?.toInt()?.coerceIn(1, 500)
191-
} catch (nfe: NumberFormatException) {
192-
1
193-
}
183+
fun ApplicationCall.queries() =
184+
request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1

0 commit comments

Comments
 (0)