11package com .devfive .vespera .bridge ;
22
3- import java .lang .ref .SoftReference ;
43import java .nio .ByteBuffer ;
54import java .util .Map ;
65import 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