Client benchmarks

This commit is contained in:
Leonid Stashevsky
2019-04-09 15:11:05 +03:00
committed by Leonid Stashevsky
parent dfc4d6bcf3
commit ae4d6efbfe
15 changed files with 350 additions and 90 deletions

View File

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

View File

@@ -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

View File

@@ -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()

View File

@@ -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
**/

View File

@@ -0,0 +1,7 @@
package io.ktor.client.benchmarks
import kotlinx.coroutines.*
internal fun <T> runBenchmark(block: suspend CoroutineScope.() -> T): Unit = runBlocking<Unit> {
block()
}

View File

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

View File

@@ -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<ByteArray>("$TEST_BENCHMARKS_SERVER/bytes?size=$size")
check(data.size == size * 1024)
}

View File

@@ -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.
*/

View File

@@ -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 {

View File

@@ -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

View File

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

View File

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

View File

@@ -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!")
}
}

View File

@@ -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.
*/

View File

@@ -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'