Browse Source
Fixes [CMP-8282](https://youtrack.jetbrains.com/issue/CMP-8282/Benchmarks-support-saveStatsToJSON-for-web) ## Release Notes N/Arelease/1.9.0-alpha01 v1.9.0-alpha02+dev2542
12 changed files with 339 additions and 15 deletions
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
/* |
||||
* Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) = saveBenchmarkStatsOnDisk(name, stats) |
||||
@ -0,0 +1,6 @@
@@ -0,0 +1,6 @@
|
||||
/* |
||||
* Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) = saveBenchmarkStatsOnDisk(name, stats) |
||||
@ -0,0 +1,92 @@
@@ -0,0 +1,92 @@
|
||||
/* |
||||
* Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
import io.ktor.http.* |
||||
import io.ktor.serialization.kotlinx.json.* |
||||
import io.ktor.server.application.* |
||||
import io.ktor.server.engine.* |
||||
import io.ktor.server.netty.* |
||||
import io.ktor.server.plugins.contentnegotiation.* |
||||
import io.ktor.server.plugins.cors.routing.CORS |
||||
import io.ktor.server.request.* |
||||
import io.ktor.server.response.* |
||||
import io.ktor.server.routing.* |
||||
import kotlinx.coroutines.Dispatchers |
||||
import kotlinx.coroutines.withContext |
||||
import kotlinx.io.files.Path |
||||
import kotlinx.io.files.SystemFileSystem |
||||
import kotlinx.serialization.Serializable |
||||
|
||||
/** |
||||
* Data class for receiving benchmark results from client. |
||||
*/ |
||||
@Serializable |
||||
data class BenchmarkResultFromClient( |
||||
val name: String, |
||||
val stats: String // JSON string of BenchmarkStats |
||||
) |
||||
|
||||
/** |
||||
* Starts a Ktor server to receive benchmark results from browsers |
||||
* and save them to disk in the same format as the direct disk saving mechanism. |
||||
*/ |
||||
object BenchmarksSaveServer { |
||||
private var server: EmbeddedServer<NettyApplicationEngine, NettyApplicationEngine.Configuration>? = null |
||||
|
||||
fun start(port: Int = BENCHMARK_SERVER_PORT) { |
||||
if (server != null) { |
||||
println("Benchmark server is already running") |
||||
return |
||||
} |
||||
|
||||
server = embeddedServer(Netty, port = port) { |
||||
install(ContentNegotiation) { |
||||
json() |
||||
} |
||||
install(CORS) { |
||||
allowMethod(HttpMethod.Get) |
||||
allowMethod(HttpMethod.Post) |
||||
allowHeader(HttpHeaders.ContentType) |
||||
anyHost() |
||||
} |
||||
routing { |
||||
post("/benchmark") { |
||||
val result = call.receive<BenchmarkResultFromClient>() |
||||
if (result.name.isEmpty()) { |
||||
println("Stopping server! Received empty name from client") |
||||
call.respond(HttpStatusCode.OK, "Server stopped.") |
||||
stop() |
||||
return@post |
||||
} |
||||
println("Received benchmark result for: ${result.name}") |
||||
|
||||
withContext(Dispatchers.IO) { |
||||
if (Config.saveStatsToJSON) { |
||||
saveJson(result.name, result.stats) |
||||
} |
||||
|
||||
if (Config.saveStatsToCSV) { |
||||
// TODO: for CSV, we would need to convert JSON to the values |
||||
println("CSV results are not yet supported for the browser.") |
||||
} |
||||
} |
||||
|
||||
call.respond(HttpStatusCode.OK, "Benchmark result saved") |
||||
} |
||||
|
||||
get("/") { |
||||
call.respondText("Benchmark server is running", ContentType.Text.Plain) |
||||
} |
||||
} |
||||
}.start(wait = true) |
||||
} |
||||
|
||||
fun stop() { |
||||
server?.stop(1000, 2000) |
||||
server = null |
||||
println("Benchmark server stopped") |
||||
System.exit(0) |
||||
} |
||||
} |
||||
@ -0,0 +1,114 @@
@@ -0,0 +1,114 @@
|
||||
/* |
||||
* Copyright 2020-2025 JetBrains s.r.o. and respective authors and developers. |
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE.txt file. |
||||
*/ |
||||
|
||||
import io.ktor.client.HttpClient |
||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation |
||||
import io.ktor.client.request.get |
||||
import io.ktor.client.request.post |
||||
import io.ktor.client.request.setBody |
||||
import io.ktor.http.ContentType |
||||
import io.ktor.http.HttpStatusCode |
||||
import io.ktor.http.contentType |
||||
import io.ktor.serialization.kotlinx.json.json |
||||
import kotlinx.browser.window |
||||
import kotlinx.coroutines.GlobalScope |
||||
import kotlinx.coroutines.delay |
||||
import kotlinx.coroutines.launch |
||||
import kotlinx.coroutines.yield |
||||
import kotlinx.serialization.Serializable |
||||
|
||||
/** |
||||
* Browser implementation for saving benchmark results. |
||||
* Instead of trying to save directly to disk (which would fail with UnsupportedOperationException), |
||||
* this implementation sends the results to a server via HTTP. |
||||
*/ |
||||
actual fun saveBenchmarkStats(name: String, stats: BenchmarkStats) { |
||||
GlobalScope.launch { |
||||
BenchmarksSaveServerClient.sendBenchmarkResult(name, stats) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Client for sending benchmark results to the server |
||||
*/ |
||||
object BenchmarksSaveServerClient { |
||||
private val client = HttpClient { |
||||
install(ContentNegotiation) { |
||||
json() |
||||
} |
||||
} |
||||
|
||||
private var resultsSendingInProgress = 0 |
||||
|
||||
private fun serverUrlRoot(): String { |
||||
val protocol = window.location.protocol |
||||
val hostname = window.location.hostname |
||||
return "$protocol//$hostname:$BENCHMARK_SERVER_PORT" |
||||
} |
||||
|
||||
private fun serverUrl(): String { |
||||
return "${serverUrlRoot()}/benchmark" |
||||
} |
||||
|
||||
/** |
||||
* Sends benchmark results to the server |
||||
*/ |
||||
suspend fun sendBenchmarkResult(name: String, stats: BenchmarkStats) { |
||||
resultsSendingInProgress++ |
||||
println("Sending results: $name") |
||||
sendBenchmarkResult(name, stats.toJsonString()) |
||||
resultsSendingInProgress-- |
||||
println("Benchmark result sent to server: ${serverUrl()}") |
||||
} |
||||
|
||||
private suspend fun sendBenchmarkResult(name: String, stats: String) { |
||||
try { |
||||
|
||||
val result = BenchmarkResultToServer( |
||||
name = name, |
||||
stats = stats |
||||
) |
||||
|
||||
client.post(serverUrl()) { |
||||
contentType(ContentType.Application.Json) |
||||
setBody(result) |
||||
} |
||||
} catch (e: Throwable) { |
||||
println("Error sending benchmark result to server: ${e.message}") |
||||
} |
||||
} |
||||
|
||||
suspend fun stopServer() { |
||||
while (resultsSendingInProgress > 0) { |
||||
yield() |
||||
} |
||||
sendBenchmarkResult("", "") |
||||
} |
||||
|
||||
suspend fun isServerAlive(): Boolean { |
||||
// waiting for the server to start for 2 seconds |
||||
val TIMEOUT = 2000 |
||||
val DELTA = 100L |
||||
var delayed = 0L |
||||
while (delayed < TIMEOUT) { |
||||
try { |
||||
return client.get(serverUrlRoot()).status == HttpStatusCode.OK |
||||
} catch (_: Throwable) { |
||||
delayed += DELTA |
||||
delay(DELTA) |
||||
} |
||||
} |
||||
return false |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Data class for sending benchmark results to the server |
||||
*/ |
||||
@Serializable |
||||
data class BenchmarkResultToServer( |
||||
val name: String, |
||||
val stats: String // JSON string of BenchmarkStats |
||||
) |
||||
Loading…
Reference in new issue