Browse Source

Runtime improvements (#9722)

* upgrade ktor depds

* Update build.gradle.kts

* Update pom.xml

* Update pom.xml

* some optimizations

* improved r2dbc

* upgrade kotlin version

* fixed update

* cleanup

* cleanup 2

* fix db params

* simplify update

* added null checks

* revert /update change

* cleanup update

---------

Co-authored-by: Ilya Nemtsev <[email protected]>
Ilya Nemtsev 5 months ago
parent
commit
7c3a5bf26b

+ 1 - 1
frameworks/Kotlin/ktor/ktor-r2dbc/pom.xml

@@ -12,7 +12,7 @@
     <name>org.jetbrains.ktor tech-empower-framework-benchmark</name>
     <name>org.jetbrains.ktor tech-empower-framework-benchmark</name>
 
 
     <properties>
     <properties>
-        <kotlin.version>2.0.21</kotlin.version>
+        <kotlin.version>2.1.20</kotlin.version>
         <kotlin.coroutines.version>1.10.1</kotlin.coroutines.version>
         <kotlin.coroutines.version>1.10.1</kotlin.coroutines.version>
         <ktor.version>3.1.1</ktor.version>
         <ktor.version>3.1.1</ktor.version>
         <serialization.version>1.8.0</serialization.version>
         <serialization.version>1.8.0</serialization.version>

+ 30 - 11
frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt

@@ -32,6 +32,14 @@ import org.jetbrains.ktor.benchmarks.models.World
 import reactor.core.publisher.Flux
 import reactor.core.publisher.Flux
 import reactor.core.publisher.Mono
 import reactor.core.publisher.Mono
 import kotlin.random.Random
 import kotlin.random.Random
+import java.time.Duration
+
+private val json = Json {
+    prettyPrint = false
+    isLenient = true
+    ignoreUnknownKeys = true
+    coerceInputValues = true
+}
 
 
 fun Application.main() {
 fun Application.main() {
     val config = ApplicationConfig("application.conf")
     val config = ApplicationConfig("application.conf")
@@ -40,6 +48,7 @@ fun Application.main() {
     install(DefaultHeaders)
     install(DefaultHeaders)
 
 
     val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain)
     val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain)
+    val helloWorldMsg = Message("Hello, world!")
 
 
     routing {
     routing {
         get("/plaintext") {
         get("/plaintext") {
@@ -47,7 +56,7 @@ fun Application.main() {
         }
         }
 
 
         get("/json") {
         get("/json") {
-            call.respondText(Json.encodeToString(Message("Hello, world!")), ContentType.Application.Json)
+            call.respondText(json.encodeToString(helloWorldMsg), ContentType.Application.Json)
         }
         }
 
 
         get("/db") {
         get("/db") {
@@ -55,7 +64,7 @@ fun Application.main() {
             val request = getWorld(dbConnFactory, random)
             val request = getWorld(dbConnFactory, random)
             val result = request.awaitFirstOrNull()
             val result = request.awaitFirstOrNull()
 
 
-            call.respondText(Json.encodeToString(result), ContentType.Application.Json)
+            call.respondText(json.encodeToString(result), ContentType.Application.Json)
         }
         }
 
 
         fun selectWorlds(queries: Int, random: Random): Flow<World> = flow {
         fun selectWorlds(queries: Int, random: Random): Flow<World> = flow {
@@ -74,7 +83,7 @@ fun Application.main() {
                 }
                 }
             }
             }
 
 
-            call.respondText(Json.encodeToString(result), ContentType.Application.Json)
+            call.respondText(json.encodeToString(result), ContentType.Application.Json)
         }
         }
 
 
         get("/fortunes") {
         get("/fortunes") {
@@ -135,7 +144,7 @@ fun Application.main() {
                 }
                 }
             }
             }
 
 
-            call.respondText(Json.encodeToString(worldsUpdated), ContentType.Application.Json)
+            call.respondText(json.encodeToString(worldsUpdated), ContentType.Application.Json)
         }
         }
     }
     }
 }
 }
