Skip to content

Commit 98bcf6b

Browse files
ShreckYefranz1981
authored andcommitted
New benchmark: Ktor and Exposed (TechEmpower#7995)
* Run `tfb --new` * Change "orm" to "Full" * Move and merge the portion into the "ktor" directory * `gradle init` * Remove unneeded code * Set up Gradle, copy the code from the "ktor" portion and adapt, and set up the Dockerfile * Adapt with Exposed DSL * Resolve the issue that performance is low for Database Updates * Implement the tests with Exposed DAO Multiple Database Queries and Database Updates fail with DAO, however. * Disable Multiple Database Queries and Database Updates in the Exposed DAO benchmark because it appears to cache results * Clean up and update the README.md * Add some missing spaces
1 parent d845e39 commit 98bcf6b

File tree

13 files changed

+705
-0
lines changed

13 files changed

+705
-0
lines changed

frameworks/Kotlin/ktor/benchmark_config.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,46 @@
139139
"display_name": "ktor-pgclient",
140140
"notes": "http://ktor.io/",
141141
"versus": "netty"
142+
},
143+
"exposed-dsl": {
144+
"db_url": "/db",
145+
"query_url": "/queries?queries=",
146+
"update_url": "/updates?queries=",
147+
"fortune_url": "/fortunes",
148+
"port": 8080,
149+
"approach": "Realistic",
150+
"classification": "Micro",
151+
"database": "postgres",
152+
"framework": "ktor",
153+
"language": "Kotlin",
154+
"flavor": "None",
155+
"orm": "Full",
156+
"platform": "Netty",
157+
"webserver": "None",
158+
"os": "Linux",
159+
"database_os": "Linux",
160+
"display_name": "ktor-netty-exposed-dsl",
161+
"notes": "",
162+
"versus": "ktor"
163+
},
164+
"exposed-dao": {
165+
"db_url": "/db",
166+
"fortune_url": "/fortunes",
167+
"port": 8080,
168+
"approach": "Realistic",
169+
"classification": "Micro",
170+
"database": "postgres",
171+
"framework": "ktor",
172+
"language": "Kotlin",
173+
"flavor": "None",
174+
"orm": "Full",
175+
"platform": "Netty",
176+
"webserver": "None",
177+
"os": "Linux",
178+
"database_os": "Linux",
179+
"display_name": "ktor-netty-exposed-dao",
180+
"notes": "",
181+
"versus": "ktor"
142182
}
143183
}
144184
]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM gradle:8.0.2-jdk11
2+
3+
WORKDIR /ktor-exposed
4+
COPY ktor-exposed/settings.gradle.kts settings.gradle.kts
5+
COPY ktor-exposed/app app
6+
RUN gradle shadowJar
7+
8+
EXPOSE 8080
9+
10+
CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app/build/libs/app-all.jar", "Dao"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM gradle:8.0.2-jdk11
2+
3+
WORKDIR /ktor-exposed
4+
COPY ktor-exposed/settings.gradle.kts settings.gradle.kts
5+
COPY ktor-exposed/app app
6+
RUN gradle shadowJar
7+
8+
EXPOSE 8080
9+
10+
CMD ["java", "-server", "-XX:+UseNUMA", "-XX:+UseParallelGC", "-XX:+AlwaysPreTouch", "-jar", "app/build/libs/app-all.jar", "Dsl"]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#
2+
# https://help.github.com/articles/dealing-with-line-endings/
3+
#
4+
# Linux start script should use lf
5+
/gradlew text eol=lf
6+
7+
# These are Windows script files and should use crlf
8+
*.bat text eol=crlf
9+
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Ignore Gradle project-specific cache directory
2+
.gradle
3+
4+
# Ignore Gradle build output directory
5+
build
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# ktor-exposed Benchmarking Test
2+
3+
### Test Type Implementation Source Code
4+
5+
* [DB](app/src/main/kotlin)
6+
* [QUERY](app/src/main/kotlin)
7+
* [UPDATE](app/src/main/kotlin)
8+
* [FORTUNES](app/src/main/kotlin)
9+
10+
## Important Libraries
11+
The tests were run with:
12+
* [Ktor](https://ktor.io/)
13+
* [Exposed](https://github.com/JetBrains/Exposed)
14+
15+
## Test URLs
16+
### DB
17+
18+
http://localhost:8080/db
19+
20+
### QUERY
21+
22+
http://localhost:8080/query?queries=
23+
24+
### UPDATE
25+
26+
http://localhost:8080/update?queries=
27+
28+
### FORTUNES
29+
30+
http://localhost:8080/fortunes
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
plugins {
2+
kotlin("jvm") version "1.8.10"
3+
kotlin("plugin.serialization") version "1.8.10"
4+
application
5+
id("com.github.johnrengelman.shadow") version "8.1.0"
6+
}
7+
8+
repositories {
9+
mavenCentral()
10+
}
11+
12+
val ktorVersion = "2.2.3"
13+
val kotlinxSerializationVersion = "1.5.0"
14+
val exposedVersion = "0.41.1"
15+
16+
dependencies {
17+
implementation("io.ktor:ktor-server-core:$ktorVersion")
18+
implementation("io.ktor:ktor-server-netty:$ktorVersion")
19+
implementation("io.ktor:ktor-server-html-builder-jvm:$ktorVersion")
20+
implementation("io.ktor:ktor-server-default-headers-jvm:$ktorVersion")
21+
22+
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")
23+
24+
implementation("org.jetbrains.exposed:exposed-core:$exposedVersion")
25+
implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion")
26+
implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion")
27+
28+
implementation("org.postgresql:postgresql:42.5.4")
29+
implementation("com.zaxxer:HikariCP:5.0.1")
30+
runtimeOnly("org.slf4j:slf4j-simple:1.7.36")
31+
}
32+
33+
application.mainClass.set("AppKt")
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
import ExposedMode.*
2+
import com.zaxxer.hikari.HikariConfig
3+
import com.zaxxer.hikari.HikariDataSource
4+
import io.ktor.http.*
5+
import io.ktor.server.application.*
6+
import io.ktor.server.engine.*
7+
import io.ktor.server.html.*
8+
import io.ktor.server.netty.*
9+
import io.ktor.server.plugins.defaultheaders.*
10+
import io.ktor.server.response.*
11+
import io.ktor.server.routing.*
12+
import kotlinx.coroutines.Dispatchers
13+
import kotlinx.coroutines.withContext
14+
import kotlinx.html.*
15+
import kotlinx.serialization.Serializable
16+
import kotlinx.serialization.encodeToString
17+
import kotlinx.serialization.json.Json
18+
import org.jetbrains.exposed.dao.IntEntity
19+
import org.jetbrains.exposed.dao.IntEntityClass
20+
import org.jetbrains.exposed.dao.id.EntityID
21+
import org.jetbrains.exposed.dao.id.IdTable
22+
import org.jetbrains.exposed.sql.*
23+
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
24+
import org.jetbrains.exposed.sql.transactions.transaction
25+
import java.util.concurrent.ThreadLocalRandom
26+
27+
@Serializable
28+
data class World(val id: Int, var randomNumber: Int)
29+
30+
@Serializable
31+
data class Fortune(val id: Int, var message: String)
32+
33+
34+
// see "toolset/databases/postgres/create-postgres.sql"
35+
36+
object WorldTable : IdTable<Int>("World") {
37+
override val id = integer("id").entityId()
38+
val randomNumber = integer("randomnumber").default(0) // The name is "randomNumber" in "create-postgres.sql".
39+
}
40+
41+
object FortuneTable : IdTable<Int>("Fortune") {
42+
override val id = integer("id").entityId()
43+
val message = varchar("message", 2048)
44+
}
45+
46+
47+
class WorldDao(id: EntityID<Int>) : IntEntity(id) {
48+
companion object : IntEntityClass<WorldDao>(WorldTable)
49+
50+
var randomNumber by WorldTable.randomNumber
51+
fun toWorld() =
52+
World(id.value, randomNumber)
53+
}
54+
55+
class FortuneDao(id: EntityID<Int>) : IntEntity(id) {
56+
companion object : IntEntityClass<FortuneDao>(FortuneTable)
57+
58+
var message by FortuneTable.message
59+
fun toFortune() =
60+
Fortune(id.value, message)
61+
}
62+
63+
64+
enum class ExposedMode {
65+
Dsl, Dao
66+
}
67+
68+
fun main(args: Array<String>) {
69+
val exposedMode = valueOf(args.first())
70+
embeddedServer(Netty, port = 8080) { module(exposedMode) }.start(wait = true)
71+
}
72+
73+
fun Application.module(exposedMode: ExposedMode) {
74+
val dbRows = 10000
75+
val poolSize = 48
76+
val pool = HikariDataSource(HikariConfig().apply { configurePostgres(poolSize) })
77+
Database.connect(pool)
78+
suspend fun <T> withDatabaseContextAndTransaction(statement: Transaction.() -> T) =
79+
withContext(Dispatchers.IO) { transaction(statement = statement) }
80+
81+
install(DefaultHeaders)
82+
83+
routing {
84+
fun selectWorldsWithIdQuery(id: Int) =
85+
WorldTable.slice(WorldTable.id, WorldTable.randomNumber).select(WorldTable.id eq id)
86+
87+
fun ResultRow.toWorld() =
88+
World(this[WorldTable.id].value, this[WorldTable.randomNumber])
89+
90+
fun ResultRow.toFortune() =
91+
Fortune(this[FortuneTable.id].value, this[FortuneTable.message])
92+
93+
fun ThreadLocalRandom.nextIntWithinRows() =
94+
nextInt(dbRows) + 1
95+
96+
fun selectSingleWorld(random: ThreadLocalRandom): World =
97+
selectWorldsWithIdQuery(random.nextIntWithinRows()).single().toWorld()
98+
99+
fun selectWorlds(queries: Int, random: ThreadLocalRandom): List<World> =
100+
List(queries) { selectSingleWorld(random) }
101+
102+
get("/db") {
103+
val random = ThreadLocalRandom.current()
104+
val result = withDatabaseContextAndTransaction {
105+
when (exposedMode) {
106+
Dsl -> selectSingleWorld(random)
107+
Dao -> WorldDao[random.nextIntWithinRows()].toWorld()
108+
}
109+
}
110+
call.respondText(Json.encodeToString(result), ContentType.Application.Json)
111+
}
112+
113+
114+
get("/queries") {
115+
val queries = call.queries()
116+
val random = ThreadLocalRandom.current()
117+
118+
val result = withDatabaseContextAndTransaction {
119+
when (exposedMode) {
120+
Dsl -> selectWorlds(queries, random)
121+
Dao -> //List(queries) { WorldDao[random.nextIntWithinRows()].toWorld() }
122+
throw IllegalArgumentException("DAO not supported because it appears to cache results")
123+
}
124+
}
125+
126+
call.respondText(Json.encodeToString(result), ContentType.Application.Json)
127+
}
128+
129+
get("/fortunes") {
130+
val result = withDatabaseContextAndTransaction {
131+
when (exposedMode) {
132+
Dsl -> FortuneTable.slice(FortuneTable.id, FortuneTable.message).selectAll()
133+
.asSequence().map { it.toFortune() }
134+
135+
Dao -> FortuneDao.all().asSequence().map { it.toFortune() }
136+
}.toMutableList()
137+
}
138+
139+
result.add(Fortune(0, "Additional fortune added at request time."))
140+
result.sortBy { it.message }
141+
call.respondHtml {
142+
head { title { +"Fortunes" } }
143+
body {
144+
table {
145+
tr {
146+
th { +"id" }
147+
th { +"message" }
148+
}
149+
for (fortune in result) {
150+
tr {
151+
td { +fortune.id.toString() }
152+
td { +fortune.message }
153+
}
154+
}
155+
}
156+
}
157+
}
158+
}
159+
160+
get("/updates") {
161+
val queries = call.queries()
162+
val random = ThreadLocalRandom.current()
163+
lateinit var result: List<World>
164+
165+
withDatabaseContextAndTransaction {
166+
when (exposedMode) {
167+
Dsl -> {
168+
result = selectWorlds(queries, random)
169+
result.forEach { it.randomNumber = random.nextInt(dbRows) + 1 }
170+
result
171+
// sort the data to avoid data race because all updates are in one transaction
172+
.sortedBy { it.id }
173+
.forEach { world ->
174+
WorldTable.update({ WorldTable.id eq world.id }) {
175+
it[randomNumber] = world.randomNumber
176+
}
177+
/*
178+
// An alternative approach: commit every change to avoid data race
179+
commit()
180+
*/
181+
}
182+
}
183+
184+
Dao -> /*{
185+
val worldDaosAndNewRandomNumbers =
186+
List(queries) { WorldDao[random.nextIntWithinRows()] to random.nextIntWithinRows() }
187+
worldDaosAndNewRandomNumbers
188+
.sortedBy { (worldDao, _) -> worldDao.id.value }
189+
.forEach { (worldDao, newRandomNumber) ->
190+
worldDao.randomNumber = newRandomNumber
191+
}
192+
result = worldDaosAndNewRandomNumbers.map { (worldDao, _) -> worldDao.toWorld() }
193+
}*/
194+
throw IllegalArgumentException("DAO not supported because it appears to cache results")
195+
}
196+
}
197+
198+
call.respondText(Json.encodeToString(result), ContentType.Application.Json)
199+
}
200+
}
201+
}
202+
203+
fun HikariConfig.configurePostgres(poolSize: Int) {
204+
jdbcUrl = "jdbc:postgresql://tfb-database/hello_world?useSSL=false"
205+
driverClassName = org.postgresql.Driver::class.java.name
206+
207+
configureCommon(poolSize)
208+
}
209+
210+
fun HikariConfig.configureCommon(poolSize: Int) {
211+
username = "benchmarkdbuser"
212+
password = "benchmarkdbpass"
213+
addDataSourceProperty("cacheServerConfiguration", true)
214+
addDataSourceProperty("cachePrepStmts", "true")
215+
addDataSourceProperty("useUnbufferedInput", "false")
216+
addDataSourceProperty("prepStmtCacheSize", "4096")
217+
addDataSourceProperty("prepStmtCacheSqlLimit", "2048")
218+
connectionTimeout = 10000
219+
maximumPoolSize = poolSize
220+
minimumIdle = poolSize
221+
}
222+
223+
fun ApplicationCall.queries() =
224+
request.queryParameters["queries"]?.toIntOrNull()?.coerceIn(1, 500) ?: 1
60.2 KB
Binary file not shown.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
4+
networkTimeout=10000
5+
zipStoreBase=GRADLE_USER_HOME
6+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)