Remove flash-bench, docs, stray .class files; rewrite README for library

- 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 <noreply@anthropic.com>
This commit is contained in:
Relism
2026-03-19 22:39:33 +01:00
co-authored by Claude Sonnet 4.6
parent 9a30c5ab16
commit 5f0681a922
18 changed files with 76 additions and 1028 deletions
+69 -17
View File
@@ -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
## 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"));
```
3. **Execute the benchmark suite** (from a new terminal):
```bash
cd flash-bench
./benchmark.sh
## Handler styles
**Lambda:**
```java
server.get("/ping", (req, res) -> "pong");
```
### 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
**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
<repositories>
<repository>
<id>relism-releases</id>
<url>https://maven.relism.dev/releases</url>
</repository>
</repositories>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>flash</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
```
Binary file not shown.
-98
View File
@@ -1,98 +0,0 @@
# Request Body
`req.body()` returns a `RequestBody` — a lazy accessor that exposes the HTTP request body in
two mutually exclusive modes. Choose one per handler based on the payload size and how you
intend to consume it.
---
## Two modes
### `bytes()` — materialise
```java
byte[] body = req.body().bytes();
```
Reads the full body into a `byte[]` and caches the result. Safe to call multiple times — the
second call returns the same cached array, no I/O is performed again.
Use for:
- JSON parsing (`new String(body, UTF_8)` then parse)
- Small form data
- Any payload that must be inspected in full before responding
**Limit:** throws `IllegalStateException` if `Content-Length` exceeds `Integer.MAX_VALUE`
(~2 GB). For larger bodies use `stream()`.
**Chunked bodies:** reads until `ChunkedInputStream` signals EOF, then caches.
---
### `stream()` — zero-copy
```java
InputStream in = req.body().stream();
```
Returns a bounded `InputStream` without allocating the full body upfront.
Use for:
- Large file uploads
- Bodies passed directly to disk, a database, or another stream
- Multipart parsing (via `Multipart.of(req)` which calls this internally)
**Fixed-length bodies:** a `SequenceInputStream` of any pre-buffered header bytes (from the
`RequestParser` read buffer, already in memory) followed by a bounded view of the socket
stream. The small pre-buffered slice costs nothing extra.
**Chunked bodies:** returns the raw `ChunkedInputStream` directly. It de-chunks on the fly —
reads the hex chunk-size line, the data, and the trailing CRLF — and signals EOF at the end
of the last chunk, leaving the socket positioned correctly for the next keep-alive request.
**After `bytes()`:** if the body was already materialised, `stream()` returns a fresh
`ByteArrayInputStream` over the cached array. No I/O is performed.
---
## Mutual exclusivity
```java
// SAFE — pick one mode
byte[] b = req.body().bytes();
InputStream s = req.body().stream();
// UNSAFE — bytes() then stream() is safe (stream() wraps the cache)
// UNSAFE — stream() partially read, then bytes() → undefined results
```
The rule: if you call `stream()` first and partially read it, calling `bytes()` afterwards
will either throw (if `contentLength` > 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()` |
-190
View File
@@ -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<Part> files = mp.parts("files"); // all parts named "files"
List<Part> 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<Part> 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<Part> 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);
```
-104
View File
@@ -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<String> accepts = req.headers("Accept"); // all values in order
List<String> 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<String> 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.
-123
View File
@@ -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 |
-58
View File
@@ -1,58 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>flash-parent</artifactId>
<groupId>dev.relism</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>flash-bench</artifactId>
<build>
<finalName>flash-bench</finalName>
<plugins>
<plugin>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer>
<mainClass>dev.relism.bench.Main</mainClass>
</transformer>
<transformer />
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.44</version>
<scope>provided</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<ahc.version>2.12.3</ahc.version>
</properties>
</project>
-79
View File
@@ -1,79 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>dev.relism</groupId>
<artifactId>flash-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>flash-bench</artifactId>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<ahc.version>2.12.3</ahc.version>
</properties>
<dependencies>
<dependency>
<groupId>dev.relism</groupId>
<artifactId>flash</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate.orm</groupId>
<artifactId>hibernate-core</artifactId>
<version>6.6.4.Final</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>2.3.232</version>
</dependency>
</dependencies>
<build>
<finalName>flash-bench</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals><goal>shade</goal></goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>dev.relism.bench.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -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;
}
}
@@ -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<String, ContentType> 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<String, BakedAsset> 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);
}
}
@@ -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}
}
@@ -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]}
}
@@ -1,2 +0,0 @@
# Silence all logs during benchmarks to avoid I/O becoming the bottleneck
org.slf4j.simpleLogger.defaultLogLevel=off
-1
View File
@@ -1 +0,0 @@
Benchmark Mode Cnt Score Error Units
-1
View File
@@ -11,7 +11,6 @@
<modules>
<module>flash</module>
<module>flash-bench</module>
</modules>
<properties>