diff --git a/frameworks/ktor-ghost/Dockerfile b/frameworks/ktor-tuned/Dockerfile similarity index 59% rename from frameworks/ktor-ghost/Dockerfile rename to frameworks/ktor-tuned/Dockerfile index ff229f797..2d87b97ca 100644 --- a/frameworks/ktor-ghost/Dockerfile +++ b/frameworks/ktor-tuned/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:9.5.1-jdk21-corretto AS build +FROM gradle:9.5.1-jdk25-corretto AS build WORKDIR /app COPY build.gradle.kts settings.gradle.kts gradle.properties ./ COPY gradle/libs.versions.toml ./gradle/ @@ -6,21 +6,17 @@ RUN gradle dependencies --no-daemon -q 2>/dev/null || true COPY src ./src RUN gradle buildFatJar --no-daemon -q -FROM eclipse-temurin:21-jre +FROM eclipse-temurin:25-jre WORKDIR /app -COPY --from=build /app/build/libs/ktor-ghost-httparena.jar . -EXPOSE 8080 8443 +COPY --from=build /app/build/libs/ktor-tuned-httparena.jar . +EXPOSE 8080 8081 8082 8443 ENTRYPOINT ["java", \ "-server", \ "-XX:+UseG1GC", \ "-XX:+UseNUMA", \ "-XX:+AlwaysPreTouch", \ - "-XX:-OmitStackTraceInFastThrow", \ "-Dio.netty.transport.noNative=false", \ "-Dio.netty.buffer.checkBounds=false", \ "-Dio.netty.buffer.checkAccessible=false", \ - "-Dio.netty.allocator.maxOrder=10", \ "-Dio.netty.leakDetection.level=disabled", \ - "-Dio.netty.recycler.maxCapacityPerThread=4096", \ - "-Dio.netty.eventLoopThreads=0", \ - "-jar", "ktor-ghost-httparena.jar"] + "-jar", "ktor-tuned-httparena.jar"] diff --git a/frameworks/ktor-ghost/build.gradle.kts b/frameworks/ktor-tuned/build.gradle.kts similarity index 79% rename from frameworks/ktor-ghost/build.gradle.kts rename to frameworks/ktor-tuned/build.gradle.kts index 5e5ebd515..384ecb627 100644 --- a/frameworks/ktor-ghost/build.gradle.kts +++ b/frameworks/ktor-tuned/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.serialization) - alias(ktorLibs.plugins.ktor) + alias(libs.plugins.ktor) alias(libs.plugins.kotlin.ksp) } @@ -31,7 +31,8 @@ dependencies { implementation(libs.logback.classic) implementation(libs.postgresql) implementation(libs.hikaricp) - runtimeOnly(libs.netty.native.epoll) + runtimeOnly(variantOf(libs.netty.native.epoll) { classifier("linux-aarch_64") }) + runtimeOnly(variantOf(libs.netty.native.openssl) { classifier("linux-aarch_64") }) testImplementation(kotlin("test")) testImplementation(ktorLibs.server.testHost) @@ -39,10 +40,10 @@ dependencies { ktor { fatJar { - archiveFileName.set("ktor-ghost-httparena.jar") + archiveFileName.set("ktor-tuned-httparena.jar") } } kotlin { - jvmToolchain(21) + jvmToolchain(25) } diff --git a/frameworks/ktor-ghost/gradle.properties b/frameworks/ktor-tuned/gradle.properties similarity index 100% rename from frameworks/ktor-ghost/gradle.properties rename to frameworks/ktor-tuned/gradle.properties diff --git a/frameworks/ktor-ghost/gradle/libs.versions.toml b/frameworks/ktor-tuned/gradle/libs.versions.toml similarity index 74% rename from frameworks/ktor-ghost/gradle/libs.versions.toml rename to frameworks/ktor-tuned/gradle/libs.versions.toml index 60f7efac4..7a71f7b80 100644 --- a/frameworks/ktor-ghost/gradle/libs.versions.toml +++ b/frameworks/ktor-tuned/gradle/libs.versions.toml @@ -1,14 +1,16 @@ [versions] -exposed = "1.2.0" -kotlin = "2.3.21" +exposed = "1.5.0" +kotlin = "2.4.20" +ghost = "1.3.1" [libraries] -logback-classic = { module = "ch.qos.logback:logback-classic", version = "1.5.15" } -netty-native-epoll = { module = "io.netty:netty-transport-native-epoll", version = "4.2.14.Final" } +logback-classic = { module = "ch.qos.logback:logback-classic", version = "1.5.38" } +netty-native-epoll = { module = "io.netty:netty-transport-native-epoll", version = "4.2.16.Final" } +netty-native-openssl = { module = "io.netty:netty-tcnative-boringssl-static", version = "2.0.81.Final" } # Ghost Serializer -ghost-serialization = { module = "com.ghostserializer:ghost-serialization", version = "1.2.2" } -ghost-compiler = { module = "com.ghostserializer:ghost-compiler", version = "1.2.2" } +ghost-serialization = { module = "com.ghostserializer:ghost-serialization", version.ref = "ghost" } +ghost-compiler = { module = "com.ghostserializer:ghost-compiler", version.ref = "ghost" } # Database exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } @@ -18,6 +20,7 @@ postgresql = { module = "org.postgresql:postgresql", version = "42.7.4" } hikaricp = { module = "com.zaxxer:HikariCP", version = "6.2.1" } [plugins] +ktor = { id = "io.ktor.plugin", version = "3.5.2" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } kotlin-ksp = { id = "com.google.devtools.ksp", version = "2.3.9" } diff --git a/frameworks/ktor-ghost/gradle/wrapper/gradle-wrapper.jar b/frameworks/ktor-tuned/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from frameworks/ktor-ghost/gradle/wrapper/gradle-wrapper.jar rename to frameworks/ktor-tuned/gradle/wrapper/gradle-wrapper.jar diff --git a/frameworks/ktor-ghost/gradle/wrapper/gradle-wrapper.properties b/frameworks/ktor-tuned/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from frameworks/ktor-ghost/gradle/wrapper/gradle-wrapper.properties rename to frameworks/ktor-tuned/gradle/wrapper/gradle-wrapper.properties diff --git a/frameworks/ktor-ghost/gradlew b/frameworks/ktor-tuned/gradlew similarity index 100% rename from frameworks/ktor-ghost/gradlew rename to frameworks/ktor-tuned/gradlew diff --git a/frameworks/ktor-ghost/gradlew.bat b/frameworks/ktor-tuned/gradlew.bat similarity index 100% rename from frameworks/ktor-ghost/gradlew.bat rename to frameworks/ktor-tuned/gradlew.bat diff --git a/frameworks/ktor-ghost/meta.json b/frameworks/ktor-tuned/meta.json similarity index 80% rename from frameworks/ktor-ghost/meta.json rename to frameworks/ktor-tuned/meta.json index 352e86f00..480b18e00 100644 --- a/frameworks/ktor-ghost/meta.json +++ b/frameworks/ktor-tuned/meta.json @@ -1,5 +1,5 @@ { - "display_name": "ktor-ghost", + "display_name": "ktor-tuned", "language": "Kotlin", "type": "flagship", "mode": "tuned", @@ -10,12 +10,13 @@ "response": true }, "engine": "netty", - "description": "JetBrains Ktor 3.x on Netty with Kotlin coroutines, Ghost Serializer, JDK 21.", + "description": "JetBrains Ktor 3.x on Netty with Ghost Serializer, JDK 25.", "repo": "https://github.com/ktorio/ktor", "enabled": true, "tests": [ "baseline", "async", "latency-1m", "latency-10k", "latency-500k-8cpu", "limited-conn", + "async", "pipelined", "json-comp", "json-tls", @@ -26,6 +27,8 @@ "static-h2", "baseline-h2c", "json-h2c", + "baseline-h3", + "static-h3", "echo-ws", "echo-ws-pipeline"], "maintainers": [ diff --git a/frameworks/ktor-ghost/settings.gradle.kts b/frameworks/ktor-tuned/settings.gradle.kts similarity index 85% rename from frameworks/ktor-ghost/settings.gradle.kts rename to frameworks/ktor-tuned/settings.gradle.kts index a98edff26..31793be2c 100644 --- a/frameworks/ktor-ghost/settings.gradle.kts +++ b/frameworks/ktor-tuned/settings.gradle.kts @@ -10,8 +10,8 @@ dependencyResolutionManagement { mavenCentral() } versionCatalogs { - create("ktorLibs").from("io.ktor:ktor-version-catalog:3.5.0") + create("ktorLibs").from("io.ktor:ktor-version-catalog:3.6.0") } } -rootProject.name = "ktor-ghost-httparena" +rootProject.name = "ktor-tuned-httparena" diff --git a/frameworks/ktor-ghost/src/main/kotlin/com/httparena/Application.kt b/frameworks/ktor-tuned/src/main/kotlin/com/httparena/Application.kt similarity index 89% rename from frameworks/ktor-ghost/src/main/kotlin/com/httparena/Application.kt rename to frameworks/ktor-tuned/src/main/kotlin/com/httparena/Application.kt index 07159edb8..c02d97d00 100644 --- a/frameworks/ktor-ghost/src/main/kotlin/com/httparena/Application.kt +++ b/frameworks/ktor-tuned/src/main/kotlin/com/httparena/Application.kt @@ -35,21 +35,34 @@ import org.jetbrains.exposed.v1.jdbc.upsert import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File +import kotlin.time.Duration.Companion.milliseconds fun main() { Ghost.prewarm() - println("Ktor HttpArena server starting on :8080 (HTTP/1.1) and :8443 (HTTPS/HTTP+2)") + println("Ktor HttpArena server starting on :8080 (HTTP/1.1), :8081 (JSON + TLS), :8082 (H2C), :8443 (HTTPS/HTTP2/HTTP3)") val deps = ArenaApplicationDepsFactory.load() val environment = applicationEnvironment {} + @OptIn(ExperimentalKtorApi::class) val server = embeddedServer(Netty, environment, { - enableHttp2 = true + shareWorkGroup = true + enableH2c = true + // HTTP/1.1 connector { port = 8080 host = "0.0.0.0" } + // h2c + connector { + port = 8082 + host = "0.0.0.0" + } deps.keyStore?.let { keyStore -> + enableHttp2 = true + enableHttp3() + + // JSON + TLS sslConnector( keyStore = keyStore, keyAlias = KEY_ALIAS, @@ -59,6 +72,7 @@ fun main() { port = 8081 host = "0.0.0.0" } + // HTTP/2 HTTP/3 sslConnector( keyStore = keyStore, keyAlias = KEY_ALIAS, @@ -73,31 +87,6 @@ fun main() { mainModule(deps) } - // Spin up a second server for H2C - embeddedServer(Netty, environment, { - enableH2c = true - - connector { - port = 8082 - host = "0.0.0.0" - } - }) { - // Reject any non-HTTP/2 request hitting the H2C connector - intercept(ApplicationCallPipeline.Plugins) { - val version = call.request.httpVersion - if (!version.startsWith("HTTP/2")) { - call.response.headers.append(HttpHeaders.Upgrade, "h2c") - call.response.headers.append(HttpHeaders.Connection, "Upgrade") - call.respond(HttpStatusCode.UpgradeRequired, "HTTP/2 (h2c) required") - finish() - return@intercept - } - } - // Import the same endpoints for this server - mainModule(deps) - - }.start(wait = false) - server.start(wait = true) } @@ -151,6 +140,18 @@ private fun Application.configureRouting(appData: ArenaApplicationDeps) { call.respondNumber(ms) } + /** + * Async delay + * https://www.http-arena.com/docs/test-profiles/h1/isolated/async/ + */ + get("/delay/{ms}") { + val ms = call.parameters["ms"]!!.toLong() + // delay() suspends the coroutine rather than parking the thread it runs on, so the + // waits in flight are bounded by memory and not by the size of the event-loop group. + if (ms > 0) delay(ms) + call.respondNumber(ms) + } + /** * Baseline 1.1 * https://www.http-arena.com/docs/test-profiles/h1/isolated/baseline/ @@ -199,13 +200,15 @@ private fun Application.configureRouting(appData: ArenaApplicationDeps) { if (count < 0) count = 0 if (count > appData.dataset.size) count = appData.dataset.size val m = call.request.queryParameters["m"]?.toIntOrNull() ?: 1 - val processed = appData.dataset.take(count).map { d -> - ProcessedItem( - id = d.id, name = d.name, category = d.category, - price = d.price, quantity = d.quantity, active = d.active, - tags = d.tags, rating = d.rating, - total = d.price.toLong() * d.quantity * m - ) + val processed = buildList(count) { + for (i in 0.. ProcessedItem( + id = d.id, name = d.name, category = d.category, + price = d.price, quantity = d.quantity, active = d.active, + tags = d.tags, rating = d.rating, + total = d.price.toLong() * d.quantity * m + )}) + } } call.respondGhost(JsonResponse(items = processed, count = count)) } @@ -254,7 +257,7 @@ private fun Application.configureRouting(appData: ArenaApplicationDeps) { * Static files * https://www.http-arena.com/docs/test-profiles/h1/isolated/static/ */ - staticFiles("/static", File("/data/static")) { + staticFiles("/static", dir = File("/data/static"), index = null) { preCompressed(CompressedFileType.BROTLI, CompressedFileType.GZIP) } @@ -314,6 +317,16 @@ private fun Application.configureRouting(appData: ArenaApplicationDeps) { } } + /** + * Async Delay benchmark. Measures what a framework does with a request it cannot answer yet. + * https://www.http-arena.com/docs/test-profiles/h1/isolated/async/implementation/ + */ + get("/delay/{ms}") { + val delayParam = call.parameters["ms"] ?: "0" + delay(delayParam.toLong().milliseconds) + call.respond(TextContent(delayParam, ContentType.Text.Plain)) + } + } } diff --git a/frameworks/ktor-ghost/src/main/kotlin/com/httparena/DBSchema.kt b/frameworks/ktor-tuned/src/main/kotlin/com/httparena/DBSchema.kt similarity index 100% rename from frameworks/ktor-ghost/src/main/kotlin/com/httparena/DBSchema.kt rename to frameworks/ktor-tuned/src/main/kotlin/com/httparena/DBSchema.kt diff --git a/frameworks/ktor-ghost/src/main/kotlin/com/httparena/DataTypes.kt b/frameworks/ktor-tuned/src/main/kotlin/com/httparena/DataTypes.kt similarity index 100% rename from frameworks/ktor-ghost/src/main/kotlin/com/httparena/DataTypes.kt rename to frameworks/ktor-tuned/src/main/kotlin/com/httparena/DataTypes.kt diff --git a/frameworks/ktor-ghost/src/main/kotlin/com/httparena/Utils.kt b/frameworks/ktor-tuned/src/main/kotlin/com/httparena/Utils.kt similarity index 100% rename from frameworks/ktor-ghost/src/main/kotlin/com/httparena/Utils.kt rename to frameworks/ktor-tuned/src/main/kotlin/com/httparena/Utils.kt diff --git a/frameworks/ktor-ghost/src/main/resources/logback.xml b/frameworks/ktor-tuned/src/main/resources/logback.xml similarity index 100% rename from frameworks/ktor-ghost/src/main/resources/logback.xml rename to frameworks/ktor-tuned/src/main/resources/logback.xml diff --git a/frameworks/ktor-ghost/src/test/kotlin/com/httparena/ApplicationTest.kt b/frameworks/ktor-tuned/src/test/kotlin/com/httparena/ApplicationTest.kt similarity index 97% rename from frameworks/ktor-ghost/src/test/kotlin/com/httparena/ApplicationTest.kt rename to frameworks/ktor-tuned/src/test/kotlin/com/httparena/ApplicationTest.kt index 19acf06a3..45b4bfc32 100644 --- a/frameworks/ktor-ghost/src/test/kotlin/com/httparena/ApplicationTest.kt +++ b/frameworks/ktor-tuned/src/test/kotlin/com/httparena/ApplicationTest.kt @@ -259,4 +259,12 @@ class ApplicationTest { assertEquals(HttpStatusCode.NotFound.value, response.status.value) } + @Test + fun delayTest() = testApplication { + setup() + val response = client.get("/delay/1000") + assertEquals(HttpStatusCode.OK.value, response.status.value) + assertEquals("1000", response.bodyAsText()) + } + } diff --git a/frameworks/ktor-ghost/src/test/resources/baseline11.http b/frameworks/ktor-tuned/src/test/resources/baseline11.http similarity index 100% rename from frameworks/ktor-ghost/src/test/resources/baseline11.http rename to frameworks/ktor-tuned/src/test/resources/baseline11.http diff --git a/frameworks/ktor-ghost/src/test/resources/baseline11post.http b/frameworks/ktor-tuned/src/test/resources/baseline11post.http similarity index 100% rename from frameworks/ktor-ghost/src/test/resources/baseline11post.http rename to frameworks/ktor-tuned/src/test/resources/baseline11post.http diff --git a/frameworks/ktor/Dockerfile b/frameworks/ktor/Dockerfile index cdceee311..d50890f82 100644 --- a/frameworks/ktor/Dockerfile +++ b/frameworks/ktor/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:9.5.1-jdk21-corretto AS build +FROM gradle:9.5.1-jdk25-corretto AS build WORKDIR /app COPY build.gradle.kts settings.gradle.kts gradle.properties ./ COPY gradle/libs.versions.toml ./gradle/ @@ -6,21 +6,17 @@ RUN gradle dependencies --no-daemon -q 2>/dev/null || true COPY src ./src RUN gradle buildFatJar --no-daemon -q -FROM eclipse-temurin:21-jre +FROM eclipse-temurin:25-jre WORKDIR /app COPY --from=build /app/build/libs/ktor-httparena.jar . -EXPOSE 8080 8443 +EXPOSE 8080 8081 8082 8443 ENTRYPOINT ["java", \ "-server", \ "-XX:+UseG1GC", \ "-XX:+UseNUMA", \ "-XX:+AlwaysPreTouch", \ - "-XX:-OmitStackTraceInFastThrow", \ "-Dio.netty.transport.noNative=false", \ "-Dio.netty.buffer.checkBounds=false", \ "-Dio.netty.buffer.checkAccessible=false", \ - "-Dio.netty.allocator.maxOrder=10", \ "-Dio.netty.leakDetection.level=disabled", \ - "-Dio.netty.recycler.maxCapacityPerThread=4096", \ - "-Dio.netty.eventLoopThreads=0", \ "-jar", "ktor-httparena.jar"] diff --git a/frameworks/ktor/build.gradle.kts b/frameworks/ktor/build.gradle.kts index 7ce14313e..7ee315b39 100644 --- a/frameworks/ktor/build.gradle.kts +++ b/frameworks/ktor/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.serialization) - alias(ktorLibs.plugins.ktor) + alias(libs.plugins.ktor) } group = "com.httparena" @@ -27,7 +27,8 @@ dependencies { implementation(libs.logback.classic) implementation(libs.postgresql) implementation(libs.hikaricp) - runtimeOnly(libs.netty.native.epoll) + runtimeOnly(variantOf(libs.netty.native.epoll) { classifier("linux-aarch_64") }) + runtimeOnly(variantOf(libs.netty.native.openssl) { classifier("linux-aarch_64") }) testImplementation(kotlin("test")) testImplementation(ktorLibs.server.testHost) @@ -40,5 +41,5 @@ ktor { } kotlin { - jvmToolchain(21) + jvmToolchain(25) } diff --git a/frameworks/ktor/gradle/libs.versions.toml b/frameworks/ktor/gradle/libs.versions.toml index 7d1c776b0..0b135bce0 100644 --- a/frameworks/ktor/gradle/libs.versions.toml +++ b/frameworks/ktor/gradle/libs.versions.toml @@ -1,10 +1,11 @@ [versions] -exposed = "1.2.0" -kotlin = "2.3.21" +exposed = "1.5.0" +kotlin = "2.4.20" [libraries] -logback-classic = { module = "ch.qos.logback:logback-classic", version = "1.5.15" } -netty-native-epoll = { module = "io.netty:netty-transport-native-epoll", version = "4.2.14.Final" } +logback-classic = { module = "ch.qos.logback:logback-classic", version = "1.5.38" } +netty-native-epoll = { module = "io.netty:netty-transport-native-epoll", version = "4.2.16.Final" } +netty-native-openssl = { module = "io.netty:netty-tcnative-boringssl-static", version = "2.0.81.Final" } # Database exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } @@ -14,5 +15,6 @@ postgresql = { module = "org.postgresql:postgresql", version = "42.7.4" } hikaricp = { module = "com.zaxxer:HikariCP", version = "6.2.1" } [plugins] +ktor = { id = "io.ktor.plugin", version = "3.5.2" } kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } \ No newline at end of file diff --git a/frameworks/ktor/meta.json b/frameworks/ktor/meta.json index a1ef5d221..de4af28f0 100644 --- a/frameworks/ktor/meta.json +++ b/frameworks/ktor/meta.json @@ -10,12 +10,13 @@ "response": true }, "engine": "netty", - "description": "JetBrains Ktor 3.x on Netty with Kotlin coroutines, kotlinx.serialization, JDK 21.", + "description": "JetBrains Ktor 3.x on Netty with kotlinx.serialization, JDK 25.", "repo": "https://github.com/ktorio/ktor", "enabled": true, "tests": [ "baseline", "async", "latency-1m", "latency-10k", "latency-500k-8cpu", "limited-conn", + "async", "pipelined", "json-comp", "json-tls", @@ -26,6 +27,8 @@ "static-h2", "baseline-h2c", "json-h2c", + "baseline-h3", + "static-h3", "echo-ws", "echo-ws-pipeline", "fortunes" diff --git a/frameworks/ktor/settings.gradle.kts b/frameworks/ktor/settings.gradle.kts index 04a351061..ce777eef3 100644 --- a/frameworks/ktor/settings.gradle.kts +++ b/frameworks/ktor/settings.gradle.kts @@ -10,7 +10,7 @@ dependencyResolutionManagement { mavenCentral() } versionCatalogs { - create("ktorLibs").from("io.ktor:ktor-version-catalog:3.5.0") + create("ktorLibs").from("io.ktor:ktor-version-catalog:3.6.0") } } diff --git a/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt b/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt index 0a74db35c..ab262d6c1 100644 --- a/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt +++ b/frameworks/ktor/src/main/kotlin/com/httparena/Application.kt @@ -16,9 +16,6 @@ import io.ktor.server.response.* import io.ktor.server.routing.* import io.ktor.server.websocket.* import io.ktor.utils.io.* -import io.netty.channel.ChannelOption -import io.netty.channel.WriteBufferWaterMark -import io.netty.handler.flush.FlushConsolidationHandler import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext @@ -33,20 +30,32 @@ import org.jetbrains.exposed.v1.jdbc.upsert import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File +import kotlin.time.Duration.Companion.milliseconds fun main() { - println("Ktor HttpArena server starting on :8080 (HTTP/1.1) and :8443 (HTTPS/HTTP+2)") + println("Ktor HttpArena server starting on :8080 (HTTP/1.1), :8081 (JSON + TLS), :8082 (H2C), :8443 (HTTPS/HTTP2/HTTP3)") val deps = ArenaApplicationDepsFactory.load() val environment = applicationEnvironment {} + @OptIn(ExperimentalKtorApi::class) val server = embeddedServer(Netty, environment, { - enableHttp2 = true + enableH2c = true + // HTTP/1.1 connector { port = 8080 host = "0.0.0.0" } + // h2c + connector { + port = 8082 + host = "0.0.0.0" + } deps.keyStore?.let { keyStore -> + enableHttp2 = true + enableHttp3() + + // JSON + TLS sslConnector( keyStore = keyStore, keyAlias = KEY_ALIAS, @@ -56,6 +65,7 @@ fun main() { port = 8081 host = "0.0.0.0" } + // HTTP/2 HTTP/3 sslConnector( keyStore = keyStore, keyAlias = KEY_ALIAS, @@ -70,31 +80,6 @@ fun main() { mainModule(deps) } - // Spin up a second server for H2C - embeddedServer(Netty, environment, { - enableH2c = true - - connector { - port = 8082 - host = "0.0.0.0" - } - }) { - // Reject any non-HTTP/2 request hitting the H2C connector - intercept(ApplicationCallPipeline.Plugins) { - val version = call.request.httpVersion - if (!version.startsWith("HTTP/2")) { - call.response.headers.append(HttpHeaders.Upgrade, "h2c") - call.response.headers.append(HttpHeaders.Connection, "Upgrade") - call.respond(HttpStatusCode.UpgradeRequired, "HTTP/2 (h2c) required") - finish() - return@intercept - } - } - // Import the same endpoints for this server - mainModule(deps) - - }.start(wait = false) - server.start(wait = true) } @@ -136,18 +121,6 @@ private fun Application.configureRouting(appData: ArenaApplicationDeps) { call.respond(pipelineResponse) } - /** - * Async delay - * https://www.http-arena.com/docs/test-profiles/h1/isolated/async/ - */ - get("/delay/{ms}") { - val ms = call.parameters["ms"]!!.toLong() - // delay() suspends the coroutine rather than parking the thread it runs on, so the - // waits in flight are bounded by memory and not by the size of the event-loop group. - if (ms > 0) delay(ms) - call.respondNumber(ms) - } - /** * Baseline 1.1 * https://www.http-arena.com/docs/test-profiles/h1/isolated/baseline/ @@ -199,13 +172,15 @@ private fun Application.configureRouting(appData: ArenaApplicationDeps) { if (count < 0) count = 0 if (count > appData.dataset.size) count = appData.dataset.size val m = call.request.queryParameters["m"]?.toIntOrNull() ?: 1 - val processed = appData.dataset.take(count).map { d -> - ProcessedItem( - id = d.id, name = d.name, category = d.category, - price = d.price, quantity = d.quantity, active = d.active, - tags = d.tags, rating = d.rating, - total = d.price.toLong() * d.quantity * m - ) + val processed = buildList(count) { + for (i in 0.. ProcessedItem( + id = d.id, name = d.name, category = d.category, + price = d.price, quantity = d.quantity, active = d.active, + tags = d.tags, rating = d.rating, + total = d.price.toLong() * d.quantity * m + )}) + } } call.respond(JsonResponse(items = processed, count = count)) } @@ -257,7 +232,7 @@ private fun Application.configureRouting(appData: ArenaApplicationDeps) { * Static files * https://www.http-arena.com/docs/test-profiles/h1/isolated/static/ */ - staticFiles("/static", File("/data/static")) { + staticFiles("/static", dir = File("/data/static"), index = null) { preCompressed(CompressedFileType.BROTLI, CompressedFileType.GZIP) } @@ -317,6 +292,16 @@ private fun Application.configureRouting(appData: ArenaApplicationDeps) { } } + /** + * Async Delay benchmark. Measures what a framework does with a request it cannot answer yet. + * https://www.http-arena.com/docs/test-profiles/h1/isolated/async/implementation/ + */ + get("/delay/{ms}") { + val delayParam = call.parameters["ms"] ?: "0" + delay(delayParam.toLong().milliseconds) + call.respond(TextContent(delayParam, ContentType.Text.Plain)) + } + } } diff --git a/frameworks/ktor/src/test/kotlin/com/httparena/ApplicationTest.kt b/frameworks/ktor/src/test/kotlin/com/httparena/ApplicationTest.kt index c4def2639..1b80cdd67 100644 --- a/frameworks/ktor/src/test/kotlin/com/httparena/ApplicationTest.kt +++ b/frameworks/ktor/src/test/kotlin/com/httparena/ApplicationTest.kt @@ -259,4 +259,12 @@ class ApplicationTest { assertEquals(HttpStatusCode.NotFound.value, response.status.value) } + @Test + fun delayTest() = testApplication { + setup() + val response = client.get("/delay/1000") + assertEquals(HttpStatusCode.OK.value, response.status.value) + assertEquals("1000", response.bodyAsText()) + } + } diff --git a/site/data/results/ktor-ghost.json b/site/data/results/ktor-tuned.json similarity index 93% rename from site/data/results/ktor-ghost.json rename to site/data/results/ktor-tuned.json index 31c9f136c..5812395b6 100644 --- a/site/data/results/ktor-ghost.json +++ b/site/data/results/ktor-tuned.json @@ -1,8 +1,8 @@ { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "results": { "8gbit-512": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 49249, "avg_latency": "195.9us", @@ -27,7 +27,7 @@ "p99_9_latency": "17000.0us" }, "async-32000": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 224114, "avg_latency": "141.02ms", @@ -47,7 +47,7 @@ "status_5xx": 0 }, "async-db-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 113590, "avg_latency": "8.61ms", @@ -67,7 +67,7 @@ "status_5xx": 0 }, "baseline-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 838039, "avg_latency": "4.90ms", @@ -87,7 +87,7 @@ "status_5xx": 0 }, "baseline-h2-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 495732, "avg_latency": "193.95ms", @@ -106,7 +106,7 @@ "status_5xx": 0 }, "baseline-h2-256": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 515664, "avg_latency": "46.06ms", @@ -125,7 +125,7 @@ "status_5xx": 0 }, "baseline-h2c-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 362388, "avg_latency": "291.47ms", @@ -144,7 +144,7 @@ "status_5xx": 0 }, "baseline-h2c-256": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 391103, "avg_latency": "54.35ms", @@ -163,7 +163,7 @@ "status_5xx": 0 }, "baseline-h2c-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 392445, "avg_latency": "868.48ms", @@ -182,7 +182,7 @@ "status_5xx": 0 }, "echo-ws-16384": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 676216, "avg_latency": "23.65ms", @@ -201,7 +201,7 @@ "status_5xx": 0 }, "echo-ws-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 973072, "avg_latency": "4.16ms", @@ -220,7 +220,7 @@ "status_5xx": 0 }, "echo-ws-512": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 1021563, "avg_latency": "500us", @@ -239,7 +239,7 @@ "status_5xx": 0 }, "echo-ws-pipeline-16384": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 8286836, "avg_latency": "30.72ms", @@ -258,7 +258,7 @@ "status_5xx": 0 }, "echo-ws-pipeline-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 11090871, "avg_latency": "5.89ms", @@ -277,7 +277,7 @@ "status_5xx": 0 }, "echo-ws-pipeline-512": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 11253401, "avg_latency": "727us", @@ -296,7 +296,7 @@ "status_5xx": 0 }, "fortunes-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 49991, "avg_latency": "19.90ms", @@ -315,7 +315,7 @@ "status_5xx": 0 }, "json-comp-16384": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 218199, "avg_latency": "74.08ms", @@ -335,7 +335,7 @@ "status_5xx": 0 }, "json-comp-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 217405, "avg_latency": "18.85ms", @@ -355,7 +355,7 @@ "status_5xx": 0 }, "json-h2c-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 424461, "avg_latency": "71.66ms", @@ -374,7 +374,7 @@ "status_5xx": 0 }, "json-h2c-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 389919, "avg_latency": "304.94ms", @@ -393,7 +393,7 @@ "status_5xx": 0 }, "json-tls-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 607943, "avg_latency": "8.03ms", @@ -412,7 +412,7 @@ "status_5xx": 0 }, "latency-10k-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 9981, "avg_latency": "138.8us", @@ -436,7 +436,7 @@ "p99_9_latency": "4642.0us" }, "latency-1m-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 866759, "avg_latency": "1709795.5us", @@ -460,7 +460,7 @@ "p99_9_latency": "3212288.0us" }, "latency-500k-8cpu-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 205267, "avg_latency": "5921873.3us", @@ -484,7 +484,7 @@ "p99_9_latency": "11743232.0us" }, "limited-conn-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 655894, "avg_latency": "6.20ms", @@ -504,7 +504,7 @@ "status_5xx": 0 }, "pipelined-4096": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 654297, "avg_latency": "99.47ms", @@ -523,7 +523,7 @@ "status_5xx": 0 }, "static-h2-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 184792, "avg_latency": "170.18ms", @@ -542,7 +542,7 @@ "status_5xx": 0 }, "static-h2-256": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 196780, "avg_latency": "40.90ms", @@ -561,7 +561,7 @@ "status_5xx": 0 }, "static-tls-1024": { - "framework": "ktor-ghost", + "framework": "ktor-tuned", "language": "Kotlin", "rps": 191799, "avg_latency": "8.84ms",