Skip to content

Commit 2b87a49

Browse files
committed
Optimize compile time
1 parent dd48f0a commit 2b87a49

10 files changed

Lines changed: 217 additions & 56 deletions

File tree

crates/vespera/src/serve.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,21 @@ impl Serve for axum::Router {
4747
axum::serve(listener, self).await
4848
}
4949
}
50+
51+
/// Lets a **stateless** merged app from `vespera!(merge = [...])` —
52+
/// which returns a [`crate::VesperaRouter<()>`] rather than a plain
53+
/// `axum::Router` — start with the same one-liner, without the user
54+
/// having to remember the `.with_state(())` finalizer first:
55+
///
56+
/// ```ignore
57+
/// vespera!(merge = [other::App]).serve("0.0.0.0:3000").await
58+
/// ```
59+
///
60+
/// Finalizing with `()` runs the deferred child-router merge and layer
61+
/// replay (see [`crate::VesperaRouter::with_state`]) before binding, so
62+
/// merged routes and layers are present when the listener starts.
63+
impl Serve for crate::VesperaRouter<()> {
64+
async fn serve(self, addr: impl ToSocketAddrs) -> io::Result<()> {
65+
self.with_state(()).serve(addr).await
66+
}
67+
}

crates/vespera_inprocess/src/dispatch.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,23 @@ async fn finish_direct_write(
470470
};
471471
let mut required = header_total;
472472