@@ -143,13 +152,20 @@ fun Application.main() {
 private fun getWorld(
 private fun getWorld(
     dbConnFactory: ConnectionFactory, random: Random
     dbConnFactory: ConnectionFactory, random: Random
 ): Mono<World> = Mono.usingWhen(dbConnFactory.create(), { connection ->
 ): Mono<World> = Mono.usingWhen(dbConnFactory.create(), { connection ->
-    Mono.from(connection.createStatement(WORLD_QUERY).bind(0, random.nextInt(DB_ROWS) + 1).execute()).flatMap { r ->
-        Mono.from(r.map { row, _ ->
-            World(
-                row.get(0, Int::class.java)!!, row.get(1, Int::class.java)!!
-            )
-        })
-    }
+    Mono.from(connection.createStatement(WORLD_QUERY)
+        .bind("$1", random.nextInt(DB_ROWS) + 1)
+        .execute())
+        .flatMap { r ->
+            Mono.from(r.map { row, _ ->
+                val id = row.get(0, Int::class.java)
+                val randomNumber = row.get(1, Int::class.java)
+                if (id != null && randomNumber != null) {
+                    World(id, randomNumber)
+                } else {
+                    throw IllegalStateException("Database returned null values for required fields")
+                }
+            })
+        }
 }, Connection::close)
 }, Connection::close)
 
 
 private fun configurePostgresR2DBC(config: ApplicationConfig): ConnectionFactory {
 private fun configurePostgresR2DBC(config: ApplicationConfig): ConnectionFactory {
@@ -170,6 +186,9 @@ private fun configurePostgresR2DBC(config: ApplicationConfig): ConnectionFactory
     val cp = ConnectionPoolConfiguration.builder(cf)
     val cp = ConnectionPoolConfiguration.builder(cf)
         .initialSize(config.property("db.initPoolSize").getString().toInt())
         .initialSize(config.property("db.initPoolSize").getString().toInt())
         .maxSize(config.property("db.maxPoolSize").getString().toInt())
         .maxSize(config.property("db.maxPoolSize").getString().toInt())
+        .maxIdleTime(Duration.ofSeconds(30))
+        .maxAcquireTime(Duration.ofSeconds(5))
+        .validationQuery("SELECT 1")
         .build()
         .build()
 
 
     return ConnectionPool(cp)
     return ConnectionPool(cp)

+ 1 - 1
frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/Fortune.kt

@@ -3,4 +3,4 @@ package org.jetbrains.ktor.benchmarks.models
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Serializable
 
 
 @Serializable
 @Serializable
-class Fortune(val id: Int, var message: String)
+data class Fortune(val id: Int, var message: String)

+ 6 - 1
frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/Message.kt

@@ -3,4 +3,9 @@ package org.jetbrains.ktor.benchmarks.models
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Serializable
 
 
 @Serializable
 @Serializable
-class Message(val message: String)
+data class Message(val message: String)
+
+// Cache common messages to reduce allocations
+object MessageCache {
+    val helloWorld = Message("Hello, world!")
+}

+ 1 - 1
frameworks/Kotlin/ktor/ktor-r2dbc/src/main/kotlin/org/jetbrains/ktor/benchmarks/models/World.kt

@@ -3,4 +3,4 @@ package org.jetbrains.ktor.benchmarks.models
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.Serializable
 
 
 @Serializable
 @Serializable
-class World(val id: Int, var randomNumber: Int)
+data class World(val id: Int, var randomNumber: Int)

+ 10 - 10
frameworks/Kotlin/ktor/ktor-r2dbc/src/main/resources/logback.xml

@@ -1,21 +1,21 @@
 <configuration>
 <configuration>
     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
         <encoder>
-            <pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+            <pattern>%msg%n</pattern>
         </encoder>
         </encoder>
     </appender>
     </appender>
 
 
-     <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
-         <neverBlock>true</neverBlock>
-         <appender-ref ref="STDOUT" />
-     </appender>
-
+    <appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
+        <neverBlock>true</neverBlock>
+        <appender-ref ref="STDOUT" />
+    </appender>
 
 
-    <root level="INFO">
+    <root level="WARN">
         <appender-ref ref="ASYNC"/>
         <appender-ref ref="ASYNC"/>
     </root>
     </root>
 
 
-    <logger name="org.eclipse.jetty" level="INFO"/>
-    <logger name="io.netty" level="INFO"/>
-
+    <logger name="org.eclipse.jetty" level="WARN"/>
+    <logger name="io.netty" level="WARN"/>
+    <logger name="io.r2dbc" level="WARN"/>
+    <logger name="reactor" level="WARN"/>
 </configuration>
 </configuration>

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

@@ -10,4 +10,4 @@ COPY --from=maven /ktor/target/tech-empower-framework-benchmark-1.0-SNAPSHOT-net
 
 
 EXPOSE 9090
 EXPOSE 9090
 
 
-CMD ["java", "-server","-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app.jar"]
+CMD ["java", "-server","-XX:+UseNUMA", "-XX:+UseG1GC", "-XX:+AlwaysPreTouch", "-XX:+UseStringDeduplication", "-jar", "app.jar"]

+ 1 - 1
frameworks/Kotlin/ktor/ktor/pom.xml

@@ -12,7 +12,7 @@
     <name>org.jetbrains.ktor tech-empower-framework-benchmark</name>
     <name>org.jetbrains.ktor tech-empower-framework-benchmark</name>
 
 
     <properties>
     <properties>
-        <kotlin.version>2.0.21</kotlin.version>
+        <kotlin.version>2.1.20</kotlin.version>
         <ktor.version>3.1.1</ktor.version>
         <ktor.version>3.1.1</ktor.version>
         <serialization.version>1.8.0</serialization.version>
         <serialization.version>1.8.0</serialization.version>
         <kotlinx.html.version>0.12.0</kotlinx.html.version>
         <kotlinx.html.version>0.12.0</kotlinx.html.version>

+ 29 - 20
frameworks/Kotlin/ktor/ktor/src/main/kotlin/org/jetbrains/ktor/benchmarks/Hello.kt

@@ -20,6 +20,7 @@ import org.jetbrains.ktor.benchmarks.Constants.WORLD_QUERY
 import java.sql.Connection
 import java.sql.Connection
 import java.util.concurrent.ThreadLocalRandom
 import java.util.concurrent.ThreadLocalRandom
 import kotlin.random.Random
 import kotlin.random.Random
+import kotlinx.serialization.Contextual
 
 
 @Serializable
 @Serializable
 data class Message(val message: String)
 data class Message(val message: String)
@@ -30,15 +31,26 @@ data class World(val id: Int, var randomNumber: Int)
 @Serializable
 @Serializable
 data class Fortune(val id: Int, var message: String)
 data class Fortune(val id: Int, var message: String)
 
 
+// Optimized JSON instance with better performance settings
+private val json = Json {
+    prettyPrint = false
+    isLenient = true
+    ignoreUnknownKeys = true
+    coerceInputValues = true
+}
+
 fun Application.main() {
 fun Application.main() {
     val dbRows = 10000
     val dbRows = 10000
-    val poolSize = 48
+    val poolSize = Runtime.getRuntime().availableProcessors() * 2
     val pool = HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) })
     val pool = HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) })
