From ae4d6efbfe4c5a5eceefe79b7ca2429bd7e258a2 Mon Sep 17 00:00:00 2001 From: Leonid Stashevsky Date: Tue, 9 Apr 2019 15:11:05 +0300 Subject: [PATCH] Client benchmarks --- build.gradle | 4 +- gradle.properties | 1 + gradle/compatibility.gradle | 3 +- .../ktor-client-benchmarks/build.gradle.kts | 52 +++++++++++ .../jvm/src/BenchmarkJvmUtils.kt | 7 ++ .../jvm/src/JvmClientBenchmarks.kt | 17 ++++ .../jvm/src/KtorClientBenchmarks.kt | 60 +++++++++++++ .../client/features/websocket/buildersCio.kt | 13 +++ .../client/engine/jetty/JettyHttpRequest.kt | 3 +- .../ktor/client/engine/okhttp/OkHttpEngine.kt | 10 +++ ktor-client/ktor-client-tests/build.gradle | 18 ++-- .../tests/utils/ClientBenchmarkServer.kt | 76 ++++++++++++++++ .../client/tests/utils/ClientTestServer.kt | 88 +++++++++++++++++++ .../io/ktor/client/tests/utils/TestServer.kt | 81 +---------------- settings.gradle | 7 ++ 15 files changed, 350 insertions(+), 90 deletions(-) create mode 100644 ktor-client/ktor-client-benchmarks/build.gradle.kts create mode 100644 ktor-client/ktor-client-benchmarks/jvm/src/BenchmarkJvmUtils.kt create mode 100644 ktor-client/ktor-client-benchmarks/jvm/src/JvmClientBenchmarks.kt create mode 100644 ktor-client/ktor-client-benchmarks/jvm/src/KtorClientBenchmarks.kt create mode 100644 ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientBenchmarkServer.kt create mode 100644 ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientTestServer.kt diff --git a/build.gradle b/build.gradle index 28ef24a0a..099dbb8d7 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { repositories { mavenLocal() jcenter() - maven { url "https://plugins.gradle.org/m2/" } + gradlePluginPortal() maven { url "https://dl.bintray.com/jetbrains/kotlin-native-dependencies" } maven { url "https://dl.bintray.com/kotlin/kotlin-dev" } maven { url "https://dl.bintray.com/orangy/maven" } @@ -18,6 +18,7 @@ buildscript { classpath "org.jetbrains.kotlinx:atomicfu-gradle-plugin:$atomic_fu_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" classpath "me.champeau.gradle:jmh-gradle-plugin:$jmh_plugin_version" + classpath "org.jetbrains.gradle.benchmarks:benchmarks.plugin:$benchmarks_version" classpath "kotlinx.team:kotlinx.team.infra:$infra_version" } } @@ -72,6 +73,7 @@ allprojects { } maven { url "https://dl.bintray.com/kotlin/kotlin-eap" } maven { url "https://dl.bintray.com/kotlin/kotlin-dev" } + maven { url "https://dl.bintray.com/orangy/maven" } jcenter() } diff --git a/gradle.properties b/gradle.properties index c514723eb..4e2eebed0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,6 +16,7 @@ kotlin_version=1.3.31 # kotlin libraries infra_version=0.1.0-dev-42 +benchmarks_version=0.1.7-dev-17 kotlinx_io_version=0.1.8 serialization_version=0.11.0 diff --git a/gradle/compatibility.gradle b/gradle/compatibility.gradle index fb963b921..fa858897b 100644 --- a/gradle/compatibility.gradle +++ b/gradle/compatibility.gradle @@ -7,7 +7,8 @@ dependencies { 'ktor-client-curl', 'ktor-client-ios', 'ktor-client', - 'ktor-client-features' + 'ktor-client-features', + 'ktor-client-benchmarks' ].toSet() def projects = [].toSet() diff --git a/ktor-client/ktor-client-benchmarks/build.gradle.kts b/ktor-client/ktor-client-benchmarks/build.gradle.kts new file mode 100644 index 000000000..70db218b0 --- /dev/null +++ b/ktor-client/ktor-client-benchmarks/build.gradle.kts @@ -0,0 +1,52 @@ +import org.jetbrains.gradle.benchmarks.* + +plugins { + id("org.jetbrains.gradle.benchmarks.plugin") + id("kotlin-allopen") + id("kotlinx-atomicfu") +} + +allOpen { + annotation("org.openjdk.jmh.annotations.State") +} + +val jmh_version by extra.properties +val benchmarks_version by extra.properties + +kotlin { + sourceSets { + val commonMain by getting { + dependencies { + implementation(project(":ktor-client:ktor-client-core")) + implementation("org.jetbrains.gradle.benchmarks:runtime:$benchmarks_version") + } + } + val jvmMain by getting { + dependencies { + implementation(project(":ktor-client:ktor-client-cio")) + implementation(project(":ktor-client:ktor-client-apache")) + implementation(project(":ktor-client:ktor-client-android")) + implementation(project(":ktor-client:ktor-client-okhttp")) + implementation(project(":ktor-client:ktor-client-jetty")) + } + } + } +} + +benchmark { + configurations { + (register("jvm") as? JvmBenchmarkConfiguration)?.apply { + jmhVersion = "$jmh_version" + } + } + + defaults.apply { + iterationTime = 100 + iterations = 3 + } +} + +/** + * Run benchmarks: + * ./gradlew :ktor-client:ktor-client-benchmarks:benchmark + **/ diff --git a/ktor-client/ktor-client-benchmarks/jvm/src/BenchmarkJvmUtils.kt b/ktor-client/ktor-client-benchmarks/jvm/src/BenchmarkJvmUtils.kt new file mode 100644 index 000000000..ec2583b5d --- /dev/null +++ b/ktor-client/ktor-client-benchmarks/jvm/src/BenchmarkJvmUtils.kt @@ -0,0 +1,7 @@ +package io.ktor.client.benchmarks + +import kotlinx.coroutines.* + +internal fun runBenchmark(block: suspend CoroutineScope.() -> T): Unit = runBlocking { + block() +} diff --git a/ktor-client/ktor-client-benchmarks/jvm/src/JvmClientBenchmarks.kt b/ktor-client/ktor-client-benchmarks/jvm/src/JvmClientBenchmarks.kt new file mode 100644 index 000000000..21cc6283d --- /dev/null +++ b/ktor-client/ktor-client-benchmarks/jvm/src/JvmClientBenchmarks.kt @@ -0,0 +1,17 @@ +package io.ktor.client.benchmarks + +import io.ktor.client.engine.android.* +import io.ktor.client.engine.apache.* +import io.ktor.client.engine.cio.* +import io.ktor.client.engine.jetty.* +import io.ktor.client.engine.okhttp.* + +internal class ApacheClientBenchmarks : KtorClientBenchmarks(Apache) + +internal class OkHttpClientBenchmarks : KtorClientBenchmarks(OkHttp) + +internal class AndroidClientBenchmarks : KtorClientBenchmarks(Android) + +internal class CIOClientBenchmarks : KtorClientBenchmarks(CIO) + +internal class JettyClientBenchmarks : KtorClientBenchmarks(Jetty) diff --git a/ktor-client/ktor-client-benchmarks/jvm/src/KtorClientBenchmarks.kt b/ktor-client/ktor-client-benchmarks/jvm/src/KtorClientBenchmarks.kt new file mode 100644 index 000000000..f4d85047b --- /dev/null +++ b/ktor-client/ktor-client-benchmarks/jvm/src/KtorClientBenchmarks.kt @@ -0,0 +1,60 @@ +package io.ktor.client.benchmarks + +import io.ktor.client.* +import io.ktor.client.engine.* +import io.ktor.client.request.* +import org.jetbrains.gradle.benchmarks.* + +internal const val TEST_BENCHMARKS_SERVER = "http://127.0.0.1:8080/benchmarks" + +@State(Scope.Benchmark) +internal abstract class KtorClientBenchmarks( + private val factory: HttpClientEngineFactory<*> +) { + lateinit var client: HttpClient + + @Setup + fun start() { + client = HttpClient(factory) + } + + @Benchmark + fun download1K() = runBenchmark { + client.download(1) + } + + @Benchmark + fun download16K() = runBenchmark { + client.download(16) + } + + @Benchmark + fun download32K() = runBenchmark { + client.download(32) + } + + @Benchmark + fun download64K() = runBenchmark { + client.download(64) + } + + @Benchmark + fun download256K() = runBenchmark { + client.download(256) + } + + @Benchmark + fun download1024K() = runBenchmark { + client.download(1024) + } + + @TearDown + fun stop() { + client.close() + } +} + +internal suspend inline fun HttpClient.download(size: Int) { + val data = get("$TEST_BENCHMARKS_SERVER/bytes?size=$size") + check(data.size == size * 1024) +} diff --git a/ktor-client/ktor-client-cio/jvm/src/io/ktor/client/features/websocket/buildersCio.kt b/ktor-client/ktor-client-cio/jvm/src/io/ktor/client/features/websocket/buildersCio.kt index 8d3f9ea05..7bd299292 100644 --- a/ktor-client/ktor-client-cio/jvm/src/io/ktor/client/features/websocket/buildersCio.kt +++ b/ktor-client/ktor-client-cio/jvm/src/io/ktor/client/features/websocket/buildersCio.kt @@ -47,6 +47,19 @@ suspend fun HttpClient.wsRaw( request: HttpRequestBuilder.() -> Unit = {}, block: suspend ClientWebSocketSession.() -> Unit ): Unit = webSocketRaw(method, host, port, path, request, block) + +/** + * Open [DefaultClientWebSocketSession]. + */ +suspend fun HttpClient.ws( + urlString: String, + request: HttpRequestBuilder.() -> Unit = {}, + block: suspend DefaultClientWebSocketSession.() -> Unit +): Unit { + val url = Url(urlString) + webSocket(HttpMethod.Get, url.host, url.port, url.encodedPath, request, block) +} + /** * Create secure raw [ClientWebSocketSession]: no ping-pong and other service messages are used. */ diff --git a/ktor-client/ktor-client-jetty/jvm/src/io/ktor/client/engine/jetty/JettyHttpRequest.kt b/ktor-client/ktor-client-jetty/jvm/src/io/ktor/client/engine/jetty/JettyHttpRequest.kt index 23e5ea141..6133ff8da 100644 --- a/ktor-client/ktor-client-jetty/jvm/src/io/ktor/client/engine/jetty/JettyHttpRequest.kt +++ b/ktor-client/ktor-client-jetty/jvm/src/io/ktor/client/engine/jetty/JettyHttpRequest.kt @@ -53,7 +53,8 @@ internal suspend fun HttpRequestData.executeRequest( internal suspend fun HTTP2Client.connect( url: Url, config: JettyEngineConfig ): Session = withPromise { promise -> - connect(config.sslContextFactory, InetSocketAddress(url.host, url.port), Session.Listener.Adapter(), promise) + val factory = if (url.protocol.isSecure()) config.sslContextFactory else null + connect(factory, InetSocketAddress(url.host, url.port), Session.Listener.Adapter(), promise) } private fun HttpRequestData.prepareHeadersFrame(): HeadersFrame { diff --git a/ktor-client/ktor-client-okhttp/jvm/src/io/ktor/client/engine/okhttp/OkHttpEngine.kt b/ktor-client/ktor-client-okhttp/jvm/src/io/ktor/client/engine/okhttp/OkHttpEngine.kt index a6fd89afc..2a3cd2d5e 100644 --- a/ktor-client/ktor-client-okhttp/jvm/src/io/ktor/client/engine/okhttp/OkHttpEngine.kt +++ b/ktor-client/ktor-client-okhttp/jvm/src/io/ktor/client/engine/okhttp/OkHttpEngine.kt @@ -36,6 +36,16 @@ class OkHttpEngine( } } + override fun close() { + super.close() + + coroutineContext[Job]?.invokeOnCompletion { + engine.dispatcher().executorService().shutdown() + engine.connectionPool().evictAll() + engine.cache()?.close() + } + } + private suspend fun executeWebSocketRequest( engineRequest: Request, callContext: CoroutineContext diff --git a/ktor-client/ktor-client-tests/build.gradle b/ktor-client/ktor-client-tests/build.gradle index e8b8ba871..d140929d3 100644 --- a/ktor-client/ktor-client-tests/build.gradle +++ b/ktor-client/ktor-client-tests/build.gradle @@ -74,15 +74,17 @@ task startTestServer(type: KtorTestServer, dependsOn: assemble) { classpath = kotlin.targets.jvm.compilations["test"].runtimeDependencyFiles } -if (!project.ext.ideaActive) { - def testTasks = [ - 'macosX64Test', 'linuxX64Test', 'iosTest', 'mingwX64Test', 'jvmTest', 'jsTestMochaChrome', 'jsTestMochaNode' - ] +def testTasks = [ + 'jvmTest', 'jvmBenchmark' +] - rootProject.allprojects { - it.tasks.matching { it.name in testTasks }*.configure { testTask -> - testTask.dependsOn startTestServer - } +if (!project.ext.ideaActive) { + testTasks += ['macosX64Test', 'linuxX64Test', 'iosTest', 'mingwX64Test', 'jsTestMochaChrome', 'jsTestMochaNode'] +} + +rootProject.allprojects { + it.tasks.matching { it.name in testTasks }*.configure { testTask -> + testTask.dependsOn startTestServer } } diff --git a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientBenchmarkServer.kt b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientBenchmarkServer.kt new file mode 100644 index 000000000..c4a69c750 --- /dev/null +++ b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientBenchmarkServer.kt @@ -0,0 +1,76 @@ +package io.ktor.client.tests.utils + +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.http.cio.websocket.* +import io.ktor.http.content.* +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.websocket.* +import kotlinx.coroutines.io.* + +internal fun Application.benchmarks() { + routing { + route("/benchmarks") { + val testBytes = makeArray(1024 * 1024) + val testData = "{'message': 'Hello World'}" + + /** + * Receive json data-class. + */ + get("/json") { + call.respondText(testData, ContentType.Application.Json) + } + + /** + * Send json data-class. + */ + post("/json") { + val request = call.receiveText() + check(testData == request) + call.respond(HttpStatusCode.OK, "OK") + } + + /** + * Submit url form. + */ + get("/form-url") { + } + + /** + * Submit body form. + */ + post("/form-body") { + } + + /** + * Download file. + */ + get("/bytes") { + val size = call.request.queryParameters["size"]!!.toInt() + call.respond(object : OutgoingContent.WriteChannelContent() { + override suspend fun writeTo(channel: ByteWriteChannel) { + channel.writeFully(testBytes, 0, size * 1024) + } + }) + } + + /** + * Upload file + */ + post("/bytes") {} + + route("/websockets") { + webSocket("/get/{count}") { + println("connected") + val count = call.parameters["count"]!!.toInt() + + repeat(count) { + send("$it") + } + } + } + } + } +} diff --git a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientTestServer.kt b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientTestServer.kt new file mode 100644 index 000000000..eb46073f4 --- /dev/null +++ b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/ClientTestServer.kt @@ -0,0 +1,88 @@ +package io.ktor.client.tests.utils + +import io.ktor.application.* +import io.ktor.auth.* +import io.ktor.features.* +import io.ktor.http.* +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.websocket.* + +fun Application.tests() { + install(WebSockets) + + install(Authentication) { + basic("test-basic") { + realm = "my-server" + validate { call -> + if (call.name == "user1" && call.password == "Password1") + UserIdPrincipal("user1") + else null + } + } + } + + routing { + post("/echo") { + val response = call.receiveText() + call.respond(response) + } + get("/bytes") { + val size = call.request.queryParameters["size"]!!.toInt() + call.respondBytes(makeArray(size)) + } + route("/json") { + get("/users") { + call.respondText("[{'id': 42, 'login': 'TestLogin'}]", contentType = ContentType.Application.Json) + } + get("/photos") { + call.respondText("[{'id': 4242, 'path': 'cat.jpg'}]", contentType = ContentType.Application.Json) + } + } + route("/compression") { + route("/deflate") { + install(Compression) { deflate() } + setCompressionEndpoints() + } + route("/gzip") { + install(Compression) { gzip() } + setCompressionEndpoints() + } + route("/identity") { + install(Compression) { identity() } + setCompressionEndpoints() + } + } + + route("/auth") { + route("/basic") { + authenticate("test-basic") { + post { + val requestData = call.receiveText() + if (requestData == "{\"test\":\"text\"}") + call.respondText("OK") + else + call.respond(HttpStatusCode.BadRequest) + } + route("/ws") { + route("/echo") { + webSocket(protocol = "ocpp2.0,ocpp1.6") { + for (message in incoming) { + send(message) + } + } + } + } + } + } + } + } +} + +private fun Route.setCompressionEndpoints() { + get { + call.respondText("Compressed response!") + } +} + diff --git a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/TestServer.kt b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/TestServer.kt index ff9165d2a..ea54bdc71 100644 --- a/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/TestServer.kt +++ b/ktor-client/ktor-client-tests/jvm/src/io/ktor/client/tests/utils/TestServer.kt @@ -1,16 +1,8 @@ package io.ktor.client.tests.utils import ch.qos.logback.classic.* -import io.ktor.application.* -import io.ktor.auth.* -import io.ktor.features.* -import io.ktor.http.* -import io.ktor.request.* -import io.ktor.response.* -import io.ktor.routing.* import io.ktor.server.engine.* import io.ktor.server.jetty.* -import io.ktor.websocket.* import org.slf4j.* private val DEFAULT_PORT: Int = 8080 @@ -20,80 +12,11 @@ internal fun startServer(): ApplicationEngine { logger.level = Level.WARN return embeddedServer(Jetty, DEFAULT_PORT) { - install(WebSockets) - install(Authentication) { - basic("test-basic") { - realm = "my-server" - validate { call -> - if (call.name == "user1" && call.password == "Password1") - UserIdPrincipal("user1") - else null - } - } - } - routing { - post("/echo") { - val response = call.receiveText() - call.respond(response) - } - get("/bytes") { - val size = call.request.queryParameters["size"]!!.toInt() - call.respondBytes(makeArray(size)) - } - route("/json") { - get("/users") { - call.respondText("[{'id': 42, 'login': 'TestLogin'}]", contentType = ContentType.Application.Json) - } - get("/photos") { - call.respondText("[{'id': 4242, 'path': 'cat.jpg'}]", contentType = ContentType.Application.Json) - } - } - route("/compression") { - route("/deflate") { - install(Compression) { deflate() } - setCompressionEndpoints() - } - route("/gzip") { - install(Compression) { gzip() } - setCompressionEndpoints() - } - route("/identity") { - install(Compression) { identity() } - setCompressionEndpoints() - } - } - route("/auth") { - route("/basic") { - authenticate("test-basic") { - post { - val requestData = call.receiveText() - if (requestData == "{\"test\":\"text\"}") - call.respondText("OK") - else - call.respond(HttpStatusCode.BadRequest) - } - route("/ws") { - route("/echo") { - webSocket(protocol = "ocpp2.0,ocpp1.6") { - for (message in incoming) { - send(message) - } - } - } - } - } - } - } - } + tests() + benchmarks() }.start() } -private fun Route.setCompressionEndpoints() { - get { - call.respondText("Compressed response!") - } -} - /** * Start server for tests. */ diff --git a/settings.gradle b/settings.gradle index ad844e2e2..4b04f41bd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,4 +1,10 @@ pluginManagement { + repositories { + mavenCentral() + jcenter() + gradlePluginPortal() + maven { url "https://dl.bintray.com/orangy/maven" } + } resolutionStrategy { eachPlugin { if (requested.id.id == "kotlin2js") { @@ -41,6 +47,7 @@ includeEx ':ktor-client:ktor-client-jetty' includeEx ':ktor-client:ktor-client-js' includeEx ':ktor-client:ktor-client-mock' includeEx ':ktor-client:ktor-client-okhttp' +includeEx ':ktor-client:ktor-client-benchmarks' includeEx ':ktor-client:ktor-client-features' includeEx ':ktor-client:ktor-client-features:ktor-client-json' includeEx ':ktor-client:ktor-client-features:ktor-client-json:ktor-client-json-tests'