From 5f0681a922fec574f11dc908314762d745a9d8ab Mon Sep 17 00:00:00 2001 From: Relism Date: Thu, 19 Mar 2026 22:39:33 +0100 Subject: [PATCH] Remove flash-bench, docs, stray .class files; rewrite README for library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove flash-bench module from git and root pom.xml (kept locally) - Remove docs/ directory (lifecycle markdown files) - Remove stray compiled fpr-core .class files tracked by mistake - Rewrite README.md: concise library overview, quick-start, handler styles, Maven dependency — no wrk/benchmarking content Co-Authored-By: Claude Sonnet 4.6 --- README.md | 100 ++++++--- dev/relism/fpr/core/FastPathRouter.class | Bin 392 -> 0 bytes .../core/internal/runtime/FrozenRouter.class | Bin 8624 -> 0 bytes .../core/internal/runtime/RouteSearch.class | Bin 7420 -> 0 bytes .../core/internal/runtime/SegmentCursor.class | Bin 1503 -> 0 bytes docs/lifecycle/request/BODY.md | 98 --------- docs/lifecycle/request/MULTIPART.md | 190 ------------------ docs/lifecycle/request/REQUEST.md | 104 ---------- docs/lifecycle/response/RESPONSE.md | 123 ------------ flash-bench/dependency-reduced-pom.xml | 58 ------ flash-bench/pom.xml | 79 -------- .../src/main/java/dev/relism/bench/Main.java | 148 -------------- .../relism/bench/StaticResourceManager.java | 176 ---------------- flash-bench/src/main/resources/element.json | 12 -- flash-bench/src/main/resources/shells.json | 12 -- .../main/resources/simplelogger.properties | 2 - jmh-result.text | 1 - pom.xml | 1 - 18 files changed, 76 insertions(+), 1028 deletions(-) delete mode 100644 dev/relism/fpr/core/FastPathRouter.class delete mode 100644 dev/relism/fpr/core/internal/runtime/FrozenRouter.class delete mode 100644 dev/relism/fpr/core/internal/runtime/RouteSearch.class delete mode 100644 dev/relism/fpr/core/internal/runtime/SegmentCursor.class delete mode 100644 docs/lifecycle/request/BODY.md delete mode 100644 docs/lifecycle/request/MULTIPART.md delete mode 100644 docs/lifecycle/request/REQUEST.md delete mode 100644 docs/lifecycle/response/RESPONSE.md delete mode 100644 flash-bench/dependency-reduced-pom.xml delete mode 100644 flash-bench/pom.xml delete mode 100644 flash-bench/src/main/java/dev/relism/bench/Main.java delete mode 100644 flash-bench/src/main/java/dev/relism/bench/StaticResourceManager.java delete mode 100644 flash-bench/src/main/resources/element.json delete mode 100644 flash-bench/src/main/resources/shells.json delete mode 100644 flash-bench/src/main/resources/simplelogger.properties delete mode 100644 jmh-result.text diff --git a/README.md b/README.md index f7f25c1..c912e82 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,83 @@ -# Flash Benchmarking Module +# Flash -This module is designed for high-concurrency benchmarks using `wrk`. +A high-performance HTTP/1.1 server library for Java 21+, built for low-latency workloads. + +## Features + +- **Virtual threads** — one virtual thread per connection via `Executors.newVirtualThreadPerTaskExecutor()` +- **Zero-allocation hot path** — header buffer reused across keep-alive requests; `ByteView` slices avoid copies +- **Fast routing** — `fpr-core` compiles routes into a byte-level finite automaton at first request; path params via `PathParams` +- **Two-tier router** — mounted sub-routers matched by longest prefix, then `FastPathRouterImpl` fallback +- **Middleware chain** — composable `Middleware` lambdas fused at registration time (no per-request allocation) +- **Chunked transfer** — `ChunkedInputStream` de-chunks on the fly, keeps socket positioned for pipelining +- **Request body** — `bytes()` for full materialization or `stream()` for zero-copy streaming + +## Requirements -## Prerequisite - Java 21+ -- `wrk` installed (`sudo apt install wrk` on Linux/WSL) +- Maven 3.8+ -## Running the benchmarks +## Build -1. **Build the project** (from the root directory): - ```bash - mvn clean package -DskipTests - ``` - -2. **Start the benchmark server**: - ```bash - java -jar flash-bench/target/flash-bench-1.0-SNAPSHOT.jar - ``` - -3. **Execute the benchmark suite** (from a new terminal): - ```bash - cd flash-bench - ./benchmark.sh - ``` - -### Advanced: Random IDs Benchmarking -To test the speed of parameter extraction across millions of unique IDs: ```bash -wrk -t12 -c500 -d30s -s pipeline.lua http://127.0.0.1:8080 +mvn clean package -DskipTests +``` + +## Quick start + +```java +HttpServer server = new HttpServer( + HttpServerConfiguration.builder().port(8080).build() +); + +server.get("/hello", (req, res) -> "Hello, world!"); + +server.register(new MyHandler()); // @Route(path="/...", method=GET) + +server.start().thenRun(() -> System.out.println("Listening on :8080")); +``` + +## Handler styles + +**Lambda:** +```java +server.get("/ping", (req, res) -> "pong"); +``` + +**Class-based (`@Route`):** +```java +@Route(path = "/users/{id}", method = HttpMethod.GET) +public class GetUser extends RequestHandler { + @Override + public Object handle(Request req, Response res) { + String id = req.pathParam("id"); + return "{\"id\":\"" + id + "\"}"; + } +} +``` + +**Middleware:** +```java +Middleware cors = next -> (req, res) -> { + res.header("Access-Control-Allow-Origin", "*"); + return next.handle(req, res); +}; +server.register(new GetUser(), cors); +``` + +## Maven dependency + +```xml + + + relism-releases + https://maven.relism.dev/releases + + + + + dev.relism + flash + 1.0-SNAPSHOT + ``` diff --git a/dev/relism/fpr/core/FastPathRouter.class b/dev/relism/fpr/core/FastPathRouter.class deleted file mode 100644 index 73c729197d3bd16356ced18f5cca2e6b6915c65d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 392 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 a6y+Rqv$~W60kuVK6>>7UmZi;QnD#sIZ&1(x diff --git a/dev/relism/fpr/core/internal/runtime/RouteSearch.class b/dev/relism/fpr/core/internal/runtime/RouteSearch.class deleted file mode 100644 index 98f12035b4adfb5bdb6ca0f5ed89354194e26839..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7420 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 diff --git a/dev/relism/fpr/core/internal/runtime/SegmentCursor.class b/dev/relism/fpr/core/internal/runtime/SegmentCursor.class deleted file mode 100644 index dfcf53bee3a263ab03006fdf44b7decd67d8625d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1503 zcmaJ>%We}%6g}l<657NhaRPZbB$!}#APwQE$xKEB3CUn!5flklbnGHD*zM46hai@S z?*Oua1)Gt^8f6yq0rLm5=06zDtvGRrTFC0Uw{F#`I``aL{`cG8{{Xm-s{@E3o<_n# z5-El3b9c*i>aM@xJXn9OtD!>bSI_stWrcXLwAPP4^rtak;TSB1#hTu70$ul74d>}* z;8dG|c051Sf$!FxpzViVLp!T_qoMuqRy$}l1BImLZ?<`Gz7mCAc^&FCPrq6^hT|Aa zBWvLVh7=}$wwWyJ+TRGD8J1;XIbtD)QO>!bpiJzfg;OTx)wbpM2@8{$B6yvb-Xx_& zv0Pda^t6T3va_$HH&#P82qidc;fw@R3|6!+p|cj|B$W2`c37@$^N3r`npPOBc)q^h zZmjFzp}SsZWT?`ty7e_T@MPW%CcPfSvb3g$rHsZ>Ib$goZ67~292Cycb7+-B?7yJ?pc6xpqb$p|5=X1++nDDJIFAdA z2>3JNhQwWazJs(qzl)4Q2g7!}gE9M0^9}D{a$fvhjK`35J|JU1;xzF@_Hbj~j&(3& z%a#rbLLqJ_%%PwDagrG1FN+a2;{t|ogWhk>16z>{M6!*(%>IRKN z$@W-(l@KHE_8EReGXAAYAip2n!7SahOuNkABim#C>Hq%>g6sL(A^tu1^bxouE~}Dw zLA(M5NPIC2Me+r52F5u~AcHApn8zZf@jGTv!7Lu~1-wK7Z`k&4DSxjt@49(Qwkaes z>CzItOGNJ_+@LxMMsoIq$UP}^r-THDv0lwbDK%K*Br80?8V{1v2z8T-?7agH1kTAD zH7e%hVH){!@*b%zxHi 2 GB) or read only the remaining socket bytes into -the array, missing the already-consumed portion. - ---- - -## Checking the size - -```java -long size = req.body().contentLength(); -``` - -- **> 0** — fixed-length body; exact byte count -- **== 0** — empty body (`Content-Length: 0` or no body present) -- **== -1** — `Transfer-Encoding: chunked`; size is unknown upfront - -```java -boolean empty = req.body().isEmpty(); // true when contentLength == 0 -``` - ---- - -## Decision guide - -| Payload size | Encoding | Use | -|--------------------|-----------|----------------| -| < 2 GB, fully needed | any | `bytes()` | -| Any size, pass-through | any | `stream()` | -| Multipart form | any | `Multipart.of(req)` (uses `stream()` internally) | -| Size unknown | chunked | `stream()` | diff --git a/docs/lifecycle/request/MULTIPART.md b/docs/lifecycle/request/MULTIPART.md deleted file mode 100644 index 1e15294..0000000 --- a/docs/lifecycle/request/MULTIPART.md +++ /dev/null @@ -1,190 +0,0 @@ -# Multipart - -`Multipart` is a lazy streaming `multipart/form-data` parser. It reads from `req.body().stream()` -— the request body is **never fully materialised**. Text fields are buffered eagerly (they are -small by definition); file part bodies are exposed as zero-copy `InputStream`s backed directly -by the socket. - ---- - -## Creating a parser - -```java -Multipart mp = Multipart.of(req); -``` - -Throws `IllegalArgumentException` if the request is not `multipart/form-data` or the -`boundary` parameter is missing. Throws `IOException` if the initial stream read fails (the -parser skips the opening `--boundary\r\n` preamble on construction). - ---- - -## Reading text fields — `field(String name)` - -```java -String userId = mp.field("userId"); // null if absent or a file part -String width = mp.field("width"); -``` - -Scans forward through the stream until the named text field is found. Text parts encountered -along the way are buffered eagerly. File bodies encountered along the way are **drained -silently** (discarded without heap allocation). - -**Order-independent for text:** once a text field has been scanned, it is cached. Calling -`mp.field("userId")` after `mp.field("width")` will hit the cache if `userId` was encountered -first in the stream. - -Returns `null` if the field is absent or if the named part is a file (has a `filename` -attribute). Never returns file content. - ---- - -## Reading file parts — `file(String name)` - -```java -Part avatar = mp.file("avatar"); // null if absent or a text field -``` - -Scans forward until the named file part is found. Text parts encountered along the way are -buffered (accessible later via `field()`). Earlier file bodies encountered along the way are -drained silently. - -The returned `Part`'s stream is backed by the socket. **Consume it before calling any other -scan method** — calling `mp.field()`, `mp.file()`, or `mp.parts()` will drain the active -stream automatically, but any bytes already read are gone. - -Returns `null` if absent or if the named part is a text field. - ---- - -## Collecting multiple parts — `parts(String name)` and `parts()` - -```java -List files = mp.parts("files"); // all parts named "files" -List all = mp.parts(); // every part in declaration order -``` - -Forces a **full scan**. All file bodies are materialised into `byte[]` arrays — accept this -heap cost explicitly when calling these methods. Useful for bulk uploads where you need all -files before processing any of them. - -`parts()` must be called before any `file()` call if you need all file parts, since a previous -`file()` call may have consumed some of them from the stream. - ---- - -## The `Part` object - -Every part — whether text or file — is returned as a `Part`. - -### Metadata - -```java -part.name() // field name from Content-Disposition -part.filename() // original filename, null for text fields -part.contentType() // declared Content-Type, null if not present -part.isFile() // true when filename() != null -``` - -### Body access - -```java -// Zero-copy stream — the default -InputStream in = part.stream(); - -// Explicit materialisation — opt-in heap allocation -byte[] data = part.materialize(); - -// UTF-8 convenience — materialises if needed, result is cached -String text = part.text(); -``` - -#### `stream()` - -For **buffered parts** (text fields and `parts()` results): returns a fresh -`ByteArrayInputStream` over the cached `byte[]`. Repeatable, no I/O. - -For **streaming file parts** (from `file()`): returns the raw bounded socket `InputStream`. -**Read it once only**, and before requesting the next part. - -#### `materialize()` - -Reads the full part body into a `byte[]` and caches it. Safe to call multiple times on -buffered parts (returns the same array). For streaming file parts, triggers a full socket read -on the first call; subsequent calls return the cached array. - -#### `text()` - -Convenience over `materialize()`. Decodes the body as UTF-8. Result is cached. - ---- - -## Memory model - -| Operation | Heap cost | -|---|---| -| `Multipart.of(req)` | 8 KB window buffer + boundary bytes | -| `mp.field("x")` | body of the text field only (~bytes of the value) | -| `mp.file("f")` | none — body is a bounded socket stream | -| `part.stream()` on a file | none | -| `part.materialize()` on a file | full file size | -| `mp.parts()` | full body of every part | -| Draining a file body (implicit) | 8 KB reused drain buffer | - ---- - -## Ordering constraints and safe patterns - -### Pattern 1 — text fields + one file (most common) - -Text fields are accessible in any order. The file is streamed zero-copy. - -```java -Multipart mp = Multipart.of(req); -String userId = mp.field("userId"); // order doesn't matter for text -String label = mp.field("label"); -Part file = mp.file("file"); // stream it after fields - -Files.copy(file.stream(), destination, StandardCopyOption.REPLACE_EXISTING); -return res.body("saved " + file.filename()); -``` - -### Pattern 2 — echo large file (zero heap) - -```java -Part file = Multipart.of(req).file("file"); -return res.chunked(file.stream()); // socket-in → chunked-out, never touches heap -``` - -### Pattern 3 — bulk upload (accept the heap cost) - -```java -List files = Multipart.of(req).parts("files"); -for (Part f : files) { - Files.write(dir.resolve(f.filename()), f.materialize()); -} -``` - -### Pattern 4 — mixed order (file before text in the form) - -If the form declares the file before a text field and you need the text first, -`field()` will drain the file body silently to reach the text part. The file is then gone. -Either reorder the form fields or use `parts()` if you need both. - -```java -// Form order: file, then userId -// This works — file is drained silently, userId is buffered -String userId = mp.field("userId"); - -// But now mp.file("file") returns null — already drained -``` - -If you need both, use `parts()` upfront: - -```java -List all = mp.parts(); -Part file = all.stream().filter(Part::isFile).findFirst().orElse(null); -String userId = all.stream().filter(p -> "userId".equals(p.name())).findFirst() - .map(p -> { try { return p.text(); } catch (IOException e) { throw new UncheckedIOException(e); } }) - .orElse(null); -``` diff --git a/docs/lifecycle/request/REQUEST.md b/docs/lifecycle/request/REQUEST.md deleted file mode 100644 index b99d289..0000000 --- a/docs/lifecycle/request/REQUEST.md +++ /dev/null @@ -1,104 +0,0 @@ -# Request - -The `Request` object is the immutable view of an incoming HTTP/1.1 request passed to every -handler. It is constructed by `RequestParser`, path/query parameters are injected once by the -router, and then it is handed to your handler unchanged. - -```java -server.get("/users/{id}", (req, res) -> { ... }); -``` - ---- - -## Method and path - -```java -HttpMethod method = req.method(); // GET, POST, PUT, … -String path = req.path(); // "/users/42" — decoded UTF-8, no query string -``` - -`path()` allocates a `String` by decoding the underlying `ByteView`. It is a convenience for -handlers that need the raw path; the router itself never calls it (it works on the `ByteView` -directly for zero-alloc routing). - ---- - -## Headers - -```java -String ct = req.header("Content-Type"); // first value, or null -List accepts = req.headers("Accept"); // all values in order -List all = req.headers(); // every header value -``` - -Lookup is **case-insensitive**: `"content-type"` and `"Content-Type"` resolve to the same header. -`header()` returns `null` if the header is absent. `headers()` returns an empty list. - ---- - -## Path parameters - -Path parameters are declared in the route pattern with `{name}` syntax and injected by the -router before the handler runs. - -```java -server.get("/users/{id}/orders/{orderId}", (req, res) -> { - String id = req.param("id"); - String orderId = req.param("orderId"); -}); -``` - -`param()` returns `null` if the route has no such parameter or is not parametric at all. -It never throws. - ---- - -## Query parameters - -The query string is parsed lazily on the first `query()` or `queries()` call and cached for -the lifetime of the request. - -```java -// GET /search?q=flash&page=2&tag=java&tag=jvm - -String q = req.query("q"); // "flash" -String page = req.query("page"); // "2" -List tags = req.queries("tag"); // ["java", "jvm"] -String miss = req.query("absent"); // null -``` - -`query()` returns the first value for duplicate keys. `queries()` returns all values in -declaration order. Both return `null` / empty list for absent parameters, never throw. - ---- - -## Body - -See [BODY.md](BODY.md) for a full reference. Quick summary: - -```java -// Small payloads — materialise into byte[] -byte[] json = req.body().bytes(); - -// Large payloads — stream without heap allocation -InputStream in = req.body().stream(); - -// Check size before choosing -long size = req.body().contentLength(); // -1 if Transfer-Encoding: chunked -``` - -The two modes are **mutually exclusive per request**. Calling `stream()` after `bytes()` returns -a `ByteArrayInputStream` over the cached array; calling `bytes()` after `stream()` has been -partially read produces undefined results. - ---- - -## Keep-alive and drain - -After the response is fully written to the socket, the server calls `req.drain()` automatically -on keep-alive connections. This discards any unread body bytes so the socket is correctly -positioned for the next pipelined request. You never need to call this yourself. - -**Important:** drain happens *after* the response write, not immediately after the handler -returns. This matters for streaming responses that consume the request body on a separate thread -(e.g. a pipe-fed echo): by the time drain runs, the body has already been consumed. diff --git a/docs/lifecycle/response/RESPONSE.md b/docs/lifecycle/response/RESPONSE.md deleted file mode 100644 index 4ef0c7e..0000000 --- a/docs/lifecycle/response/RESPONSE.md +++ /dev/null @@ -1,123 +0,0 @@ -# Response - -The `Response` object is the mutable view of an outgoing HTTP/1.1 response. It is -pre-initialised by the server (`200 / text/plain`) and passed to every handler as `res`. -All mutating methods return `this` for fluent chaining. - -```java -server.get("/hello", (req, res) -> res.body("hello")); -server.post("/items", (req, res) -> res.status(HttpStatus.CREATED).body("created")); -``` - ---- - -## Status code - -```java -res.status(404) // int overload — phrase looked up via HttpStatus.bytesForCode() -res.status(HttpStatus.NOT_FOUND) // enum overload — pre-encoded bytes written directly, zero lookup -``` - -Prefer the `HttpStatus` overload on hot paths: the pre-encoded `byte[]` is written to the socket -without any lookup. The `int` overload falls back to an O(1) array lookup — still zero allocation, -just one extra indirection. - ---- - -## Content type - -```java -res.type(ContentType.JSON) // enum — pre-encoded bytes, zero allocation -res.type("application/octet-stream") // String — encoded once at call time -``` - -Same pattern as `status`: the `ContentType` enum carries pre-encoded bytes; the `String` overload -encodes once and stores the result. - ---- - -## Body modes - -Three mutually exclusive modes. The last one set wins. - -### Fixed body - -```java -res.body("hello") // String → UTF-8 bytes -res.body(new byte[]{...}) // raw bytes -``` - -Written with `Content-Length`. The byte array is stored as-is; no copy on the write path. - -### Known-length stream - -```java -res.stream(Files.newInputStream(path), Files.size(path)) -``` - -Written with `Content-Length`. The stream is drained directly to the socket in the write path — -zero intermediate buffer beyond the OS socket buffer. - -### Unknown-length stream (chunked) - -```java -res.chunked(source) -``` - -Written with `Transfer-Encoding: chunked`. The server reads 8 KB at a time from `source`, -writes each chunk with its hex-encoded length, and closes with the terminal `0\r\n\r\n`. -No `Content-Length` header is emitted. - -Use when the body length is not known upfront: generated content, pipe-fed streams, proxied -responses. - ---- - -## Custom headers - -```java -res.header("X-Request-Id", "abc123") - .header("Cache-Control", "no-store") -``` - -Each header is encoded as `"Name: Value\r\n"` bytes **at call time** and stored in a list. -On the write path the pre-encoded arrays are written directly — zero allocation per request. - ---- - -## Fluent chaining - -All mutating methods return `this`. A complete response in one expression: - -```java -return res.status(HttpStatus.CREATED) - .type(ContentType.JSON) - .header("X-Trace-Id", traceId) - .body(json); -``` - ---- - -## Body mode decision guide - -| Payload | Mode | Header emitted | -|----------------------------------|-------------------------------|-------------------------| -| Small / fully known | `body()` | `Content-Length` | -| File or stream with known size | `stream(is, length)` | `Content-Length` | -| Generated / piped / unknown size | `chunked(is)` | `Transfer-Encoding: chunked` | - ---- - -## Memory model - -| Call | Allocation | -|-----------------------------|-------------------------------------------------| -| `status(HttpStatus)` | none — writes pre-encoded `byte[]` from enum | -| `status(int)` | none — O(1) array lookup at write time | -| `type(ContentType)` | none — pre-encoded `byte[]` from enum | -| `type(String)` | encodes once at call time, stored as `byte[]` | -| `body(String)` | one UTF-8 encode at call time | -| `body(byte[])` | none — array stored by reference | -| `stream(is, len)` | none — stream drained directly to socket | -| `chunked(is)` | 8 KB read buffer per write call (stack-local) | -| `header(name, value)` | one UTF-8 encode at call time per header | diff --git a/flash-bench/dependency-reduced-pom.xml b/flash-bench/dependency-reduced-pom.xml deleted file mode 100644 index 01864f1..0000000 --- a/flash-bench/dependency-reduced-pom.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - flash-parent - dev.relism - 1.0-SNAPSHOT - - 4.0.0 - flash-bench - - flash-bench - - - maven-shade-plugin - 3.5.3 - - - package - - shade - - - - - dev.relism.bench.Main - - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - - - org.projectlombok - lombok - 1.18.44 - provided - - - - 21 - UTF-8 - 2.12.3 - - diff --git a/flash-bench/pom.xml b/flash-bench/pom.xml deleted file mode 100644 index 92a1e88..0000000 --- a/flash-bench/pom.xml +++ /dev/null @@ -1,79 +0,0 @@ - - 4.0.0 - - - dev.relism - flash-parent - 1.0-SNAPSHOT - - - flash-bench - - - 21 - UTF-8 - 2.12.3 - - - - - dev.relism - flash - - - org.slf4j - slf4j-simple - - - org.projectlombok - lombok - - - org.hibernate.orm - hibernate-core - 6.6.4.Final - - - com.h2database - h2 - 2.3.232 - - - - - flash-bench - - - org.apache.maven.plugins - maven-shade-plugin - 3.5.3 - - - package - shade - - - - dev.relism.bench.Main - - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - - - - - - - \ No newline at end of file diff --git a/flash-bench/src/main/java/dev/relism/bench/Main.java b/flash-bench/src/main/java/dev/relism/bench/Main.java deleted file mode 100644 index 8483367..0000000 --- a/flash-bench/src/main/java/dev/relism/bench/Main.java +++ /dev/null @@ -1,148 +0,0 @@ -package dev.relism.bench; - -import dev.relism.HttpServer; -import dev.relism.HttpServerConfiguration; -import dev.relism.http.ContentType; -import org.asynchttpclient.AsyncHttpClient; -import org.asynchttpclient.DefaultAsyncHttpClientConfig; -import org.asynchttpclient.Dsl; -import org.asynchttpclient.Request; -import org.asynchttpclient.RequestBuilder; - -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; -import java.util.Arrays; - -public class Main { - - // Lazy holder — Netty starts only on the first AHC request, not at class load. - // Routes like /plaintext that never call AHC are unaffected. - private static final class AhcHolder { - static final AsyncHttpClient AHC = Dsl.asyncHttpClient( - new DefaultAsyncHttpClientConfig.Builder() - .setMaxConnections(256) - .setMaxConnectionsPerHost(256) - .setKeepAlive(true) - .setPooledConnectionIdleTimeout(60_000) - .setConnectTimeout(5_000) - .setRequestTimeout(10_000) - .setIoThreadsCount(1) - .build() - ); - } - - // Pre-built request objects — URI parsing done once at startup. - private static final Request ELEMENT_REQ = new RequestBuilder("GET") - .setUrl("http://web-data-source/element.json").build(); - private static final Request SHELLS_REQ = new RequestBuilder("GET") - .setUrl("http://web-data-source/shells.json").build(); - - private static final byte[] PREFIX_SHELLS = "{\"shells\":".getBytes(StandardCharsets.UTF_8); - private static final byte[] SUFFIX = "}".getBytes(StandardCharsets.UTF_8); - - public static void main(String[] args) throws Exception { - int port = args.length > 0 ? Integer.parseInt(args[0]) : 3000; - - HttpServer server = new HttpServer( - HttpServerConfiguration.builder() - .port(port).host("0.0.0.0").build() - ); - - server.get("/api/v1/periodic-table/element", (req, res) -> - fetchAndFilter(req.query("symbol"), false)); - server.get("/api/v1/periodic-table/shells", (req, res) -> - fetchAndFilter(req.query("symbol"), true)); - server.get("/plaintext", (req, res) -> "Hello, World!"); - server.get("/json", (req, res) -> { - res.setContentType(ContentType.JSON); - return "{\"message\":\"Hello, World!\"}"; - }); - - Path frontendDist = Path.of("C:\\Users\\elorc\\Documents\\Coding\\web\\PixelDocs\\.vitepress\\dist"); - StaticResourceManager cdn = new StaticResourceManager(frontendDist, "/"); - - cdn.register(server); - - server.start().thenRun(() -> System.out.println("Flash Sharkbench online on port " + port)); - } - - private static dev.relism.models.Response fetchAndFilter(String symbol, boolean wrapShells) { - if (symbol == null) return new dev.relism.models.Response(400, "Missing symbol", ContentType.TEXT_PLAIN); - try { - // executeRequest() returns a ListenableFuture; .get() parks the calling - // virtual thread (cheap) while Netty's I/O thread handles the socket. - byte[] body = AhcHolder.AHC.executeRequest(wrapShells ? SHELLS_REQ : ELEMENT_REQ) - .get() - .getResponseBodyAsBytes(); - - byte[] value = extractJsonValue(body, symbol); - if (value == null) return new dev.relism.models.Response(404, "Not Found", ContentType.TEXT_PLAIN); - - if (wrapShells) { - byte[] wrapped = new byte[PREFIX_SHELLS.length + value.length + SUFFIX.length]; - System.arraycopy(PREFIX_SHELLS, 0, wrapped, 0, PREFIX_SHELLS.length); - System.arraycopy(value, 0, wrapped, PREFIX_SHELLS.length, value.length); - System.arraycopy(SUFFIX, 0, wrapped, PREFIX_SHELLS.length + value.length, SUFFIX.length); - return new dev.relism.models.Response(200, wrapped, ContentType.JSON); - } - - return new dev.relism.models.Response(200, value, ContentType.JSON); - } catch (Exception e) { - return new dev.relism.models.Response(500, "Internal Error", ContentType.TEXT_PLAIN); - } - } - - /** - * Extracts the JSON value for a given key from a flat JSON object. - * Zero Jackson allocations — pure byte scan. - */ - private static byte[] extractJsonValue(byte[] json, String key) { - byte[] keyBytes = ("\"" + key + "\":").getBytes(StandardCharsets.UTF_8); - - int pos = indexOf(json, keyBytes); - if (pos == -1) return null; - pos += keyBytes.length; - - while (pos < json.length && json[pos] == ' ') pos++; - if (pos >= json.length) return null; - - byte opener = json[pos]; - byte closer; - if (opener == '{') closer = '}'; - else if (opener == '[') closer = ']'; - else return null; - - int depth = 0; - boolean inString = false; - int start = pos; - - while (pos < json.length) { - byte b = json[pos]; - if (b == '"' && !isEscaped(json, pos)) inString = !inString; - if (!inString) { - if (b == opener) depth++; - else if (b == closer) { if (--depth == 0) { pos++; break; } } - } - pos++; - } - - return Arrays.copyOfRange(json, start, pos); - } - - private static int indexOf(byte[] haystack, byte[] needle) { - outer: - for (int i = 0; i <= haystack.length - needle.length; i++) { - for (int j = 0; j < needle.length; j++) { - if (haystack[i + j] != needle[j]) continue outer; - } - return i; - } - return -1; - } - - private static boolean isEscaped(byte[] data, int pos) { - int backslashes = 0; - while (--pos >= 0 && data[pos] == '\\') backslashes++; - return (backslashes & 1) == 1; - } -} \ No newline at end of file diff --git a/flash-bench/src/main/java/dev/relism/bench/StaticResourceManager.java b/flash-bench/src/main/java/dev/relism/bench/StaticResourceManager.java deleted file mode 100644 index 64d8efb..0000000 --- a/flash-bench/src/main/java/dev/relism/bench/StaticResourceManager.java +++ /dev/null @@ -1,176 +0,0 @@ -package dev.relism.bench; - -import dev.relism.HttpServer; -import dev.relism.http.ContentType; -import dev.relism.http.HttpStatus; -import lombok.extern.slf4j.Slf4j; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.zip.GZIPOutputStream; - -@Slf4j -public class StaticResourceManager { - private static final int GZIP_THRESHOLD = 1024; - private static final Map MIME_TYPES = new HashMap<>(); - - static { - MIME_TYPES.put("html", ContentType.TEXT_HTML); - MIME_TYPES.put("css", ContentType.TEXT_CSS); - MIME_TYPES.put("js", ContentType.TEXT_JAVASCRIPT); - MIME_TYPES.put("mjs", ContentType.TEXT_JAVASCRIPT); - MIME_TYPES.put("png", ContentType.IMAGE_PNG); - MIME_TYPES.put("jpg", ContentType.IMAGE_JPEG); - MIME_TYPES.put("jpeg", ContentType.IMAGE_JPEG); - MIME_TYPES.put("svg", ContentType.IMAGE_SVG); - MIME_TYPES.put("json", ContentType.JSON); - MIME_TYPES.put("ico", ContentType.BINARY); - MIME_TYPES.put("webp", ContentType.BINARY); - } - - public record BakedAsset(byte[] raw, byte[] gzipped, ContentType contentType, boolean immutable, String etag) {} - - private final Map cache = new ConcurrentHashMap<>(); - private final String mountPath; - private BakedAsset indexFallback; - - // Campi per le statistiche del report - private long totalRawSize = 0, totalCompressedSize = 0; - private int compressedCount = 0; - - public StaticResourceManager(Path rootPath, String mountPath) { - this.mountPath = normalizeMountPath(mountPath); - - long start = System.currentTimeMillis(); - bakeAll(rootPath); - long duration = System.currentTimeMillis() - start; - - // Richiamo del report qui - printReport(duration); - } - - private String normalizeMountPath(String path) { - String p = path.startsWith("/") ? path : "/" + path; - return p.endsWith("/") ? p.substring(0, p.length() - 1) : p; - } - - private void bakeAll(Path rootPath) { - if (!Files.exists(rootPath)) return; - try (var stream = Files.walk(rootPath)) { - stream.filter(Files::isRegularFile).forEach(file -> { - try { - String relative = rootPath.relativize(file).toString().replace("\\", "/"); - byte[] raw = Files.readAllBytes(file); - byte[] gzipped = compressIfBeneficial(raw, relative); - String etag = "\"" + Integer.toHexString(Arrays.hashCode(raw)) + "-" + raw.length + "\""; - boolean isImmutable = relative.matches(".*\\.[a-f0-9]{8,}\\..*"); - - BakedAsset asset = new BakedAsset(raw, gzipped, resolveContentType(relative), isImmutable, etag); - cache.put(relative, asset); - - // Aggiornamento statistiche - totalRawSize += raw.length; - totalCompressedSize += (gzipped != null) ? gzipped.length : raw.length; - if (gzipped != null) compressedCount++; - if ("index.html".equals(relative)) indexFallback = asset; - } catch (IOException ignored) {} - }); - } catch (IOException ignored) {} - } - - // --- IL METODO PRINTREPORT --- - private void printReport(long duration) { - double saved = totalRawSize > 0 ? (1.0 - (double)totalCompressedSize / totalRawSize) * 100 : 0; - long immutableCount = cache.values().stream().filter(BakedAsset::immutable).count(); - - System.out.println("\n" + "=".repeat(45)); - System.out.printf("🚀 FLASH ASSET BAKE COMPLETE [%dms]\n", duration); - System.out.println("-".repeat(45)); - System.out.printf("📦 Total Assets: %d\n", cache.size()); - System.out.printf("🤐 Gzipped (On-Disk): %d\n", compressedCount); - System.out.printf("💾 RAM Usage: %.2f MB\n", (double)totalRawSize / (1024 * 1024)); - System.out.printf("📉 Bandwidth Saving: %.1f%%\n", saved); - System.out.println("-".repeat(45)); - System.out.println("🛡️ STRATEGIES ENABLED:"); - System.out.println(" • ETag Validation: [ACTIVE] (304 Not Modified)"); - System.out.printf(" • Immutable Assets: [%d files] (Cache: 1 year)\n", immutableCount); - System.out.println(" • SPA Fallback: [ENABLED] (Route -> index.html)"); - System.out.println("=".repeat(45) + "\n"); - } - - public void register(HttpServer server) { - server.get(mountPath + "/**", (req, res) -> { - String path = req.getRequestLine().getPath().toString(); - String subPath = path.substring(mountPath.length()).replaceFirst("^/", ""); - - BakedAsset asset = subPath.isEmpty() ? indexFallback : cache.get(subPath); - - if (asset == null) { - if (!subPath.contains(".") && indexFallback != null) { - return serve(res, indexFallback, req); - } - return res.status(HttpStatus.NOT_FOUND); - } - return serve(res, asset, req); - }); - } - - private Object serve(dev.relism.models.Response res, BakedAsset asset, dev.relism.models.Request req) { - // Caching condizionale (ETag) - String ifNoneMatch = req.header("If-None-Match"); - if (asset.etag().equals(ifNoneMatch)) { - return res.status(HttpStatus.NOT_MODIFIED); - } - - res.type(asset.contentType()); - res.header("ETag", asset.etag()); - - // Cache-Control Strategy - if (asset.contentType() == ContentType.TEXT_HTML) { - res.header("Cache-Control", "no-cache, must-revalidate"); - } else if (asset.immutable()) { - res.header("Cache-Control", "public, max-age=31536000, immutable"); - } else { - res.header("Cache-Control", "public, max-age=3600"); - } - - if (asset.gzipped() != null && acceptsGzip(req)) { - res.header("Content-Encoding", "gzip"); - res.header("Vary", "Accept-Encoding"); - return res.body(asset.gzipped()); - } - return res.body(asset.raw()); - } - - private boolean acceptsGzip(dev.relism.models.Request req) { - String enc = req.header("Accept-Encoding"); - return enc != null && enc.contains("gzip"); - } - - private byte[] compressIfBeneficial(byte[] data, String name) { - if (data.length < GZIP_THRESHOLD || isAlreadyCompressed(name)) return null; - try (var baos = new ByteArrayOutputStream(); var gzip = new GZIPOutputStream(baos)) { - gzip.write(data); - gzip.finish(); - byte[] compressed = baos.toByteArray(); - return compressed.length < (data.length * 0.9) ? compressed : null; - } catch (IOException e) { return null; } - } - - private boolean isAlreadyCompressed(String n) { - n = n.toLowerCase(); - return n.endsWith(".png") || n.endsWith(".jpg") || n.endsWith(".jpeg") || n.endsWith(".ico") || n.endsWith(".webp"); - } - - private ContentType resolveContentType(String filename) { - int dot = filename.lastIndexOf('.'); - String ext = (dot > 0) ? filename.substring(dot + 1).toLowerCase() : ""; - return MIME_TYPES.getOrDefault(ext, ContentType.BINARY); - } -} \ No newline at end of file diff --git a/flash-bench/src/main/resources/element.json b/flash-bench/src/main/resources/element.json deleted file mode 100644 index 0353f15..0000000 --- a/flash-bench/src/main/resources/element.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "H": {"name": "Hydrogen", "number": 1, "group": 1}, - "He": {"name": "Helium", "number": 2, "group": 18}, - "Li": {"name": "Lithium", "number": 3, "group": 1}, - "Be": {"name": "Beryllium", "number": 4, "group": 2}, - "B": {"name": "Boron", "number": 5, "group": 13}, - "C": {"name": "Carbon", "number": 6, "group": 14}, - "N": {"name": "Nitrogen", "number": 7, "group": 15}, - "O": {"name": "Oxygen", "number": 8, "group": 16}, - "F": {"name": "Fluorine", "number": 9, "group": 17}, - "Ne": {"name": "Neon", "number": 10, "group": 18} -} diff --git a/flash-bench/src/main/resources/shells.json b/flash-bench/src/main/resources/shells.json deleted file mode 100644 index e645b57..0000000 --- a/flash-bench/src/main/resources/shells.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "H": {"shells": [1]}, - "He": {"shells": [2]}, - "Li": {"shells": [2, 1]}, - "Be": {"shells": [2, 2]}, - "B": {"shells": [2, 3]}, - "C": {"shells": [2, 4]}, - "N": {"shells": [2, 5]}, - "O": {"shells": [2, 6]}, - "F": {"shells": [2, 7]}, - "Ne": {"shells": [2, 8]} -} diff --git a/flash-bench/src/main/resources/simplelogger.properties b/flash-bench/src/main/resources/simplelogger.properties deleted file mode 100644 index 263f2a3..0000000 --- a/flash-bench/src/main/resources/simplelogger.properties +++ /dev/null @@ -1,2 +0,0 @@ -# Silence all logs during benchmarks to avoid I/O becoming the bottleneck -org.slf4j.simpleLogger.defaultLogLevel=off diff --git a/jmh-result.text b/jmh-result.text deleted file mode 100644 index 48aabd1..0000000 --- a/jmh-result.text +++ /dev/null @@ -1 +0,0 @@ -Benchmark Mode Cnt Score Error Units diff --git a/pom.xml b/pom.xml index be9c68e..61f2318 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,6 @@ flash - flash-bench