d49d4a1025
Adds an opt-in load benchmark suite (`-DrunLoadBenchmark=true`)
covering: held-open WebSocket count, single + concurrent EVENT
publish throughput, and live-event fan-out latency to N
subscribers on one connection.
Numbers from a small VM (4 GiB RAM, 4 vCPU, ulimit -n 4096):
Connections (raw WS, one REQ each)
100 : 100 OK, 0.7 s
500 : 500 OK, 1.2 s
1000 : 1000 OK, 2.0 s
2000 : 1993 OK (hits the 4096-fd ceiling: 2 fds per WS)
Single-publisher serial publish + OK
10000 events, 13.2 s, 760 EPS (round-trip latency bound)
Concurrent publishers (each on its own WS)
parallel=2 : 807 EPS
parallel=4 : 1452 EPS
parallel=8 : 1989 EPS ← knee
parallel=16 : 1704 EPS ← SQLite single-writer ceiling
parallel=32 : 1778 EPS
Fanout (one publish, N active subs on a single WS)
100 subs : 67 ms last
500 subs : 52 ms last
1000 subs : 51 ms last
2000 subs : 111 ms last
Bumped SESSION_OUTGOING_BUFFER from 1024 → 8192. The 1024 cap was
hit by the 2000-sub fanout test (2000 outbound frames into one
session's queue), causing the relay to drop the connection as a
slow-consumer protection. 8192 fits the realistic upper bound for
a high-fan-out client (a few thousand subs on one WS) and caps
per-session memory at ~2 MiB before we drop.
Numbers also confirm:
- The SQLite single-writer plateau is around 2000 EPS on this
box. Production behind WAL + a faster disk should beat that.
- Each WebSocket consumes 2 file descriptors. Operators must
raise `ulimit -n` to ~3× their target connection count.
- Fan-out scales sub-linearly (2000 subs ≈ 2× the latency of
100 subs) — the bottleneck is shared (broadcast + SQLite
write), not per-sub.
Total :quartz-relay tests: 99 (4 new benchmarks, opt-in), 0 failures.
71 lines
2.0 KiB
Kotlin
71 lines
2.0 KiB
Kotlin
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
|
|
|
plugins {
|
|
alias(libs.plugins.jetbrainsKotlinJvm)
|
|
alias(libs.plugins.serialization)
|
|
application
|
|
}
|
|
|
|
application {
|
|
mainClass.set("com.vitorpamplona.quartz.relay.MainKt")
|
|
applicationName = "quartz-relay"
|
|
}
|
|
|
|
kotlin {
|
|
jvmToolchain(21)
|
|
compilerOptions {
|
|
jvmTarget.set(JvmTarget.JVM_21)
|
|
}
|
|
}
|
|
|
|
sourceSets {
|
|
main {
|
|
kotlin.srcDir("src/main/kotlin")
|
|
}
|
|
test {
|
|
kotlin.srcDir("src/test/kotlin")
|
|
}
|
|
}
|
|
|
|
tasks.withType<Test>().configureEach {
|
|
// Forward `-DrunLoadBenchmark=true` to the test JVM so the
|
|
// perf.LoadBenchmark tests opt in. Off by default — load tests
|
|
// are noisy and slow.
|
|
systemProperty("runLoadBenchmark", System.getProperty("runLoadBenchmark") ?: "false")
|
|
// Show println output from test JVM so the benchmark numbers are
|
|
// actually visible without grepping the report XML.
|
|
testLogging {
|
|
showStandardStreams =
|
|
(System.getProperty("runLoadBenchmark") == "true")
|
|
events("standard_out")
|
|
}
|
|
}
|
|
|
|
dependencies {
|
|
api(project(":quartz"))
|
|
|
|
implementation(libs.kotlinx.coroutines.core)
|
|
implementation(libs.jackson.module.kotlin)
|
|
implementation(libs.kotlinx.serialization.json)
|
|
|
|
// Bundled SQLite driver — Relay's default in-memory EventStore creates
|
|
// an in-memory DB at runtime.
|
|
implementation(libs.androidx.sqlite.bundled.jvm)
|
|
|
|
// Ktor server engine + WebSocket plugin so Relay can serve real ws://
|
|
// traffic. CIO is the coroutine-based engine — lighter than Netty.
|
|
api(libs.ktor.server.core)
|
|
api(libs.ktor.server.cio)
|
|
api(libs.ktor.server.websockets)
|
|
|
|
// TOML parsing for the operator config file. Mirrors the section
|
|
// layout of nostr-rs-relay's config.toml so existing operators can
|
|
// port their configs nearly verbatim.
|
|
implementation(libs.fourkoma)
|
|
|
|
testImplementation(libs.kotlin.test)
|
|
testImplementation(libs.kotlinx.coroutines.test)
|
|
testImplementation(libs.secp256k1.kmp.jni.jvm)
|
|
testImplementation(libs.okhttp)
|
|
}
|