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:
co-authored by
Claude Sonnet 4.6
parent
9a30c5ab16
commit
5f0681a922
@@ -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+
|
- 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
|
```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
|
||||||
|
<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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()` |
|
|
||||||
@@ -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);
|
|
||||||
```
|
|
||||||
@@ -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.
|
|
||||||
@@ -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 |
|
|
||||||
@@ -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>
|
|
||||||
@@ -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 +0,0 @@
|
|||||||
Benchmark Mode Cnt Score Error Units
|
|
||||||
Reference in New Issue
Block a user