Skip to content
2 changes: 1 addition & 1 deletion frameworks/Kotlin/ktor/ktor-r2dbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<name>org.jetbrains.ktor tech-empower-framework-benchmark</name>

<properties>
<kotlin.version>2.0.21</kotlin.version>
<kotlin.version>2.1.20</kotlin.version>
<kotlin.coroutines.version>1.10.1</kotlin.coroutines.version>
<ktor.version>3.1.1</ktor.version>
<serialization.version>1.8.0</serialization.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ import org.jetbrains.ktor.benchmarks.models.World
import reactor.core.publisher.Flux
import reactor.core.publisher.Mono
import kotlin.random.Random
import java.time.Duration

private val json = Json {
prettyPrint = false
isLenient = true
ignoreUnknownKeys = true
coerceInputValues = true
}

fun Application.main() {
val config = ApplicationConfig("application.conf")
Expand All @@ -40,22 +48,23 @@ fun Application.main() {
install(DefaultHeaders)

val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain)
val helloWorldMsg = Message("Hello, world!")

routing {
get("/plaintext") {
call.respond(helloWorldContent)
}

get("/json") {
call.respondText(Json.encodeToString(Message("Hello, world!")), ContentType.Application.Json)
call.respondText(json.encodeToString(helloWorldMsg), ContentType.Application.Json)
}

get("/db") {
val random = Random.Default
val request = getWorld(dbConnFactory, random)
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 {
Expand All @@ -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") {
Expand Down Expand Up @@ -135,21 +144,28 @@ fun Application.main() {
}
}

call.respondText(Json.encodeToString(worldsUpdated), ContentType.Application.Json)
call.respondText(json.encodeToString(worldsUpdated), ContentType.Application.Json)
}
}
}

private fun getWorld(
dbConnFactory: ConnectionFactory, random: Random
): 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)

private fun configurePostgresR2DBC(config: ApplicationConfig): ConnectionFactory {
Expand All @@ -170,6 +186,9 @@ private fun configurePostgresR2DBC(config: ApplicationConfig): ConnectionFactory
val cp = ConnectionPoolConfiguration.builder(cf)
.initialSize(config.property("db.initPoolSize").getString().toInt())
.maxSize(config.property("db.maxPoolSize").getString().toInt())
.maxIdleTime(Duration.ofSeconds(30))
.maxAcquireTime(Duration.ofSeconds(5))
.validationQuery("SELECT 1")
.build()

return ConnectionPool(cp)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package org.jetbrains.ktor.benchmarks.models
import kotlinx.serialization.Serializable

@Serializable
class Fortune(val id: Int, var message: String)
data class Fortune(val id: Int, var message: String)
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,9 @@ package org.jetbrains.ktor.benchmarks.models
import kotlinx.serialization.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!")
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ package org.jetbrains.ktor.benchmarks.models
import kotlinx.serialization.Serializable

@Serializable
class World(val id: Int, var randomNumber: Int)
data class World(val id: Int, var randomNumber: Int)
20 changes: 10 additions & 10 deletions frameworks/Kotlin/ktor/ktor-r2dbc/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<pattern>%msg%n</pattern>
</encoder>
</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"/>
</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>
2 changes: 1 addition & 1 deletion frameworks/Kotlin/ktor/ktor.dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ COPY --from=maven /ktor/target/tech-empower-framework-benchmark-1.0-SNAPSHOT-net

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"]
2 changes: 1 addition & 1 deletion frameworks/Kotlin/ktor/ktor/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
<name>org.jetbrains.ktor tech-empower-framework-benchmark</name>

<properties>
<kotlin.version>2.0.21</kotlin.version>
<kotlin.version>2.1.20</kotlin.version>
<ktor.version>3.1.1</ktor.version>
<serialization.version>1.8.0</serialization.version>
<kotlinx.html.version>0.12.0</kotlinx.html.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.jetbrains.ktor.benchmarks.Constants.WORLD_QUERY
import java.sql.Connection
import java.util.concurrent.ThreadLocalRandom
import kotlin.random.Random
import kotlinx.serialization.Contextual

@Serializable
data class Message(val message: String)
Expand All @@ -30,23 +31,34 @@ data class World(val id: Int, var randomNumber: Int)
@Serializable
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() {
val dbRows = 10000
val poolSize = 48
val poolSize = Runtime.getRuntime().availableProcessors() * 2
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)

val helloWorldContent = TextContent("Hello, World!", ContentType.Text.Plain)
val jsonResponse = json.encodeToString(Message("Hello, world!"))

routing {
get("/plaintext") {
call.respond(helloWorldContent)
}

get("/json") {
call.respondText(Json.encodeToString(Message("Hello, world!")), ContentType.Application.Json)
call.respondText(jsonResponse, ContentType.Application.Json)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I was just making some updates for Ktor and I noticed this might be in violation of one of the test rules:

For each request, an object mapping the key message to Hello, World! must be instantiated.

Is this not applicable here, or should I change it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bjhham I just checked, you are correct, I missed this rule. We should fix this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bah, I'm going too quickly this morning. You are correct - you need to instantiate the object on every request with the json test.

Your plaintext is fine.

Unfortunately, Github won't let me un-merge - can you please open a second PR and resolve?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'll open one soon with a few tweaks to response streaming and an update to 3.1.2. as well. Thanks for updating it to 3.0 btw, I had intended to in 2024 but it slipped my mind.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No worries, I really enjoy using Ktor.

I struggled to optimize "/update" endpoint though, maybe you know a better way 😅

}

get("/db") {
Expand All @@ -56,7 +68,6 @@ fun Application.main() {
pool.connection.use { connection ->
connection.prepareStatement(WORLD_QUERY).use { statement ->
statement.setInt(1, random.nextInt(dbRows) + 1)

statement.executeQuery().use { rs ->
rs.next()
World(rs.getInt(1), rs.getInt(2))
Expand All @@ -65,22 +76,20 @@ 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> {
val result = ArrayList<World>(queries)
prepareStatement(WORLD_QUERY).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
}

Expand All @@ -92,7 +101,7 @@ fun Application.main() {
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") {
Expand Down Expand Up @@ -137,30 +146,27 @@ fun Application.main() {
withContext(databaseDispatcher) {
pool.connection.use { connection ->
result = connection.selectWorlds(queries, random)

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

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)
}
}
}

fun HikariConfig.configurePostgres(poolSize: Int) {
jdbcUrl = "jdbc:postgresql://tfb-database/hello_world?useSSL=false"
driverClassName = org.postgresql.Driver::class.java.name

configureCommon(poolSize)
}

Expand All @@ -172,9 +178,13 @@ fun HikariConfig.configureCommon(poolSize: Int) {
addDataSourceProperty("useUnbufferedInput", "false")
addDataSourceProperty("prepStmtCacheSize", "4096")
addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
connectionTimeout = 10000
connectionTimeout = 5000
maximumPoolSize = poolSize
minimumIdle = poolSize
idleTimeout = 300000 // 5 minutes
maxLifetime = 600000 // 10 minutes
validationTimeout = 5000
leakDetectionThreshold = 60000
}

fun HikariConfig.configureMySql(poolSize: Int) {
Expand All @@ -186,7 +196,6 @@ fun HikariConfig.configureMySql(poolSize: Int) {
fun ApplicationCall.queries() =
request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1


object Constants {
const val WORLD_QUERY = "SELECT id, randomNumber FROM World WHERE id = ?"
const val FORTUNES_QUERY = "SELECT id, message FROM fortune"
Expand Down
Loading