473+
// Fast overflow: when the body length is known exactly (a `Full` body /
474+
// explicit `Content-Length`) and the response cannot fit, report the
475+
// exact required size immediately instead of draining every frame just
476+
// to count bytes — this is the undersized-buffer retry path the pooled
477+
// JNI `dispatchDirect` takes. Unknown-length (streaming) bodies have no
478+
// exact hint and fall through to the drain loop unchanged.
479+
if let Some(exact) = body.size_hint().exact() {
480+
let required_u64 = u64::try_from(header_total)
481+
.unwrap_or(u64::MAX)
482+
.saturating_add(exact);
483+
if required_u64 > u64::try_from(out.len()).unwrap_or(u64::MAX) {
484+
return DirectWriteResult::Overflow(
485+
usize::try_from(required_u64).unwrap_or(usize::MAX),
486+
);
487+
}
488+
}
489+
473490
loop {
474491
match body.frame().await {
475492
Some(Ok(frame)) => {

crates/vespera_inprocess/src/wire.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,9 +555,16 @@ fn body_is_json(headers: &http::HeaderMap) -> bool {
555555
.get(http::header::CONTENT_TYPE)
556556
.and_then(|v| v.to_str().ok())
557557
.is_some_and(|s| {
558-
let mime = s.split(';').next().unwrap_or("").trim();
559-
mime.eq_ignore_ascii_case("application/json")
560-
|| (mime.len() >= 5 && mime[mime.len() - 5..].eq_ignore_ascii_case("+json"))
558+
// Any `application/json`, `*/json`, or `*+json` media type. The
559+
// trailing-5-byte suffix is compared on raw bytes (not a `str`
560+
// slice), so an exotic non-ASCII value can never panic on a
561+
// non-char-boundary index — and `/json` (e.g. `text/json`) now
562+
// hoists too, matching the documented contract.
563+
let mime = s.split(';').next().unwrap_or("").trim().as_bytes();
564+
mime.len() >= 5 && {
565+
let suffix = &mime[mime.len() - 5..];
566+
suffix.eq_ignore_ascii_case(b"/json") || suffix.eq_ignore_ascii_case(b"+json")
567+
}
561568
})
562569
}
563570

libs/vespera-bridge/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,9 +341,11 @@ vespera:
341341
| Small (≤ 256 KiB Content-Length) + unsafe (POST/PUT/PATCH/DELETE) | `SYNC` | ~3,200 |
342342
| Large or unknown-length body | `BIDIRECTIONAL_STREAMING` | ~24,100 |
343343

344-
The idempotency gate on DIRECT matters because a response that
344+
The safe-method gate on DIRECT matters because a response that
345345
overflows the pooled buffer (`vespera.direct.maxBufferBytes`, default
346346
4 MiB) is retried by default — which re-runs the Rust handler once.
347+
Safe methods are not intended to mutate server state, but the replayed
348+
response may still differ (for example timestamps or generated IDs).
347349
Set `vespera.bridge.direct-retry-on-overflow=false` to surface the
348350
overflow instead. SYNC never re-runs the handler (safe for POST), but
349351
buffers the full response on the JVM heap, which the request-size gate

libs/vespera-bridge/src/main/java/com/devfive/vespera/bridge/HttpMethods.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ private HttpMethods() {
1616

1717
/**
1818
* Whether {@code method} is idempotent per RFC 9110
19-
* (GET / HEAD / PUT / DELETE / OPTIONS). Idempotent requests are
20-
* safe to re-run, which the DIRECT dispatch path requires for its
21-
* response-overflow retry. {@code null} is treated as non-idempotent.
19+
* (GET / HEAD / PUT / DELETE / OPTIONS). Idempotent requests are not
20+
* necessarily replay-identical, so this is NOT the DIRECT overflow-retry
21+
* gate. {@code null} is treated as non-idempotent.
2222
*/
2323
static boolean isIdempotent(String method) {
2424
if (method == null) {
@@ -33,8 +33,8 @@ static boolean isIdempotent(String method) {
3333

3434
/**
3535
* Whether {@code method} is "safe" per RFC 9110 §9.2.1
36-
* (GET / HEAD / OPTIONS) — read-only, so re-running it yields the
37-
* <em>same response</em>, not merely the same server-state effect.
36+
* (GET / HEAD / OPTIONS) — not intended to mutate server state. Re-running
37+
* it can still yield a different response (timestamps, random IDs).
3838
*
3939
* <p>This is the correct gate for the DIRECT overflow retry, which
4040
* re-runs the handler: an idempotent-but-unsafe method (PUT / DELETE)

libs/vespera-bridge/src/main/java/com/devfive/vespera/bridge/SmartDispatchModeResolver.java

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@
4444
*/
4545
public class SmartDispatchModeResolver implements DispatchModeResolver {
4646

47+
private static final String CURRENT_THREAD_IS_VIRTUAL_ATTRIBUTE =
48+
SmartDispatchModeResolver.class.getName() + ".currentThreadIsVirtual";
49+
4750
/**
4851
* Default DIRECT request-size gate: 1 MiB (raised from 256 KiB,
4952
* measured 2026-06). Safe requests up to this size dispatch
@@ -101,6 +104,19 @@ public SmartDispatchModeResolver(long maxDirectBytes, long maxSyncBytes) {
101104

102105
@Override
103106
public DispatchMode resolveMode(HttpServletRequest request) {
107+
return resolveMode(request, null);
108+
}
109+
110+
static Boolean cachedCurrentThreadIsVirtual(HttpServletRequest request) {
111+
Object value = request.getAttribute(CURRENT_THREAD_IS_VIRTUAL_ATTRIBUTE);
112+
return value instanceof Boolean ? (Boolean) value : null;
113+
}
114+
115+
DispatchMode resolveMode(HttpServletRequest request, boolean currentThreadIsVirtual) {
116+
return resolveMode(request, Boolean.valueOf(currentThreadIsVirtual));
117+
}
118+
119+
private DispatchMode resolveMode(HttpServletRequest request, Boolean currentThreadIsVirtual) {
104120
long contentLength = request.getContentLengthLong();
105121
// Bodyless requests fit the direct buffer by definition even when
106122
// Content-Length is absent (the common shape of GET) — without this,
@@ -124,7 +140,11 @@ public DispatchMode resolveMode(HttpServletRequest request) {
124140
// SYNC (no off-heap pooling, no re-run) when small, but stream
125141
// above the SYNC gate — SYNC's heap buffering loses to streaming
126142
// for larger bodies, idempotent or not.
127-
if (VesperaBridge.currentThreadIsVirtual()) {
143+
boolean virtualThread = currentThreadIsVirtual != null
144+
? currentThreadIsVirtual.booleanValue()
145+
: VesperaBridge.currentThreadIsVirtual();
146+
request.setAttribute(CURRENT_THREAD_IS_VIRTUAL_ATTRIBUTE, Boolean.valueOf(virtualThread));
147+
if (virtualThread) {
128148
return syncSized(contentLength, bodyless)
129149
? DispatchMode.SYNC
130150
: DispatchMode.BIDIRECTIONAL_STREAMING;

libs/vespera-bridge/src/main/java/com/devfive/vespera/bridge/VesperaBridge.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,21 @@ public static ByteBuffer dispatchDirectPooled(
706706
appName, method, path, query, headers, body, retryOnOverflow);
707707
}
708708

709+
static ByteBuffer dispatchDirectPooled(
710+
String appName,
711+
String method,
712+
String path,
713+
String query,
714+
HeaderSource headers,
715+
byte[] body,
716+
boolean retryOnOverflow,
717+
boolean currentThreadIsVirtual) {
718+
requireRequestInputs(method, path);
719+
return VesperaDirectBufferPool.dispatchDirectPooled(
720+
appName, method, path, query, headers, body,
721+
retryOnOverflow, currentThreadIsVirtual);
722+
}
723+
709724
/**
710725
* Encode a request <strong>directly into</strong> {@code target}
711726
* starting at position 0 — no intermediate wire-sized {@code byte[]}.

libs/vespera-bridge/src/main/java/com/devfive/vespera/bridge/VesperaDirectBufferPool.java

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.devfive.vespera.bridge;
22

3-
import java.lang.ref.SoftReference;
43
import java.nio.ByteBuffer;
54
import java.util.Map;
65
import java.util.Objects;
@@ -74,17 +73,13 @@ private static int directMaxCapacity() {
7473
/**
7574
* Index 0 = request buffer, index 1 = response buffer.
7675
*
77-
* <p>Held through a {@link SoftReference} so the JVM can reclaim the
78-
* off-heap direct buffers under memory pressure — the
79-
* {@code DirectByteBuffer} Cleaner frees the native memory once the
80-
* soft reference is cleared — instead of pinning up to {@code 2 ×}
81-
* {@link #DIRECT_MAX_CAPACITY} per thread for the whole thread
82-
* lifetime. Under normal load the soft reference survives, so the
83-
* pooling benefit is preserved; see {@link #directPool()} for the
84-
* resolve + retention-cap logic.
76+
* <p>Held strongly per platform thread so baseline direct buffers stay
77+
* resident on the hot DIRECT path. Oversized buffers are shrunk
78+
* deterministically by {@link #recordDirectPoolUse(ByteBuffer[], int, int)}
79+
* after an idle streak instead of relying on heap-pressure-driven soft
80+
* reference clearing to manage off-heap memory.
8581
*/
86-
private static final ThreadLocal<SoftReference<ByteBuffer[]>> DIRECT_POOL =
87-
new ThreadLocal<>();
82+
private static final ThreadLocal<ByteBuffer[]> DIRECT_POOL = new ThreadLocal<>();
8883

8984
private static final int DIRECT_SHRINK_IDLE_DISPATCHES = 8;
9085
private static final ThreadLocal<Integer> DIRECT_UNDER_RETAIN_STREAK =
@@ -133,17 +128,15 @@ static boolean currentThreadIsVirtual() {
133128

134129
/**
135130
* Resolve the calling thread's pooled direct buffers, (re)allocating
136-
* a baseline pair when the {@link SoftReference} has been cleared
137-
* under memory pressure.
131+
* a baseline pair when none exists for this thread.
138132
*/
139133
private static ByteBuffer[] directPool() {
140-
SoftReference<ByteBuffer[]> ref = DIRECT_POOL.get();
141-
ByteBuffer[] pool = ref == null ? null : ref.get();
134+
ByteBuffer[] pool = DIRECT_POOL.get();
142135
if (pool == null) {
143136
pool = new ByteBuffer[] {
144137
ByteBuffer.allocateDirect(DIRECT_INITIAL_CAPACITY),
145138
ByteBuffer.allocateDirect(DIRECT_INITIAL_CAPACITY)};
146-
DIRECT_POOL.set(new SoftReference<>(pool));
139+
DIRECT_POOL.set(pool);
147140
DIRECT_UNDER_RETAIN_STREAK.set(0);
148141
return pool;
149142
}
@@ -160,13 +153,24 @@ private static void recordDirectPoolUse(ByteBuffer[] pool, int requestLen, int r
160153
DIRECT_UNDER_RETAIN_STREAK.set(streak);
161154
return;
162155
}
163-
DIRECT_POOL.remove();
156+
boolean requestGrown = pool[0].capacity() > DIRECT_INITIAL_CAPACITY;
157+
boolean responseGrown = pool[1].capacity() > DIRECT_INITIAL_CAPACITY;
158+
if (requestGrown) {
159+
pool[0] = ByteBuffer.allocateDirect(DIRECT_INITIAL_CAPACITY);
160+
}
161+
if (responseGrown) {
162+
pool[1] = ByteBuffer.allocateDirect(DIRECT_INITIAL_CAPACITY);
163+
}
164164
DIRECT_UNDER_RETAIN_STREAK.set(0);
165165
}
166166

167+
static void clearCurrentThreadBuffers() {
168+
DIRECT_POOL.remove();
169+
DIRECT_UNDER_RETAIN_STREAK.remove();
170+
}
171+
167172
static boolean directPoolPresentForTest() {
168-
SoftReference<ByteBuffer[]> ref = DIRECT_POOL.get();
169-
return ref != null && ref.get() != null;
173+
return DIRECT_POOL.get() != null;
170174
}
171175

172176
static ByteBuffer[] directPoolForTest() {
@@ -195,8 +199,13 @@ private static int grownCapacity(int needed) {
195199
* boolean)} for the full contract.
196200
*/
197201
static ByteBuffer dispatchDirectPooled(byte[] wireRequest, boolean retryOnOverflow) {
202+
return dispatchDirectPooled(wireRequest, retryOnOverflow, currentThreadIsVirtual());
203+
}
204+
205+
static ByteBuffer dispatchDirectPooled(
206+
byte[] wireRequest, boolean retryOnOverflow, boolean currentThreadIsVirtual) {
198207
Objects.requireNonNull(wireRequest, "wireRequest");
199-
if (currentThreadIsVirtual() || wireRequest.length > DIRECT_MAX_CAPACITY) {
208+
if (currentThreadIsVirtual || wireRequest.length > DIRECT_MAX_CAPACITY) {
200209
// Virtual thread: the per-thread direct buffer pool would
201210
// accumulate off-heap memory per vthread (ThreadLocal binds to
202211
// the vthread, not the carrier) — use the GC-managed heap path.
@@ -260,12 +269,26 @@ static ByteBuffer dispatchDirectPooled(
260269
HeaderSource headers,
261270
byte[] body,
262271
boolean retryOnOverflow) {
272+
return dispatchDirectPooled(
273+
appName, method, path, query, headers, body,
274+
retryOnOverflow, currentThreadIsVirtual());
275+
}
276+
277+
static ByteBuffer dispatchDirectPooled(
278+
String appName,
279+
String method,
280+
String path,
281+
String query,
282+
HeaderSource headers,
283+
byte[] body,
284+
boolean retryOnOverflow,
285+
boolean currentThreadIsVirtual) {
263286
byte[] bodyBytes = body != null ? body : VesperaWireCodec.EMPTY_BODY;
264287
ExposedByteArrayOutputStream hdr =
265288
VesperaWireCodec.fillHeaderJson(appName, method, path, query, headers);
266289
int headerLen = hdr.size();
267290
int total = VesperaWireCodec.wireTotalLength(headerLen, bodyBytes.length);
268-
if (currentThreadIsVirtual() || total > DIRECT_MAX_CAPACITY) {
291+
if (currentThreadIsVirtual || total > DIRECT_MAX_CAPACITY) {
269292
return ByteBuffer.wrap(
270293
VesperaBridge.dispatchBytes(
271294
VesperaWireCodec.assembleWire(hdr.backingArray(), headerLen, bodyBytes)))

0 commit comments

Comments
 (0)