From 1f7b48e693574455b3ed0fa75ee51900532dc5b5 Mon Sep 17 00:00:00 2001 From: zoujiaqing Date: Wed, 9 Sep 2026 05:40:05 +0800 Subject: [PATCH 01/10] neton: update to 1.0.0-beta13 (hot-path fixes, static contract, Ktor TLS fail-fast, listener readiness) --- frameworks/neton/README.md | 2 +- frameworks/neton/build.gradle.kts | 2 +- frameworks/neton/meta.json | 9 +- .../neton/src/nativeMain/kotlin/Main.kt | 86 ++++++++++++++++--- 4 files changed, 83 insertions(+), 16 deletions(-) diff --git a/frameworks/neton/README.md b/frameworks/neton/README.md index e8571755a..a43e265a4 100644 --- a/frameworks/neton/README.md +++ b/frameworks/neton/README.md @@ -6,7 +6,7 @@ Kotlin Multiplatform compiled to a native executable, on the hyper4k engine The framework is consumed from Maven Central as a single coordinate: ```kotlin -implementation("com.netonstream:neton:1.0.0-beta7") +implementation("com.netonstream:neton:1.0.0-beta13") ``` That one dependency carries core, logging, HTTP, routing and the hyper4k engine, diff --git a/frameworks/neton/build.gradle.kts b/frameworks/neton/build.gradle.kts index 478855232..80457da92 100644 --- a/frameworks/neton/build.gradle.kts +++ b/frameworks/neton/build.gradle.kts @@ -10,7 +10,7 @@ repositories { // One published coordinate. `neton` brings core + logging + http + routing and // the hyper4k engine, so the arena entry builds from Maven exactly like any // other application would — no source checkout, no composite build. -val netonVersion = "1.0.0-beta7" +val netonVersion = "1.0.0-beta13" kotlin { // The arena builds linuxX64; macosArm64 is here so the endpoints can be diff --git a/frameworks/neton/meta.json b/frameworks/neton/meta.json index 1434e2cbd..3993af3fd 100644 --- a/frameworks/neton/meta.json +++ b/frameworks/neton/meta.json @@ -10,7 +10,7 @@ "request": true, "response": true }, - "description": "Neton, a Kotlin Multiplatform framework compiled to a native executable. Routing, middleware, request parsing and the response pipeline are the framework's; the hyper4k engine (Tokio + Hyper 1.x, linked in as a Rust static library) only does protocol and transport. Serves HTTP/1.1 on 8080 and HTTP/2 cleartext on 8082 from one route table.", + "description": "Neton, a Kotlin Multiplatform framework compiled to a native executable. Routing, middleware, request parsing and the response pipeline are the framework's; the hyper4k engine (Tokio + Hyper 1.x, linked in as a Rust static library) only does protocol and transport. Serves HTTP/1.1 on 8080, HTTP/2 cleartext on 8082, HTTP/1.1+TLS on 8081 and HTTP/2+TLS on 8443 (ALPN) from one route table; static files and TLS terminate in the framework.", "repo": "https://github.com/netonframework/neton", "enabled": true, "tests": [ @@ -21,7 +21,12 @@ "latency-1m", "latency-10k", "baseline-h2c", - "json-h2c" + "json-h2c", + "json-tls", + "8gbit", + "static-tls", + "baseline-h2", + "static-h2" ], "maintainers": [ "zoujiaqing" diff --git a/frameworks/neton/src/nativeMain/kotlin/Main.kt b/frameworks/neton/src/nativeMain/kotlin/Main.kt index 621a94913..f1d0174de 100644 --- a/frameworks/neton/src/nativeMain/kotlin/Main.kt +++ b/frameworks/neton/src/nativeMain/kotlin/Main.kt @@ -1,5 +1,7 @@ import kotlin.native.runtime.GC import kotlin.native.runtime.NativeRuntimeApi +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.withTimeout import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -26,6 +28,8 @@ import neton.core.http.HttpStatus import neton.core.http.adapter.HttpServerConfig import neton.http.http import neton.http.hyper4k.Hyper4kHttpAdapter +import neton.http.static.staticFiles +import neton.core.http.adapter.TlsSettings import neton.routing.* /** @@ -57,6 +61,16 @@ import neton.routing.* */ private const val H1_PORT = 8080 private val H2C_PORT = getEnv("ARENA_H2C_PORT")?.toIntOrNull() ?: 8082 +// TLS listeners. 8081 serves the HTTP/1.1 + TLS profiles (json-tls, static-tls, +// 8gbit, tls); 8443 serves the HTTP/2 + TLS ones (baseline-h2, static-h2) via +// ALPN. Certificates are mounted read-only at /certs by the harness. Overridable +// so a dev machine need not hold the harness ports or certs. +private val H1TLS_PORT = getEnv("ARENA_H1TLS_PORT")?.toIntOrNull() ?: 8081 +private val H2TLS_PORT = getEnv("ARENA_H2TLS_PORT")?.toIntOrNull() ?: 8443 +private val CERT_PATH = getEnv("ARENA_CERT") ?: "/certs/server.crt" +private val KEY_PATH = getEnv("ARENA_KEY") ?: "/certs/server.key" +private val STATIC_DIR = getEnv("ARENA_STATIC") ?: "/data/static" +private val TLS_ENABLED = readConfigFile(CERT_PATH) != null && readConfigFile(KEY_PATH) != null /** * Mounted read-only by the harness: -v data/dataset.json:/data/dataset.json:ro. @@ -113,9 +127,30 @@ fun main(args: Array) { get("/pipeline") { it.response.text("ok") } get("/delay/{ms}") { it.writeDelay() } get("/json/{count}") { it.writeItems(items) } + + // 8gbit: read the posted body through the standard API and write it + // back verbatim — not from Content-Length, so chunked echoes too. + post("/echo") { it.echoBody() } + + // static-tls / static-h2: serve the mounted files with pre-compressed + // .br/.gz variants selected off Accept-Encoding by the framework. + staticFiles("/static", STATIC_DIR) { precompressed = true } } - onReady { startH2cListener(this) } + onReady { + // Each listener is awaited to its bind before READY returns, so the + // harness never probes a TLS port that is not up yet. A listener that + // fails to bind fails the launch rather than leaving a silent gap. + check(startListener(this, H2C_PORT, null)) { "h2c listener failed to bind on $H2C_PORT" } + if (TLS_ENABLED) { + check(startListener(this, H1TLS_PORT, TlsSettings(CERT_PATH, KEY_PATH, listOf("http/1.1")))) { + "h1+TLS listener failed to bind on $H1TLS_PORT" + } + check(startListener(this, H2TLS_PORT, TlsSettings(CERT_PATH, KEY_PATH, listOf("h2", "http/1.1")))) { + "h2+TLS listener failed to bind on $H2TLS_PORT" + } + } + } } } @@ -265,20 +300,47 @@ private class ArenaItems(private val source: List) { * Same frozen context, so both listeners serve an identical route table and * hyper4k negotiates HTTP/1.1 or HTTP/2 per connection on either of them. */ -private fun startH2cListener(application: KotlinApplication) { +/** /echo: hand back exactly the bytes that arrived. */ +private suspend fun HttpContext.echoBody() { + val body = request.body() + response.contentType = "application/octet-stream" + response.write(body) +} + +/** + * Brings up a TLS listener sharing the frozen route table. [alpn] is the server's + * preference order: `["http/1.1"]` on 8081, `["h2","http/1.1"]` on 8443, so ALPN + * chooses the protocol per connection. + */ +/** + * Brings up one listener sharing the frozen route table and returns only once it + * has bound (or failed). [tls] null serves cleartext; non-null terminates TLS + * with the given ALPN. The serve loop runs for the process lifetime on its own + * scope; this function returns as soon as the bind is confirmed so READY can gate + * on every listener being up. + */ +private suspend fun startListener( + application: KotlinApplication, + port: Int, + tls: TlsSettings?, +): Boolean { val context = application.get() - // Copy the config the framework already resolved from application.conf and - // change only the port. Spelling the fields out again here meant the h2c - // listener silently ignored the file: the h1 listener ran with the - // configured timeout and connection ceiling while this one kept whatever - // was hard-coded, so the two listeners were never the same server and no - // config-level experiment could reach the h2c profiles. val adapter = Hyper4kHttpAdapter( - context.get(HttpServerConfig::class).copy(port = H2C_PORT), + context.get(HttpServerConfig::class).copy(port = port, tls = tls), ) - // start() holds the listener open for the process lifetime and never - // returns, so it cannot run on the framework's own start path. + val bound = CompletableDeferred() CoroutineScope(SupervisorJob() + Dispatchers.Default).launch { - adapter.start(context, null) + try { + adapter.start(context) { bound.complete(Unit) } + } catch (e: Throwable) { + bound.completeExceptionally(e) + } + } + return try { + withTimeout(10_000) { bound.await() } + true + } catch (_: Throwable) { + false } } + From 0e244f846457d895288fa28ff7d368607ce03dd6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Sep 2026 22:08:48 +0000 Subject: [PATCH 02/10] Benchmark results: neton [skip ci] --- site/data/frameworks.json | 2 +- site/data/results/neton.json | 303 ++++++++++++++----- site/static/logs/8gbit/512/neton.log | 16 + site/static/logs/async/32000/neton.log | 2 +- site/static/logs/baseline-h2/1024/neton.log | 16 + site/static/logs/baseline-h2/256/neton.log | 16 + site/static/logs/baseline-h2c/1024/neton.log | 4 +- site/static/logs/baseline-h2c/256/neton.log | 2 +- site/static/logs/baseline-h2c/4096/neton.log | 4 +- site/static/logs/baseline/4096/neton.log | 2 +- site/static/logs/json-h2c/1024/neton.log | 4 +- site/static/logs/json-h2c/4096/neton.log | 2 +- site/static/logs/json-tls/4096/neton.log | 16 + site/static/logs/latency-10k/1024/neton.log | 2 +- site/static/logs/latency-1m/1024/neton.log | 2 +- site/static/logs/limited-conn/4096/neton.log | 2 +- site/static/logs/pipelined/4096/neton.log | 4 +- site/static/logs/static-h2/1024/neton.log | 16 + site/static/logs/static-h2/256/neton.log | 16 + site/static/logs/static-tls/1024/neton.log | 16 + 20 files changed, 349 insertions(+), 98 deletions(-) create mode 100644 site/static/logs/8gbit/512/neton.log create mode 100644 site/static/logs/baseline-h2/1024/neton.log create mode 100644 site/static/logs/baseline-h2/256/neton.log create mode 100644 site/static/logs/json-tls/4096/neton.log create mode 100644 site/static/logs/static-h2/1024/neton.log create mode 100644 site/static/logs/static-h2/256/neton.log create mode 100644 site/static/logs/static-tls/1024/neton.log diff --git a/site/data/frameworks.json b/site/data/frameworks.json index 872e82ee8..c7af4df78 100644 --- a/site/data/frameworks.json +++ b/site/data/frameworks.json @@ -1192,7 +1192,7 @@ }, "neton": { "dir": "neton", - "description": "Neton, a Kotlin Multiplatform framework compiled to a native executable. Routing, middleware, request parsing and the response pipeline are the framework's; the hyper4k engine (Tokio + Hyper 1.x, linked in as a Rust static library) only does protocol and transport. Serves HTTP/1.1 on 8080 and HTTP/2 cleartext on 8082 from one route table.", + "description": "Neton, a Kotlin Multiplatform framework compiled to a native executable. Routing, middleware, request parsing and the response pipeline are the framework's; the hyper4k engine (Tokio + Hyper 1.x, linked in as a Rust static library) only does protocol and transport. Serves HTTP/1.1 on 8080, HTTP/2 cleartext on 8082, HTTP/1.1+TLS on 8081 and HTTP/2+TLS on 8443 (ALPN) from one route table; static files and TLS terminate in the framework.", "repo": "https://github.com/netonframework/neton", "type": "emerging", "engine": "hyper", diff --git a/site/data/results/neton.json b/site/data/results/neton.json index 763fb6268..b18a0c686 100644 --- a/site/data/results/neton.json +++ b/site/data/results/neton.json @@ -1,22 +1,47 @@ { "framework": "neton", "results": { + "8gbit-512": { + "framework": "neton", + "language": "Kotlin", + "rps": 49301, + "avg_latency": "438.2us", + "p99_latency": "12220.0us", + "cpu": "539.4%", + "memory": "1.1GiB", + "connections": 512, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "510.71MB/s", + "input_bw": "481.46MB/s", + "reconnects": 0, + "status_2xx": 246627, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0, + "cpu_usec": 29885536, + "cpu_per_req_us": 121.1771, + "target_rate": 50000, + "rate_ratio": 0.986, + "p99_9_latency": "20328.0us" + }, "async-32000": { "framework": "neton", "language": "Kotlin", - "rps": 105512, - "avg_latency": "77.42ms", - "p99_latency": "89.30ms", - "cpu": "803.8%", + "rps": 108675, + "avg_latency": "76.75ms", + "p99_latency": "87.30ms", + "cpu": "783.5%", "memory": "1.4GiB", "connections": 32000, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "11.87MB/s", - "input_bw": "4.83MB/s", + "bandwidth": "12.23MB/s", + "input_bw": "4.97MB/s", "reconnects": 0, - "status_2xx": 1055122, + "status_2xx": 1086754, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -24,19 +49,57 @@ "baseline-4096": { "framework": "neton", "language": "Kotlin", - "rps": 760483, - "avg_latency": "1.53ms", - "p99_latency": "10.80ms", - "cpu": "5480.6%", - "memory": "1.2GiB", + "rps": 858481, + "avg_latency": "1.34ms", + "p99_latency": "7.89ms", + "cpu": "6019.6%", + "memory": "1.1GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "85.55MB/s", - "input_bw": "58.75MB/s", + "bandwidth": "96.57MB/s", + "input_bw": "66.32MB/s", "reconnects": 0, - "status_2xx": 3802419, + "status_2xx": 4292408, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-h2-1024": { + "framework": "neton", + "language": "Kotlin", + "rps": 544308, + "avg_latency": "145.88ms", + "p99_latency": "536.05ms", + "cpu": "5050.2%", + "memory": "1.5GiB", + "connections": 1024, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "13.70MB/s", + "reconnects": 0, + "status_2xx": 2754199, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "baseline-h2-256": { + "framework": "neton", + "language": "Kotlin", + "rps": 584458, + "avg_latency": "42.86ms", + "p99_latency": "178.70ms", + "cpu": "4760.3%", + "memory": "1.2GiB", + "connections": 256, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "14.59MB/s", + "reconnects": 0, + "status_2xx": 2939827, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -44,18 +107,18 @@ "baseline-h2c-1024": { "framework": "neton", "language": "Kotlin", - "rps": 533030, - "avg_latency": "166.79ms", - "p99_latency": "645.63ms", - "cpu": "4842.5%", + "rps": 538751, + "avg_latency": "153.90ms", + "p99_latency": "599.25ms", + "cpu": "4798.9%", "memory": "1.6GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "13.41MB/s", + "bandwidth": "13.56MB/s", "reconnects": 0, - "status_2xx": 2697134, + "status_2xx": 2726083, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -63,18 +126,18 @@ "baseline-h2c-256": { "framework": "neton", "language": "Kotlin", - "rps": 577322, - "avg_latency": "43.82ms", - "p99_latency": "164.52ms", - "cpu": "4777.7%", + "rps": 584312, + "avg_latency": "43.16ms", + "p99_latency": "159.95ms", + "cpu": "4756.5%", "memory": "1.3GiB", "connections": 256, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "14.41MB/s", + "bandwidth": "14.56MB/s", "reconnects": 0, - "status_2xx": 2903933, + "status_2xx": 2933250, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -82,18 +145,18 @@ "baseline-h2c-4096": { "framework": "neton", "language": "Kotlin", - "rps": 485744, - "avg_latency": "394.60ms", - "p99_latency": "1.84s", - "cpu": "4982.7%", + "rps": 534358, + "avg_latency": "334.74ms", + "p99_latency": "1.70s", + "cpu": "4760.9%", "memory": "2.2GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "12.36MB/s", + "bandwidth": "13.61MB/s", "reconnects": 0, - "status_2xx": 2472442, + "status_2xx": 2725229, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -101,18 +164,18 @@ "json-h2c-1024": { "framework": "neton", "language": "Kotlin", - "rps": 258529, - "avg_latency": "101.06ms", - "p99_latency": "1.06s", - "cpu": "4500.5%", + "rps": 264714, + "avg_latency": "103.10ms", + "p99_latency": "680.38ms", + "cpu": "4403.3%", "memory": "1.4GiB", "connections": 1024, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "881.47MB/s", + "bandwidth": "904.34MB/s", "reconnects": 0, - "status_2xx": 1305573, + "status_2xx": 1339453, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -120,18 +183,37 @@ "json-h2c-4096": { "framework": "neton", "language": "Kotlin", - "rps": 245577, - "avg_latency": "310.95ms", - "p99_latency": "4.49s", - "cpu": "4275.6%", + "rps": 0, + "avg_latency": "", + "p99_latency": "", + "cpu": "3730.0%", "memory": "1.6GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "840.82MB/s", + "bandwidth": "0", "reconnects": 0, - "status_2xx": 1249987, + "status_2xx": 0, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "json-tls-4096": { + "framework": "neton", + "language": "Kotlin", + "rps": 286810, + "avg_latency": "14.41ms", + "p99_latency": "105.01ms", + "cpu": "6275.5%", + "memory": "1.3GiB", + "connections": 4096, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "0.97GB", + "reconnects": 0, + "status_2xx": 1462616, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -139,67 +221,67 @@ "latency-10k-1024": { "framework": "neton", "language": "Kotlin", - "rps": 9718, - "avg_latency": "117.8us", - "p99_latency": "197.0us", - "cpu": "39.7%", - "memory": "1019MiB", + "rps": 9669, + "avg_latency": "104.9us", + "p99_latency": "176.0us", + "cpu": "38.0%", + "memory": "840MiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "1.14MB/s", - "reconnects": 18, - "status_2xx": 194389, + "bandwidth": "1.13MB/s", + "reconnects": 0, + "status_2xx": 193416, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 8190152, - "cpu_per_req_us": 42.1328, + "cpu_usec": 7574027, + "cpu_per_req_us": 39.1593, "target_rate": 10000, - "rate_ratio": 0.9718, - "p99_9_latency": "16172.0us" + "rate_ratio": 0.9669, + "p99_9_latency": "10396.0us" }, "latency-1m-1024": { "framework": "neton", "language": "Kotlin", - "rps": 781096, - "avg_latency": "1888492.4us", - "p99_latency": "4011008.0us", - "cpu": "5505.8%", + "rps": 848610, + "avg_latency": "1315609.9us", + "p99_latency": "2757632.0us", + "cpu": "5267.2%", "memory": "1.1GiB", "connections": 1024, "threads": 64, "duration": "20s", "pipeline": 1, - "bandwidth": "91.39MB/s", - "reconnects": 77, - "status_2xx": 15628640, + "bandwidth": "99.29MB/s", + "reconnects": 10, + "status_2xx": 16978772, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0, - "cpu_usec": 1078791749, - "cpu_per_req_us": 69.0266, + "cpu_usec": 1026601326, + "cpu_per_req_us": 60.4638, "target_rate": 1000000, - "rate_ratio": 0.7811, - "p99_9_latency": "4097024.0us" + "rate_ratio": 0.8486, + "p99_9_latency": "2827264.0us" }, "limited-conn-4096": { "framework": "neton", "language": "Kotlin", - "rps": 463436, - "avg_latency": "483us", - "p99_latency": "2.42ms", - "cpu": "2782.7%", + "rps": 493348, + "avg_latency": "512us", + "p99_latency": "2.28ms", + "cpu": "2698.8%", "memory": "1.1GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 1, - "bandwidth": "52.13MB/s", - "input_bw": "35.80MB/s", - "reconnects": 231731, - "status_2xx": 2317181, + "bandwidth": "55.50MB/s", + "input_bw": "38.11MB/s", + "reconnects": 246716, + "status_2xx": 2466743, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 @@ -207,18 +289,75 @@ "pipelined-4096": { "framework": "neton", "language": "Kotlin", - "rps": 368285, - "avg_latency": "40.00ms", - "p99_latency": "415.60ms", - "cpu": "4180.9%", + "rps": 336940, + "avg_latency": "39.71ms", + "p99_latency": "437.00ms", + "cpu": "3364.7%", "memory": "1.2GiB", "connections": 4096, "threads": 64, "duration": "5s", "pipeline": 16, - "bandwidth": "41.43MB/s", + "bandwidth": "37.90MB/s", + "reconnects": 0, + "status_2xx": 1684702, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "static-h2-1024": { + "framework": "neton", + "language": "Kotlin", + "rps": 267476, + "avg_latency": "110.84ms", + "p99_latency": "355.24ms", + "cpu": "3642.4%", + "memory": "1.4GiB", + "connections": 1024, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "4.11GB/s", + "reconnects": 0, + "status_2xx": 1353431, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "static-h2-256": { + "framework": "neton", + "language": "Kotlin", + "rps": 350163, + "avg_latency": "23.01ms", + "p99_latency": "144.89ms", + "cpu": "5392.6%", + "memory": "1.3GiB", + "connections": 256, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "5.36GB/s", + "reconnects": 0, + "status_2xx": 1761323, + "status_3xx": 0, + "status_4xx": 0, + "status_5xx": 0 + }, + "static-tls-1024": { + "framework": "neton", + "language": "Kotlin", + "rps": 380137, + "avg_latency": "3.45ms", + "p99_latency": "65.95ms", + "cpu": "6198.6%", + "memory": "1.2GiB", + "connections": 1024, + "threads": 64, + "duration": "5s", + "pipeline": 1, + "bandwidth": "5.83GB", "reconnects": 0, - "status_2xx": 1841428, + "status_2xx": 1929709, "status_3xx": 0, "status_4xx": 0, "status_5xx": 0 diff --git a/site/static/logs/8gbit/512/neton.log b/site/static/logs/8gbit/512/neton.log new file mode 100644 index 000000000..1d92ef579 --- /dev/null +++ b/site/static/logs/8gbit/512/neton.log @@ -0,0 +1,16 @@ + +░█▀█░█▀▀░▀█▀░█▀█░█▀█ +░█░█░█▀▀░░█░░█░█░█░█ +░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ + +Neton 1.0.0-beta13 +Kotlin/Native Runtime + +Adapter : Hyper4k +Environment : dev +Port : 8080 +PID : 1 +Cold Start : 3 ms + +Ready → http://localhost:8080 + diff --git a/site/static/logs/async/32000/neton.log b/site/static/logs/async/32000/neton.log index 2138b754f..1d92ef579 100644 --- a/site/static/logs/async/32000/neton.log +++ b/site/static/logs/async/32000/neton.log @@ -3,7 +3,7 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k diff --git a/site/static/logs/baseline-h2/1024/neton.log b/site/static/logs/baseline-h2/1024/neton.log new file mode 100644 index 000000000..1d92ef579 --- /dev/null +++ b/site/static/logs/baseline-h2/1024/neton.log @@ -0,0 +1,16 @@ + +░█▀█░█▀▀░▀█▀░█▀█░█▀█ +░█░█░█▀▀░░█░░█░█░█░█ +░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ + +Neton 1.0.0-beta13 +Kotlin/Native Runtime + +Adapter : Hyper4k +Environment : dev +Port : 8080 +PID : 1 +Cold Start : 3 ms + +Ready → http://localhost:8080 + diff --git a/site/static/logs/baseline-h2/256/neton.log b/site/static/logs/baseline-h2/256/neton.log new file mode 100644 index 000000000..b89079a37 --- /dev/null +++ b/site/static/logs/baseline-h2/256/neton.log @@ -0,0 +1,16 @@ + +░█▀█░█▀▀░▀█▀░█▀█░█▀█ +░█░█░█▀▀░░█░░█░█░█░█ +░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ + +Neton 1.0.0-beta13 +Kotlin/Native Runtime + +Adapter : Hyper4k +Environment : dev +Port : 8080 +PID : 1 +Cold Start : 6 ms + +Ready → http://localhost:8080 + diff --git a/site/static/logs/baseline-h2c/1024/neton.log b/site/static/logs/baseline-h2c/1024/neton.log index 44c445748..1d92ef579 100644 --- a/site/static/logs/baseline-h2c/1024/neton.log +++ b/site/static/logs/baseline-h2c/1024/neton.log @@ -3,14 +3,14 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k Environment : dev Port : 8080 PID : 1 -Cold Start : 4 ms +Cold Start : 3 ms Ready → http://localhost:8080 diff --git a/site/static/logs/baseline-h2c/256/neton.log b/site/static/logs/baseline-h2c/256/neton.log index 2138b754f..1d92ef579 100644 --- a/site/static/logs/baseline-h2c/256/neton.log +++ b/site/static/logs/baseline-h2c/256/neton.log @@ -3,7 +3,7 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k diff --git a/site/static/logs/baseline-h2c/4096/neton.log b/site/static/logs/baseline-h2c/4096/neton.log index 44c445748..1d92ef579 100644 --- a/site/static/logs/baseline-h2c/4096/neton.log +++ b/site/static/logs/baseline-h2c/4096/neton.log @@ -3,14 +3,14 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k Environment : dev Port : 8080 PID : 1 -Cold Start : 4 ms +Cold Start : 3 ms Ready → http://localhost:8080 diff --git a/site/static/logs/baseline/4096/neton.log b/site/static/logs/baseline/4096/neton.log index 44c445748..320712c95 100644 --- a/site/static/logs/baseline/4096/neton.log +++ b/site/static/logs/baseline/4096/neton.log @@ -3,7 +3,7 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k diff --git a/site/static/logs/json-h2c/1024/neton.log b/site/static/logs/json-h2c/1024/neton.log index 2138b754f..320712c95 100644 --- a/site/static/logs/json-h2c/1024/neton.log +++ b/site/static/logs/json-h2c/1024/neton.log @@ -3,14 +3,14 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k Environment : dev Port : 8080 PID : 1 -Cold Start : 3 ms +Cold Start : 4 ms Ready → http://localhost:8080 diff --git a/site/static/logs/json-h2c/4096/neton.log b/site/static/logs/json-h2c/4096/neton.log index 2138b754f..1d92ef579 100644 --- a/site/static/logs/json-h2c/4096/neton.log +++ b/site/static/logs/json-h2c/4096/neton.log @@ -3,7 +3,7 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k diff --git a/site/static/logs/json-tls/4096/neton.log b/site/static/logs/json-tls/4096/neton.log new file mode 100644 index 000000000..320712c95 --- /dev/null +++ b/site/static/logs/json-tls/4096/neton.log @@ -0,0 +1,16 @@ + +░█▀█░█▀▀░▀█▀░█▀█░█▀█ +░█░█░█▀▀░░█░░█░█░█░█ +░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ + +Neton 1.0.0-beta13 +Kotlin/Native Runtime + +Adapter : Hyper4k +Environment : dev +Port : 8080 +PID : 1 +Cold Start : 4 ms + +Ready → http://localhost:8080 + diff --git a/site/static/logs/latency-10k/1024/neton.log b/site/static/logs/latency-10k/1024/neton.log index 2138b754f..1d92ef579 100644 --- a/site/static/logs/latency-10k/1024/neton.log +++ b/site/static/logs/latency-10k/1024/neton.log @@ -3,7 +3,7 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k diff --git a/site/static/logs/latency-1m/1024/neton.log b/site/static/logs/latency-1m/1024/neton.log index 2138b754f..1d92ef579 100644 --- a/site/static/logs/latency-1m/1024/neton.log +++ b/site/static/logs/latency-1m/1024/neton.log @@ -3,7 +3,7 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k diff --git a/site/static/logs/limited-conn/4096/neton.log b/site/static/logs/limited-conn/4096/neton.log index 2138b754f..1d92ef579 100644 --- a/site/static/logs/limited-conn/4096/neton.log +++ b/site/static/logs/limited-conn/4096/neton.log @@ -3,7 +3,7 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k diff --git a/site/static/logs/pipelined/4096/neton.log b/site/static/logs/pipelined/4096/neton.log index 44c445748..1d92ef579 100644 --- a/site/static/logs/pipelined/4096/neton.log +++ b/site/static/logs/pipelined/4096/neton.log @@ -3,14 +3,14 @@ ░█░█░█▀▀░░█░░█░█░█░█ ░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ -Neton 1.0.0-beta7 +Neton 1.0.0-beta13 Kotlin/Native Runtime Adapter : Hyper4k Environment : dev Port : 8080 PID : 1 -Cold Start : 4 ms +Cold Start : 3 ms Ready → http://localhost:8080 diff --git a/site/static/logs/static-h2/1024/neton.log b/site/static/logs/static-h2/1024/neton.log new file mode 100644 index 000000000..1d92ef579 --- /dev/null +++ b/site/static/logs/static-h2/1024/neton.log @@ -0,0 +1,16 @@ + +░█▀█░█▀▀░▀█▀░█▀█░█▀█ +░█░█░█▀▀░░█░░█░█░█░█ +░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ + +Neton 1.0.0-beta13 +Kotlin/Native Runtime + +Adapter : Hyper4k +Environment : dev +Port : 8080 +PID : 1 +Cold Start : 3 ms + +Ready → http://localhost:8080 + diff --git a/site/static/logs/static-h2/256/neton.log b/site/static/logs/static-h2/256/neton.log new file mode 100644 index 000000000..1d92ef579 --- /dev/null +++ b/site/static/logs/static-h2/256/neton.log @@ -0,0 +1,16 @@ + +░█▀█░█▀▀░▀█▀░█▀█░█▀█ +░█░█░█▀▀░░█░░█░█░█░█ +░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ + +Neton 1.0.0-beta13 +Kotlin/Native Runtime + +Adapter : Hyper4k +Environment : dev +Port : 8080 +PID : 1 +Cold Start : 3 ms + +Ready → http://localhost:8080 + diff --git a/site/static/logs/static-tls/1024/neton.log b/site/static/logs/static-tls/1024/neton.log new file mode 100644 index 000000000..1d92ef579 --- /dev/null +++ b/site/static/logs/static-tls/1024/neton.log @@ -0,0 +1,16 @@ + +░█▀█░█▀▀░▀█▀░█▀█░█▀█ +░█░█░█▀▀░░█░░█░█░█░█ +░▀░▀░▀▀▀░░▀░░▀▀▀░▀░▀░ + +Neton 1.0.0-beta13 +Kotlin/Native Runtime + +Adapter : Hyper4k +Environment : dev +Port : 8080 +PID : 1 +Cold Start : 3 ms + +Ready → http://localhost:8080 + From 5f9af4678186479fd89ba13b688b788f7c2cf07d Mon Sep 17 00:00:00 2001 From: zoujiaqing Date: Wed, 9 Sep 2026 20:12:57 +0800 Subject: [PATCH 03/10] neton: bump to 1.0.0-beta14 (JSON string bulk-append encoding) --- frameworks/neton/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frameworks/neton/build.gradle.kts b/frameworks/neton/build.gradle.kts index 80457da92..9a39e3eb7 100644 --- a/frameworks/neton/build.gradle.kts +++ b/frameworks/neton/build.gradle.kts @@ -10,7 +10,7 @@ repositories { // One published coordinate. `neton` brings core + logging + http + routing and // the hyper4k engine, so the arena entry builds from Maven exactly like any // other application would — no source checkout, no composite build. -val netonVersion = "1.0.0-beta13" +val netonVersion = "1.0.0-beta14" kotlin { // The arena builds linuxX64; macosArm64 is here so the endpoints can be From 159d7071daf4d061f10730a89b4df911c5a82bb0 Mon Sep 17 00:00:00 2001 From: zoujiaqing Date: Wed, 9 Sep 2026 20:23:31 +0800 Subject: [PATCH 04/10] neton: subscribe to latency-500k-8cpu (same /baseline11 endpoint as latency-1m/10k) --- frameworks/neton/meta.json | 1 + 1 file changed, 1 insertion(+) diff --git a/frameworks/neton/meta.json b/frameworks/neton/meta.json index 3993af3fd..d5dedaaaf 100644 --- a/frameworks/neton/meta.json +++ b/frameworks/neton/meta.json @@ -20,6 +20,7 @@ "async", "latency-1m", "latency-10k", + "latency-500k-8cpu", "baseline-h2c", "json-h2c", "json-tls", From 4219a85d8e34b1cc6a75c3e0ae9f1d48aceffc0f Mon Sep 17 00:00:00 2001 From: zoujiaqing Date: Wed, 9 Sep 2026 21:24:10 +0800 Subject: [PATCH 05/10] neton: bump to 1.0.0-beta15, subscribe json-comp (dynamic gzip) --- frameworks/neton/build.gradle.kts | 2 +- frameworks/neton/meta.json | 1 + frameworks/neton/src/nativeMain/kotlin/Main.kt | 6 ++++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frameworks/neton/build.gradle.kts b/frameworks/neton/build.gradle.kts index 9a39e3eb7..e9e56f5c4 100644 --- a/frameworks/neton/build.gradle.kts +++ b/frameworks/neton/build.gradle.kts @@ -10,7 +10,7 @@ repositories { // One published coordinate. `neton` brings core + logging + http + routing and // the hyper4k engine, so the arena entry builds from Maven exactly like any // other application would — no source checkout, no composite build. -val netonVersion = "1.0.0-beta14" +val netonVersion = "1.0.0-beta15" kotlin { // The arena builds linuxX64; macosArm64 is here so the endpoints can be diff --git a/frameworks/neton/meta.json b/frameworks/neton/meta.json index d5dedaaaf..11ad3abf2 100644 --- a/frameworks/neton/meta.json +++ b/frameworks/neton/meta.json @@ -23,6 +23,7 @@ "latency-500k-8cpu", "baseline-h2c", "json-h2c", + "json-comp", "json-tls", "8gbit", "static-tls", diff --git a/frameworks/neton/src/nativeMain/kotlin/Main.kt b/frameworks/neton/src/nativeMain/kotlin/Main.kt index f1d0174de..2e7358d21 100644 --- a/frameworks/neton/src/nativeMain/kotlin/Main.kt +++ b/frameworks/neton/src/nativeMain/kotlin/Main.kt @@ -46,8 +46,10 @@ import neton.routing.* * same socket by prior knowledge, so the second listener is a second adapter * over the same frozen context, not a second application. * - * Not subscribed (see meta.json): the TLS profiles — the engine terminates no - * TLS today — plus json-comp, which needs gzip/br response compression. + * Not subscribed (see meta.json): the profiles that need capabilities the engine + * does not have yet — HTTP/3, gRPC, WebSocket — and the multi-service DB profiles + * (async-db, fortunes, production-stack). json-comp is served here: the framework + * gzip-compresses compressible responses when the client sends Accept-Encoding. */ /** From 7c584745bd9d012833bc73c9aae179677ba4a91f Mon Sep 17 00:00:00 2001 From: zoujiaqing Date: Wed, 9 Sep 2026 21:59:50 +0800 Subject: [PATCH 06/10] ci: re-trigger validate now that neton 1.0.0-beta15 + hyper4k 0.8.0 are live on Central From 02b9dd48212d51f59141872fc59ef408f1c43d24 Mon Sep 17 00:00:00 2001 From: zoujiaqing Date: Wed, 9 Sep 2026 22:13:48 +0800 Subject: [PATCH 07/10] neton: enable response compression (beta15 implements gzip) for json-comp --- frameworks/neton/config/application.conf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frameworks/neton/config/application.conf b/frameworks/neton/config/application.conf index 792564eae..daaa6aad3 100644 --- a/frameworks/neton/config/application.conf +++ b/frameworks/neton/config/application.conf @@ -37,8 +37,9 @@ timeout = 0 # 524,288 clears the peak with room to spare. The counter itself costs nothing; # the memory belongs to the requests actually in flight, and the box has 251 GiB. maxConnections = 524288 -# No response compression is implemented, so nothing claims to do it. -enableCompression = false +# Dynamic gzip response compression (beta15+): compressible responses are gzipped +# when the client sends Accept-Encoding. Required for json-comp. +enableCompression = true [logging] # Measured runs must not spend request time on log lines. From bcd51d8380b8935eab9d6e005af33afd9c045319 Mon Sep 17 00:00:00 2001 From: zoujiaqing Date: Wed, 9 Sep 2026 22:44:07 +0800 Subject: [PATCH 08/10] neton: add async-db (async Postgres via neton-database/sqlx4k) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /async-db?min&max&limit → sequential scan of items by price range, returns {count, items:[{id,name,category,price,quantity,active,tags,rating:{score,count}}]}. database{} is gated by ARENA_DB (default on) so the baseline A/B can run the same binary with the pool out; measured on a 2-core box, the idle pool costs baseline nothing (per-req CPU flat, rps within noise). Links hyper4k + sqlx4k with --allow-multiple-definition (both bundle the Rust runtime). --- frameworks/neton/build.gradle.kts | 10 ++- frameworks/neton/config/database.conf | 7 ++ frameworks/neton/meta.json | 3 +- .../neton/src/nativeMain/kotlin/Main.kt | 65 +++++++++++++++++++ 4 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 frameworks/neton/config/database.conf diff --git a/frameworks/neton/build.gradle.kts b/frameworks/neton/build.gradle.kts index e9e56f5c4..cf4d4a1a4 100644 --- a/frameworks/neton/build.gradle.kts +++ b/frameworks/neton/build.gradle.kts @@ -16,7 +16,13 @@ kotlin { // The arena builds linuxX64; macosArm64 is here so the endpoints can be // exercised on a developer machine. listOf(macosArm64(), linuxX64(), linuxArm64()).forEach { target -> - target.binaries.executable { entryPoint = "main" } + target.binaries.executable { + entryPoint = "main" + // hyper4k and sqlx4k (neton-database) are each a Rust static library, so + // both bundle the Rust runtime — linking both defines rust_eh_personality + // (and friends) twice. The copies are identical; take the first. + linkerOpts("--allow-multiple-definition") + } } sourceSets { @@ -27,6 +33,8 @@ kotlin { dependsOn(commonMain.get()) dependencies { implementation("com.netonstream:neton:$netonVersion") + // async-db / fortunes: async Postgres via sqlx4k. + implementation("com.netonstream:neton-database:$netonVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0") implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0") // Encoding straight into a byte buffer, rather than to a String the diff --git a/frameworks/neton/config/database.conf b/frameworks/neton/config/database.conf new file mode 100644 index 000000000..9105508ae --- /dev/null +++ b/frameworks/neton/config/database.conf @@ -0,0 +1,7 @@ +# Postgres sidecar the arena starts for the DB profiles (async-db / fortunes). +# Host networking, so the framework reaches it on localhost:5432. Fixed +# credentials/db name are set by the harness (validate.sh / benchmark.sh). +[default] +driver = "POSTGRESQL" +uri = "postgresql://bench:bench@localhost:5432/benchmark" +debug = false diff --git a/frameworks/neton/meta.json b/frameworks/neton/meta.json index 11ad3abf2..5204cf266 100644 --- a/frameworks/neton/meta.json +++ b/frameworks/neton/meta.json @@ -28,7 +28,8 @@ "8gbit", "static-tls", "baseline-h2", - "static-h2" + "static-h2", + "async-db" ], "maintainers": [ "zoujiaqing" diff --git a/frameworks/neton/src/nativeMain/kotlin/Main.kt b/frameworks/neton/src/nativeMain/kotlin/Main.kt index 2e7358d21..f71f882e5 100644 --- a/frameworks/neton/src/nativeMain/kotlin/Main.kt +++ b/frameworks/neton/src/nativeMain/kotlin/Main.kt @@ -26,6 +26,8 @@ import neton.core.config.readConfigFile import neton.core.http.HttpContext import neton.core.http.HttpStatus import neton.core.http.adapter.HttpServerConfig +import neton.database.database +import neton.database.dbContext import neton.http.http import neton.http.hyper4k.Hyper4kHttpAdapter import neton.http.static.staticFiles @@ -119,6 +121,13 @@ fun main(args: Array) { port = H1_PORT } + // async-db / fortunes need Postgres. Gated so the baseline A/B can run the + // exact same binary with the DB out of the picture (ARENA_DB=0): the plain + // profiles never touch the pool, and this proves it costs them nothing. + if (getEnv("ARENA_DB") != "0") { + database { } + } + routing { get("/baseline11") { it.writeSum() } post("/baseline11") { it.writeSum(withBody = true) } @@ -130,6 +139,10 @@ fun main(args: Array) { get("/delay/{ms}") { it.writeDelay() } get("/json/{count}") { it.writeItems(items) } + // async-db: async Postgres sequential scan (no index on price) → + // {count, items:[{..., active:bool, tags:[...], rating:{score,count}}]}. + get("/async-db") { it.writeDbItems() } + // 8gbit: read the posted body through the standard API and write it // back verbatim — not from Content-Length, so chunked echoes too. post("/echo") { it.echoBody() } @@ -196,6 +209,58 @@ private suspend fun HttpContext.writeItems(items: ArenaItems) { response.write(items.render(request.pathParam("count"), request.queryParam("m"))) } +/** + * /async-db?min=&max=&limit=: rows from Postgres selected by price range. There is + * no index on price, so this is a sequential scan — the point of the profile. The + * body is built straight to bytes; `tags` is a JSONB column whose text is already a + * valid JSON array, so it is embedded verbatim. + */ +private suspend fun HttpContext.writeDbItems() { + val min = request.queryParam("min")?.toIntOrNull() ?: 0 + val max = request.queryParam("max")?.toIntOrNull() ?: Int.MAX_VALUE + val limit = (request.queryParam("limit")?.toIntOrNull() ?: 1).coerceIn(0, 1000) + val rows = dbContext().fetchAll( + "SELECT id, name, category, price, quantity, active, tags, rating_score, rating_count " + + "FROM items WHERE price BETWEEN :min AND :max LIMIT :limit", + mapOf("min" to min, "max" to max, "limit" to limit), + ) + val sb = StringBuilder(64 + rows.size * 160) + sb.append("{\"count\":").append(rows.size).append(",\"items\":[") + for (i in rows.indices) { + val r = rows[i] + if (i > 0) sb.append(',') + sb.append("{\"id\":").append(r.int("id")) + sb.append(",\"name\":"); appendJsonString(sb, r.string("name")) + sb.append(",\"category\":"); appendJsonString(sb, r.string("category")) + sb.append(",\"price\":").append(r.int("price")) + sb.append(",\"quantity\":").append(r.int("quantity")) + sb.append(",\"active\":").append(r.boolean("active")) + sb.append(",\"tags\":").append(r.string("tags")) + sb.append(",\"rating\":{\"score\":").append(r.int("rating_score")) + sb.append(",\"count\":").append(r.int("rating_count")).append("}}") + } + sb.append("]}") + response.contentType = "application/json; charset=utf-8" + response.write(sb.toString().encodeToByteArray()) +} + +/** Minimal JSON string emitter for the DB text columns (name/category). */ +private fun appendJsonString(sb: StringBuilder, value: String) { + sb.append('"') + for (c in value) { + when { + c == '"' -> sb.append("\\\"") + c == '\\' -> sb.append("\\\\") + c == '\n' -> sb.append("\\n") + c == '\r' -> sb.append("\\r") + c == '\t' -> sb.append("\\t") + c.code < 0x20 -> sb.append("\\u").append(c.code.toString(16).padStart(4, '0')) + else -> sb.append(c) + } + } + sb.append('"') +} + /** * The dataset behind `/json/{count}?m=M`: the first `count` items of * /data/dataset.json, field for field, each with total = price * quantity * m. From d15e3d888040551ee23a27a9d51ae9f0655e9350 Mon Sep 17 00:00:00 2001 From: zoujiaqing Date: Wed, 9 Sep 2026 22:48:05 +0800 Subject: [PATCH 09/10] neton: add fortunes (DB query + HTML template render) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /fortunes → all fortune rows + one runtime-injected row, sorted by message, rendered as an escaped HTML table (row 11's