Browse Source

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`).
Shreck Ye 2 years ago
parent
commit
4f46d7ded9

+ 3 - 3
frameworks/Kotlin/ktor/benchmark_config.json

@@ -6,7 +6,7 @@
         "plaintext_url": "/plaintext",
         "json_url": "/json",
         "db_url": "/db",
-        "query_url": "/db?queries=",
+        "query_url": "/queries?queries=",
         "update_url": "/updates?queries=",
         "fortune_url": "/fortunes",
 
@@ -29,7 +29,7 @@
         "plaintext_url": "/plaintext",
         "json_url": "/json",
         "db_url": "/db",
-        "query_url": "/db?queries=",
+        "query_url": "/queries?queries=",
         "update_url": "/updates?queries=",
         "fortune_url": "/fortunes",
 
@@ -52,7 +52,7 @@
         "plaintext_url": "/plaintext",
         "json_url": "/json",
         "db_url": "/db",
-        "query_url": "/db?queries=",
+        "query_url": "/queries?queries=",
         "update_url": "/updates?queries=",
         "fortune_url": "/fortunes",
 

+ 1 - 1
frameworks/Kotlin/ktor/ktor-pgclient.dockerfile

@@ -12,4 +12,4 @@ COPY --from=build /app/build/libs/ktor-pgclient.jar ktor-pgclient.jar
 
 EXPOSE 8080
 
-CMD ["java", "-server", "-Xms1G", "-Xmx1G", "-XX:-UseBiasedLocking", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "ktor-pgclient.jar"]
+CMD ["java", "-server", "-XX:MaxRAMFraction=1", "-XX:-UseBiasedLocking", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "ktor-pgclient.jar"]

+ 20 - 27
frameworks/Kotlin/ktor/ktor-pgclient/src/main/kotlin/main.kt

@@ -13,7 +13,7 @@ import io.vertx.sqlclient.PoolOptions
 import io.vertx.sqlclient.Tuple
 import kotlinx.html.*
 import kotlinx.serialization.Serializable
-import kotlinx.serialization.builtins.ListSerializer
+import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
 import java.util.concurrent.ThreadLocalRandom
 
@@ -36,15 +36,15 @@ interface Repository {
 
 class PgclientRepository : Repository {
     private val connectOptions =
-        PgConnectOptions()
-            .setPort(5432)
-            .setHost("tfb-database")
-            .setDatabase("hello_world")
-            .setUser("benchmarkdbuser")
-            .setPassword("benchmarkdbpass")
-            .apply {
-                cachePreparedStatements = true
-            }
+        PgConnectOptions().apply {
+            port = 5432
+            host = "tfb-database"
+            database = "hello_world"
+            user = "benchmarkdbuser"
+            password = "benchmarkdbpass"
+            cachePreparedStatements = true
+            pipeliningLimit = 100000
+        }
 
     private val poolOptions = PoolOptions()
     private val client = ThreadLocal.withInitial { PgPool.client(connectOptions, poolOptions) }
@@ -67,7 +67,8 @@ class PgclientRepository : Repository {
     }
 
     override suspend fun updateWorlds(worlds: List<World>) {
-        val batch = worlds.map { Tuple.of(it.id, it.randomNumber) }
+        // Worlds should be sorted before being batch-updated with to avoid data race and deadlocks.
+        val batch = worlds.sortedBy { it.id }.map { Tuple.of(it.randomNumber, it.id) }
         client()
             .preparedQuery("update world set randomNumber = $1 where id = $2")
             .executeBatch(batch)
@@ -75,12 +76,8 @@ class PgclientRepository : Repository {
     }
 }
 
-fun String.toBoxedInt(range: IntRange): Int =
-    try {
-        this.toInt().coerceIn(range)
-    } catch (e: NumberFormatException) {
-        1
-    }
+fun String.toBoxedInt(range: IntRange): Int? =
+    toIntOrNull()?.coerceIn(range)
 
 class MainTemplate : Template<HTML> {
     val content = Placeholder<HtmlBlockTag>()
@@ -121,10 +118,6 @@ class FortuneTemplate(
 fun main() {
     val db = PgclientRepository()
 
-    val messageSerializer = Message.serializer()
-    val worldSerializer = World.serializer()
-    val worldListSerializer = ListSerializer(World.serializer())
-
     val server = embeddedServer(Netty, 8080, configure = {
         shareWorkGroup = true
     }) {
@@ -136,19 +129,19 @@ fun main() {
 
             get("/json") {
                 call.respondText(
-                    Json.encodeToString(messageSerializer, Message("Hello, World!")),
+                    Json.encodeToString(Message("Hello, World!")),
                     ContentType.Application.Json
                 )
             }
 
             get("/db") {
-                call.respondText(Json.encodeToString(worldSerializer, db.getWorld()), ContentType.Application.Json)
+                call.respondText(Json.encodeToString(db.getWorld()), ContentType.Application.Json)
             }
 
             get("/query") {
                 val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1
-                val worlds = (1..queries).map { db.getWorld() }
-                call.respondText(Json.encodeToString(worldListSerializer, worlds), ContentType.Application.Json)
+                val worlds = List(queries) { db.getWorld() }
+                call.respondText(Json.encodeToString(worlds), ContentType.Application.Json)
             }
 
             get("/fortunes") {
@@ -161,12 +154,12 @@ fun main() {
 
             get("/updates") {
                 val queries = call.parameters["queries"]?.toBoxedInt(1..500) ?: 1
-                val worlds = (1..queries).map { db.getWorld() }
+                val worlds = List(queries) { db.getWorld() }
                 val newWorlds = worlds.map { it.copy(randomNumber = rand.nextInt(1, 10001)) }
 
                 db.updateWorlds(newWorlds)
 
-                call.respondText(Json.encodeToString(worldListSerializer, newWorlds), ContentType.Application.Json)
+                call.respondText(Json.encodeToString(newWorlds), ContentType.Application.Json)
             }
         }
     }

+ 43 - 52
frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt

@@ -13,9 +13,9 @@ import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import kotlinx.html.*
 import kotlinx.serialization.Serializable
-import kotlinx.serialization.builtins.ListSerializer
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
+import java.sql.Connection
 import java.util.concurrent.ThreadLocalRandom
 
 @Serializable
@@ -28,9 +28,6 @@ data class World(val id: Int, var randomNumber: Int)
 data class Fortune(val id: Int, var message: String)
 
 fun Application.main() {
-    val worldSerializer = World.serializer()
-    val worldListSerializer = ListSerializer(World.serializer())
-
     val dbRows = 10000
     val poolSize = 48
     val pool by lazy { HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) }) }
@@ -38,48 +35,61 @@ fun Application.main() {
 
     install(DefaultHeaders)
 
-    val okContent = TextContent("Hello, World!", ContentType.Text.Plain, HttpStatusCode.OK).also { it.contentLength }
+    val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain).also { it.contentLength }
 
     routing {
         get("/plaintext") {
-            call.respond(okContent)
+            call.respond(helloWorldContent)
         }
 
         get("/json") {
-            call.respondText(
-                Json.encodeToString(Message("Hello, world!")),
-                ContentType.Application.Json,
-                HttpStatusCode.OK
-            )
+            call.respondText(Json.encodeToString(Message("Hello, world!")), ContentType.Application.Json)
         }
 
         get("/db") {
             val random = ThreadLocalRandom.current()
-            val queries = call.queries()
-            val result = ArrayList<World>(queries ?: 1)
 
-            withContext(databaseDispatcher) {
+            val world = withContext(databaseDispatcher) {
                 pool.connection.use { connection ->
                     connection.prepareStatement("SELECT id, randomNumber FROM World WHERE id = ?").use { statement ->
-                        for (i in 1..(queries ?: 1)) {
-                            statement.setInt(1, random.nextInt(dbRows) + 1)
-                            statement.executeQuery().use { rs ->
-                                while (rs.next()) {
-                                    result += World(rs.getInt(1), rs.getInt(2))
-                                }
-                            }
+                        statement.setInt(1, random.nextInt(dbRows) + 1)
+
+                        statement.executeQuery().use { rs ->
+                            rs.next()
+                            World(rs.getInt(1), rs.getInt(2))
                         }
                     }
                 }
             }
 
-            call.respondText(
-                when (queries) {
-                    null -> Json.encodeToString(worldSerializer, result.single())
-                    else -> Json.encodeToString(worldListSerializer, result)
-                },
-                ContentType.Application.Json, HttpStatusCode.OK
-            )
+            call.respondText(Json.encodeToString(world), ContentType.Application.Json)
+        }
+
+        fun Connection.selectWorlds(queries: Int, random: ThreadLocalRandom): List<World> {
+            val result = ArrayList<World>(queries)
+            prepareStatement("SELECT id, randomNumber FROM World WHERE id = ?").use { statement ->
+                repeat(queries) {
+                    statement.setInt(1, random.nextInt(dbRows) + 1)
+
+                    statement.executeQuery().use { rs ->
+                        rs.next()
+                        result += World(rs.getInt(1), rs.getInt(2))
+                    }
+                }
+            }
+
+            return result
+        }
+
+        get("/queries") {
+            val queries = call.queries()
+            val random = ThreadLocalRandom.current()
+
+            val result = withContext(databaseDispatcher) {
+                pool.connection.use { it.selectWorlds(queries, random) }
+            }
+
+            call.respondText(Json.encodeToString(result), ContentType.Application.Json)
         }
 
         get("/fortunes") {
@@ -119,21 +129,11 @@ fun Application.main() {
         get("/updates") {
             val queries = call.queries()
             val random = ThreadLocalRandom.current()
-            val result = ArrayList<World>(queries ?: 1)
+            val result: List<World>
 
             withContext(databaseDispatcher) {
                 pool.connection.use { connection ->
-                    connection.prepareStatement("SELECT id, randomNumber FROM World WHERE id = ?").use { statement ->
-                        for (i in 1..(queries ?: 1)) {
-                            statement.setInt(1, random.nextInt(dbRows) + 1)
-
-                            statement.executeQuery().use { rs ->
-                                while (rs.next()) {
-                                    result += World(rs.getInt(1), rs.getInt(2))
-                                }
-                            }
-                        }
-                    }
+                    result = connection.selectWorlds(queries, random)
 
                     result.forEach { it.randomNumber = random.nextInt(dbRows) + 1 }
 
@@ -149,13 +149,7 @@ fun Application.main() {
                 }
             }
 
-            call.respondText(
-                when (queries) {
-                    null -> Json.encodeToString(worldSerializer, result.single())
-                    else -> Json.encodeToString(worldListSerializer, result)
-                },
-                ContentType.Application.Json, HttpStatusCode.OK
-            )
+            call.respondText(Json.encodeToString(result), ContentType.Application.Json)
         }
     }
 }
@@ -186,8 +180,5 @@ fun HikariConfig.configureMySql(poolSize: Int) {
     configureCommon(poolSize)
 }
 
-fun ApplicationCall.queries() = try {
-    request.queryParameters["queries"]?.toInt()?.coerceIn(1, 500)
-} catch (nfe: NumberFormatException) {
-    1
-}
+fun ApplicationCall.queries() =
+    request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1