Skip to content

Commit 44800e2

Browse files
MDA2AVclaudegithub-actions[bot]
authored
fishcake: add static, json-comp and json-tls profiles (#803)
* fishcake: add static, json-comp and json-tls profiles Adds the three remaining isolated HTTP/1.1 profiles to the CodeGreen entry: - static: serves /data/static with pre-compressed variant selection (serves the on-disk .br/.gz sibling with the original file's Content-Type when the client accepts it, else the raw file; 404 for a missing file). Implemented in the entry (StaticFiles.kt) on CodeGreen's IO primitives. - json-comp: enables the gzip compression concern on the host (.add(Compression.default())) so /json responses carry Content-Encoding: gzip only when Accept-Encoding is sent. - json-tls: binds HTTPS on :8081 with the harness certificate (/certs), serving HTTP/1.1 over TLS. json-comp and json-tls rely on the response-compression module + TLS engine support added in CodeGreen PR dotnet-web-stack/CodeGreen#9; the Dockerfile temporarily clones that branch (add-compression-tls) until it merges to stream-bodies. ./scripts/validate.sh fishcake passes 41/0 (12 profiles). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fishcake: clone CodeGreen from main (compression/TLS/pipelining merged) CodeGreen PRs #9 (response compression + TLS + pipelined flush coalescing) and #10 (request-body streaming) are merged to main, so the build no longer needs the temporary add-compression-tls branch. Clone main, which carries everything these profiles use. Validated via the real (clone-from-main) Docker build: ./scripts/validate.sh fishcake = 41/0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Benchmark results: fishcake --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent f66f958 commit 44800e2

46 files changed

Lines changed: 591 additions & 13 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frameworks/fishcake/Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ WORKDIR /app
88
COPY . .
99

1010
# CodeGreen (the Kotlin GenHTTP port) is consumed as a composite build, wired in
11-
# settings.gradle.kts via includeBuild("codegreen"). Pinned to the stream-bodies branch
12-
# while the request-body streaming work is benchmarked; folds back into port once proven.
13-
RUN git clone --depth 1 --branch stream-bodies https://github.com/dotnet-web-stack/CodeGreen.git codegreen
11+
# settings.gradle.kts via includeBuild("codegreen"). Cloned from main, which carries the
12+
# request-body streaming, response-compression, TLS and pipelined-flush work these profiles use.
13+
RUN git clone --depth 1 --branch main https://github.com/dotnet-web-stack/CodeGreen.git codegreen
1414

1515
RUN chmod +x ./gradlew && ./gradlew shadowJar --no-daemon
1616

frameworks/fishcake/meta.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
"pipelined",
1212
"limited-conn",
1313
"json",
14+
"json-comp",
15+
"json-tls",
1416
"upload",
1517
"async-db",
1618
"crud",
19+
"static",
1720
"api-4",
1821
"api-16"
1922
],

frameworks/fishcake/src/main/kotlin/com/httparena/fishcake/Main.kt

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package com.httparena.fishcake
22

33
import kotlinx.coroutines.runBlocking
44
import org.codegreen.engine.internal.Host
5+
import org.codegreen.modules.io.Compression
6+
import java.io.File
7+
import java.net.InetAddress
58
import kotlin.system.exitProcess
69

710
/**
@@ -12,14 +15,21 @@ fun main() {
1215
// Touch the singletons up front so the dataset is parsed and the DB pool is opened
1316
// before the first request arrives.
1417
val datasetSize = Dataset.items.size
15-
println("fishcake (CodeGreen) starting on :8080 — dataset items: $datasetSize, database: ${if (Database.available) "connected" else "disabled"}")
1618

17-
val exitCode = runBlocking {
18-
Host.create()
19-
.handler(Project.create())
20-
.port(8080)
21-
.run()
22-
}
19+
// Plain HTTP/1.1 on :8080; HTTPS on :8081 when the harness mounts a certificate (json-tls).
20+
val host = Host.create()
21+
.handler(Project.create())
22+
.add(Compression.default())
23+
.bind(null, 8080)
24+
25+
val certificate = File(System.getenv("TLS_CERT") ?: "/certs/server.crt")
26+
val key = File(System.getenv("TLS_KEY") ?: "/certs/server.key")
27+
val tls = certificate.exists() && key.exists()
28+
if (tls) host.bind(InetAddress.getByName("0.0.0.0"), 8081, certificate, key)
29+
30+
println("fishcake (CodeGreen) starting on :8080${if (tls) " + TLS on :8081" else ""} — dataset items: $datasetSize, database: ${if (Database.available) "connected" else "disabled"}")
31+
32+
val exitCode = runBlocking { host.run() }
2333

2434
exitProcess(exitCode)
2535
}

frameworks/fishcake/src/main/kotlin/com/httparena/fishcake/Project.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,14 @@ import org.codegreen.modules.webservices.addService
1414
/**
1515
* Builds the routing tree, mirroring the C# `genhttp-11` entry's `Project.Create()`.
1616
*
17-
* Compression, static files, websockets and TLS/HTTP-2 from the original entry rely on
18-
* modules the CodeGreen port does not provide yet and are omitted; the implemented profiles
19-
* are baseline, pipelined, json, upload, async-db and crud.
17+
* `static` serves /data/static with pre-compressed variant selection (see [StaticFiles]).
18+
* Websockets and HTTP-2/3 from the original entry rely on modules the CodeGreen port does not
19+
* provide yet and are omitted.
2020
*/
2121
object Project {
2222

23+
private val STATIC_DIR: String = System.getenv("STATIC_DIR") ?: "/data/static"
24+
2325
fun create(): LayoutBuilder =
2426
Layout.create()
2527
.add("pipeline", Content.from(Resource.fromString("ok")))
@@ -29,4 +31,5 @@ object Project {
2931
.addService<JsonService>("json")
3032
.addService<AsyncDatabase>("async-db")
3133
.add("crud", Layout.create().addService<Crud>("items"))
34+
.add("static", StaticFiles.from(STATIC_DIR))
3235
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.httparena.fishcake
2+
3+
import org.codegreen.api.MemoryView
4+
import org.codegreen.api.content.HandlerBuilder
5+
import org.codegreen.api.protocol.Request
6+
import org.codegreen.api.protocol.Response
7+
import org.codegreen.api.protocol.getEntry
8+
import org.codegreen.modules.io.Handler
9+
import org.codegreen.modules.io.Resource
10+
import org.codegreen.modules.io.guessContentType
11+
import org.codegreen.modules.io.streaming.ResourceContent
12+
import java.io.File
13+
14+
/**
15+
* Serves a directory of static files (the HttpArena "static" profile) with pre-compressed
16+
* variant selection: when the request carries `Accept-Encoding` and a sibling `<file>.br` or
17+
* `<file>.gz` exists on disk, that variant is served with the matching `Content-Encoding` and the
18+
* original file's `Content-Type`. Mirrors GenHTTP's pre-compressed assets (#840) — the bytes are
19+
* served straight off disk, so no runtime compression is needed here. A missing file yields
20+
* `null`, which the surrounding layout turns into a 404.
21+
*/
22+
object StaticFiles {
23+
24+
// Brotli before gzip: Brotli has the higher priority in GenHTTP's compression set, so when the
25+
// client accepts both we serve the smaller payload.
26+
private val PRECOMPRESSED = listOf("br" to ".br", "gzip" to ".gz")
27+
28+
fun from(directory: String): HandlerBuilder {
29+
val root = File(directory).canonicalFile
30+
return Handler.from { request -> serve(request, root) }
31+
}
32+
33+
private fun serve(request: Request, root: File): Response? {
34+
val relative = request.header.target.asString(decode = true, remainingOnly = true).trimStart('/')
35+
if (relative.isEmpty() || relative.endsWith("/")) return null
36+
37+
val file = File(root, relative)
38+
// Containment guard: never serve outside the configured root (e.g. via "..").
39+
if (!file.canonicalFile.path.startsWith(root.path + File.separator)) return null
40+
41+
// Content-Type comes from the ORIGINAL name, not the .br/.gz variant.
42+
val contentType = relative.guessContentType()
43+
44+
val accepted = acceptedEncodings(request)
45+
for ((algorithm, extension) in PRECOMPRESSED) {
46+
if (algorithm in accepted) {
47+
val variant = File(root, relative + extension)
48+
if (variant.isFile) {
49+
return request.respond()
50+
.content(ResourceContent(Resource.fromFile(variant).build(), contentType, MemoryView.ofAscii(algorithm)))
51+
.build()
52+
}
53+
}
54+
}
55+
56+
if (!file.isFile) return null
57+
58+
return request.respond()
59+
.content(ResourceContent(Resource.fromFile(file).build(), contentType))
60+
.build()
61+
}
62+
63+
private fun acceptedEncodings(request: Request): Set<String> {
64+
val header = request.header.headers.getEntry("Accept-Encoding") ?: return emptySet()
65+
return header.split(',')
66+
.mapNotNull { part -> part.substringBefore(';').trim().lowercase().takeIf(String::isNotEmpty) }
67+
.toSet()
68+
}
69+
}

site/data/api-16-1024.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,32 @@
333333
"tpl_static": 0,
334334
"tpl_async_db": 206360
335335
},
336+
{
337+
"framework": "fishcake",
338+
"language": "Kotlin",
339+
"rps": 127641,
340+
"avg_latency": "5.94ms",
341+
"p99_latency": "34.90ms",
342+
"cpu": "1634.4%",
343+
"memory": "2.0GiB",
344+
"connections": 1024,
345+
"threads": 64,
346+
"duration": "5s",
347+
"pipeline": 1,
348+
"bandwidth": "646.20MB/s",
349+
"input_bw": "7.18MB/s",
350+
"reconnects": 382743,
351+
"status_2xx": 1914617,
352+
"status_3xx": 0,
353+
"status_4xx": 0,
354+
"status_5xx": 0,
355+
"tpl_baseline": 717886,
356+
"tpl_json": 718593,
357+
"tpl_db": 0,
358+
"tpl_upload": 0,
359+
"tpl_static": 0,
360+
"tpl_async_db": 478138
361+
},
336362
{
337363
"framework": "flask",
338364
"language": "Python",

site/data/api-4-256.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,32 @@
333333
"tpl_static": 0,
334334
"tpl_async_db": 74416
335335
},
336+
{
337+
"framework": "fishcake",
338+
"language": "Kotlin",
339+
"rps": 52583,
340+
"avg_latency": "3.13ms",
341+
"p99_latency": "19.40ms",
342+
"cpu": "401.3%",
343+
"memory": "607MiB",
344+
"connections": 256,
345+
"threads": 64,
346+
"duration": "5s",
347+
"pipeline": 1,
348+
"bandwidth": "266.17MB/s",
349+
"input_bw": "2.96MB/s",
350+
"reconnects": 157747,
351+
"status_2xx": 788753,
352+
"status_3xx": 0,
353+
"status_4xx": 0,
354+
"status_5xx": 0,
355+
"tpl_baseline": 295815,
356+
"tpl_json": 296006,
357+
"tpl_db": 0,
358+
"tpl_upload": 0,
359+
"tpl_static": 0,
360+
"tpl_async_db": 196931
361+
},
336362
{
337363
"framework": "flask",
338364
"language": "Python",

site/data/async-db-1024.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,26 @@
255255
"status_4xx": 0,
256256
"status_5xx": 0
257257
},
258+
{
259+
"framework": "fishcake",
260+
"language": "Kotlin",
261+
"rps": 211839,
262+
"avg_latency": "4.30ms",
263+
"p99_latency": "7.40ms",
264+
"cpu": "4568.6%",
265+
"memory": "5.8GiB",
266+
"connections": 1024,
267+
"threads": 64,
268+
"duration": "5s",
269+
"pipeline": 1,
270+
"bandwidth": "817.21MB/s",
271+
"input_bw": "14.14MB/s",
272+
"reconnects": 84711,
273+
"status_2xx": 2118391,
274+
"status_3xx": 0,
275+
"status_4xx": 0,
276+
"status_5xx": 0
277+
},
258278
{
259279
"framework": "flask",
260280
"language": "Python",

site/data/baseline-4096.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,26 @@
351351
"status_4xx": 0,
352352
"status_5xx": 0
353353
},
354+
{
355+
"framework": "fishcake",
356+
"language": "Kotlin",
357+
"rps": 1918113,
358+
"avg_latency": "1.88ms",
359+
"p99_latency": "10.20ms",
360+
"cpu": "6236.7%",
361+
"memory": "10.6GiB",
362+
"connections": 4096,
363+
"threads": 64,
364+
"duration": "5s",
365+
"pipeline": 1,
366+
"bandwidth": "234.08MB/s",
367+
"input_bw": "148.17MB/s",
368+
"reconnects": 0,
369+
"status_2xx": 9590569,
370+
"status_3xx": 0,
371+
"status_4xx": 0,
372+
"status_5xx": 0
373+
},
354374
{
355375
"framework": "flask",
356376
"language": "Python",

site/data/baseline-512.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -351,6 +351,26 @@
351351
"status_4xx": 0,
352352
"status_5xx": 0
353353
},
354+
{
355+
"framework": "fishcake",
356+
"language": "Kotlin",
357+
"rps": 1887468,
358+
"avg_latency": "270us",
359+
"p99_latency": "6.10ms",
360+
"cpu": "6477.9%",
361+
"memory": "9.5GiB",
362+
"connections": 512,
363+
"threads": 64,
364+
"duration": "5s",
365+
"pipeline": 1,
366+
"bandwidth": "230.35MB/s",
367+
"input_bw": "145.80MB/s",
368+
"reconnects": 0,
369+
"status_2xx": 9437344,
370+
"status_3xx": 0,
371+
"status_4xx": 0,
372+
"status_5xx": 0
373+
},
354374
{
355375
"framework": "flask",
356376
"language": "Python",

0 commit comments

Comments
 (0)