-    val databaseDispatcher = Dispatchers.IO
+    
+    // Create a dedicated dispatcher for database operations
+    val databaseDispatcher = Dispatchers.IO.limitedParallelism(poolSize)
 
 
     install(DefaultHeaders)
     install(DefaultHeaders)
 
 
     val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain)
     val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain)
+    val jsonResponse = json.encodeToString(Message("Hello, world!"))
 
 
     routing {
     routing {
         get("/plaintext") {
         get("/plaintext") {
@@ -46,7 +58,7 @@ fun Application.main() {
         }
         }
 
 
         get("/json") {
         get("/json") {
-            call.respondText(Json.encodeToString(Message("Hello, world!")), ContentType.Application.Json)
+            call.respondText(jsonResponse, ContentType.Application.Json)
         }
         }
 
 
         get("/db") {
         get("/db") {
@@ -56,7 +68,6 @@ fun Application.main() {
                 pool.connection.use { connection ->
                 pool.connection.use { connection ->
                     connection.prepareStatement(WORLD_QUERY).use { statement ->
                     connection.prepareStatement(WORLD_QUERY).use { statement ->
                         statement.setInt(1, random.nextInt(dbRows) + 1)
                         statement.setInt(1, random.nextInt(dbRows) + 1)
-
                         statement.executeQuery().use { rs ->
                         statement.executeQuery().use { rs ->
                             rs.next()
                             rs.next()
                             World(rs.getInt(1), rs.getInt(2))
                             World(rs.getInt(1), rs.getInt(2))
@@ -65,7 +76,7 @@ fun Application.main() {
                 }
                 }
             }
             }
 
 
-            call.respondText(Json.encodeToString(world), ContentType.Application.Json)
+            call.respondText(json.encodeToString(world), ContentType.Application.Json)
         }
         }
 
 
         fun Connection.selectWorlds(queries: Int, random: Random): List<World> {
         fun Connection.selectWorlds(queries: Int, random: Random): List<World> {
@@ -73,14 +84,12 @@ fun Application.main() {
             prepareStatement(WORLD_QUERY).use { statement ->
             prepareStatement(WORLD_QUERY).use { statement ->
                 repeat(queries) {
                 repeat(queries) {
                     statement.setInt(1, random.nextInt(dbRows) + 1)
                     statement.setInt(1, random.nextInt(dbRows) + 1)
-
                     statement.executeQuery().use { rs ->
                     statement.executeQuery().use { rs ->
                         rs.next()
                         rs.next()
                         result += World(rs.getInt(1), rs.getInt(2))
                         result += World(rs.getInt(1), rs.getInt(2))
                     }
                     }
                 }
                 }
             }
             }
-
             return result
             return result
         }
         }
 
 
@@ -92,7 +101,7 @@ fun Application.main() {
                 pool.connection.use { it.selectWorlds(queries, random) }
                 pool.connection.use { it.selectWorlds(queries, random) }
             }
             }
 
 
-            call.respondText(Json.encodeToString(result), ContentType.Application.Json)
+            call.respondText(json.encodeToString(result), ContentType.Application.Json)
         }
         }
 
 
         get("/fortunes") {
         get("/fortunes") {
@@ -137,22 +146,20 @@ fun Application.main() {
             withContext(databaseDispatcher) {
             withContext(databaseDispatcher) {
                 pool.connection.use { connection ->
                 pool.connection.use { connection ->
                     result = connection.selectWorlds(queries, random)
                     result = connection.selectWorlds(queries, random)
-
                     result.forEach { it.randomNumber = random.nextInt(dbRows) + 1 }
                     result.forEach { it.randomNumber = random.nextInt(dbRows) + 1 }
 
 
                     connection.prepareStatement(UPDATE_QUERY).use { updateStatement ->
                     connection.prepareStatement(UPDATE_QUERY).use { updateStatement ->
-                            for ((id, randomNumber) in result) {
-                                updateStatement.setInt(1, randomNumber)
-                                updateStatement.setInt(2, id)
-                                updateStatement.addBatch()
-                            }
-
-                            updateStatement.executeBatch()
+                        for ((id, randomNumber) in result) {
+                            updateStatement.setInt(1, randomNumber)
+                            updateStatement.setInt(2, id)
+                            updateStatement.addBatch()
                         }
                         }
+                        updateStatement.executeBatch()
+                    }
                 }
                 }
             }
             }
 
 
-            call.respondText(Json.encodeToString(result), ContentType.Application.Json)
+            call.respondText(json.encodeToString(result), ContentType.Application.Json)
         }
         }
     }
     }
 }
 }
@@ -160,7 +167,6 @@ fun Application.main() {
 fun HikariConfig.configurePostgres(poolSize: Int) {
 fun HikariConfig.configurePostgres(poolSize: Int) {
     jdbcUrl = "jdbc:postgresql://tfb-database/hello_world?useSSL=false"
     jdbcUrl = "jdbc:postgresql://tfb-database/hello_world?useSSL=false"
     driverClassName = org.postgresql.Driver::class.java.name
     driverClassName = org.postgresql.Driver::class.java.name
-
     configureCommon(poolSize)
     configureCommon(poolSize)
 }
 }
 
 
@@ -172,9 +178,13 @@ fun HikariConfig.configureCommon(poolSize: Int) {
     addDataSourceProperty("useUnbufferedInput", "false")
     addDataSourceProperty("useUnbufferedInput", "false")
     addDataSourceProperty("prepStmtCacheSize", "4096")
     addDataSourceProperty("prepStmtCacheSize", "4096")
     addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
     addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
-    connectionTimeout = 10000
+    connectionTimeout = 5000 
     maximumPoolSize = poolSize
     maximumPoolSize = poolSize
     minimumIdle = poolSize
     minimumIdle = poolSize
+    idleTimeout = 300000 // 5 minutes
+    maxLifetime = 600000 // 10 minutes
+    validationTimeout = 5000
+    leakDetectionThreshold = 60000
 }
 }
 
 
 fun HikariConfig.configureMySql(poolSize: Int) {
 fun HikariConfig.configureMySql(poolSize: Int) {
@@ -186,7 +196,6 @@ fun HikariConfig.configureMySql(poolSize: Int) {
 fun ApplicationCall.queries() =
 fun ApplicationCall.queries() =
     request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1
     request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1
 
 
-
 object Constants {
 object Constants {
     const val WORLD_QUERY = "SELECT id, randomNumber FROM World WHERE id = ?"
     const val WORLD_QUERY = "SELECT id, randomNumber FROM World WHERE id = ?"
     const val FORTUNES_QUERY = "SELECT id, message FROM fortune"
     const val FORTUNES_QUERY = "SELECT id, message FROM fortune"