Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions benchmarks/multiplatform/benchmarks/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,29 @@ kotlin {
implementation(libs.kotlinx.serialization.json)
implementation(libs.kotlinx.io)
implementation(libs.kotlinx.datetime)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
}
}

val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
runtimeOnly(libs.kotlinx.coroutines.swing)
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.content.negotiation)
implementation(libs.ktor.client.java)
implementation(libs.ktor.server.cors)
}
}

val wasmJsMain by getting {
dependencies {
implementation(libs.ktor.client.js)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
}
}
}
Expand Down Expand Up @@ -142,6 +158,44 @@ tasks.register("buildD8Distribution", Zip::class.java) {
destinationDirectory.set(rootProject.layout.buildDirectory.dir("distributions"))
}

tasks.register("runBrowserAndSaveStats") {
fun printProcessOutput(inputStream: java.io.InputStream) {
Thread {
inputStream.bufferedReader().use { reader ->
reader.lines().forEach { line ->
println(line)
}
}
}.start()
}

fun runCommand(vararg command: String): Process {
return ProcessBuilder(*command).start().also {
printProcessOutput(it.inputStream)
printProcessOutput(it.errorStream)
}
}

doFirst {
var serverProcess: Process? = null
var clientProcess: Process? = null
try {
serverProcess = runCommand("./gradlew", "benchmarks:run",
"-PrunArguments=runServer=true saveStatsToJSON=true")

clientProcess = runCommand("./gradlew", "benchmarks:wasmJsBrowserProductionRun",
"-PrunArguments=$runArguments saveStatsToJSON=true")

serverProcess.waitFor()
} catch (e: Throwable) {
e.printStackTrace()
} finally {
serverProcess?.destroy()
clientProcess?.destroy()
}
}
}

tasks.withType<org.jetbrains.kotlin.gradle.targets.js.binaryen.BinaryenExec>().configureEach {
binaryenArgs.add("-g") // keep the readable names
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import benchmarks.multipleComponents.MultipleComponentsExample
import benchmarks.lazygrid.LazyGrid
import benchmarks.visualeffects.NYContent
import kotlinx.serialization.Serializable
import kotlinx.serialization.Transient
import kotlinx.serialization.json.Json
import kotlin.math.roundToInt
import kotlin.time.Duration
Expand Down Expand Up @@ -247,7 +246,9 @@ suspend fun runBenchmark(
content = content
).generateStats()
stats.prettyPrint()
saveBenchmarkStatsOnDisk(name = name, stats = stats)
if (Config.saveStats()) {
saveBenchmarkStats(name = name, stats = stats)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,24 @@ import kotlinx.io.files.Path
import kotlinx.io.files.SystemFileSystem
import kotlinx.io.readByteArray

// port for the benchmarks save server
val BENCHMARK_SERVER_PORT = 8090

private val BENCHMARKS_SAVE_DIR = "build/benchmarks"
private fun pathToCsv(name: String) = Path("$BENCHMARKS_SAVE_DIR/$name.csv")
private fun pathToJson(name: String) = Path("$BENCHMARKS_SAVE_DIR/json-reports/$name.json")

internal fun saveJson(benchmarkName: String, jsonString: String) {
val jsonPath = pathToJson(benchmarkName)
SystemFileSystem.createDirectories(jsonPath.parent!!)
SystemFileSystem.sink(jsonPath).writeText(jsonString)
println("JSON results saved to ${SystemFileSystem.resolve(jsonPath)}")
}

fun saveBenchmarkStatsOnDisk(name: String, stats: BenchmarkStats) {
try {
if (Config.saveStatsToCSV) {
val path = Path("build/benchmarks/$name.csv")
val path = pathToCsv(name)

val keyToValue = mutableMapOf<String, String>()
keyToValue.put("Date", currentFormattedDate)
Expand All @@ -40,12 +54,7 @@ fun saveBenchmarkStatsOnDisk(name: String, stats: BenchmarkStats) {
println("CSV results saved to ${SystemFileSystem.resolve(path)}")
println()
} else if (Config.saveStatsToJSON) {
val jsonString = stats.toJsonString()
val jsonPath = Path("build/benchmarks/json-reports/$name.json")

SystemFileSystem.createDirectories(jsonPath.parent!!)
SystemFileSystem.sink(jsonPath).writeText(jsonString)
println("JSON results saved to ${SystemFileSystem.resolve(jsonPath)}")
saveJson(name, stats.toJsonString())
println()
}
} catch (_: IOException) {
Expand All @@ -55,11 +64,17 @@ fun saveBenchmarkStatsOnDisk(name: String, stats: BenchmarkStats) {
}
}

/**
* Saves benchmark statistics to disk or sends them to a server.
* This is an expect function with platform-specific implementations.
*/
expect fun saveBenchmarkStats(name: String, stats: BenchmarkStats)

private fun RawSource.readText() = use {
it.buffered().readByteArray().decodeToString()
}

private fun RawSink.writeText(text: String) = use {
internal fun RawSink.writeText(text: String) = use {
it.buffered().apply {
write(text.encodeToByteArray())
flush()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ object Args {
var versionInfo: String? = null
var saveStatsToCSV: Boolean = false
var saveStatsToJSON: Boolean = false
var runServer: Boolean = false

for (arg in args) {
if (arg.startsWith("modes=", ignoreCase = true)) {
Expand All @@ -51,6 +52,8 @@ object Args {
saveStatsToJSON = arg.substringAfter("=").toBoolean()
} else if (arg.startsWith("disabledBenchmarks=", ignoreCase = true)) {
disabledBenchmarks += argToMap(arg.decodeArg()).keys
} else if (arg.startsWith("runServer=", ignoreCase = true)) {
runServer = arg.substringAfter("=").toBoolean()
}
}

Expand All @@ -60,7 +63,8 @@ object Args {
disabledBenchmarks = disabledBenchmarks,
versionInfo = versionInfo,
saveStatsToCSV = saveStatsToCSV,
saveStatsToJSON = saveStatsToJSON
saveStatsToJSON = saveStatsToJSON,
runServer = runServer,
)
}
}
Expand All @@ -83,7 +87,8 @@ data class Config(
val disabledBenchmarks: Set<String> = emptySet(),
val versionInfo: String? = null,
val saveStatsToCSV: Boolean = false,
val saveStatsToJSON: Boolean = false
val saveStatsToJSON: Boolean = false,
val runServer: Boolean = false,
) {
/**
* Checks if a specific mode is enabled based on the configuration.
Expand Down Expand Up @@ -127,6 +132,9 @@ data class Config(
val saveStatsToJSON: Boolean
get() = global.saveStatsToJSON

val runServer: Boolean
get() = global.runServer

fun setGlobal(global: Config) {
this.global = global
}
Expand All @@ -143,5 +151,7 @@ data class Config(

fun getBenchmarkProblemSize(benchmark: String, default: Int): Int =
global.getBenchmarkProblemSize(benchmark, default)
}

fun saveStats() = saveStatsToCSV || saveStatsToJSON
}
}
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
Comment thread
pjBooms marked this conversation as resolved.
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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,11 @@ import kotlinx.coroutines.runBlocking

fun main(args: Array<String>) {
Config.setGlobalFromArgs(args)
runBlocking(Dispatchers.Main) { runBenchmarks() }

if (Config.runServer) {
// Start the benchmark server to receive results from browsers
BenchmarksSaveServer.start()
Comment thread
pjBooms marked this conversation as resolved.
} else {
runBlocking(Dispatchers.Main) { runBenchmarks() }
}
}
Loading