11use std:: {
2- cell:: Cell ,
2+ cell:: { Cell , RefCell } ,
33 ffi:: c_void,
44 panic:: { AssertUnwindSafe , catch_unwind, resume_unwind} ,
55 ptr,
@@ -39,6 +39,104 @@ static RUNTIME_WORKER_THREADS: std::sync::OnceLock<Option<usize>> = std::sync::O
3939
4040thread_local ! {
4141 static ASYNC_DAEMON_ENV : Cell <* mut jni:: sys:: JNIEnv > = const { Cell :: new( ptr:: null_mut( ) ) } ;
42+ static STREAMING_PULL_BUFFER : RefCell <Option <CachedStreamingChunkBuffer >> = const { RefCell :: new( None ) } ;
43+ static STREAMING_PUSH_BUFFER : RefCell <Option <CachedStreamingChunkBuffer >> = const { RefCell :: new( None ) } ;
44+ }
45+
46+ type StreamingChunkBuffer = Global < JByteArray < ' static > > ;
47+
48+ #[ derive( Clone , Copy ) ]
49+ enum StreamingBufferRole {
50+ Pull ,
51+ Push ,
52+ }
53+
54+ impl StreamingBufferRole {
55+ fn with_cache < R > (
56+ self ,
57+ callback : impl FnOnce ( & RefCell < Option < CachedStreamingChunkBuffer > > ) -> R ,
58+ ) -> R {
59+ match self {
60+ Self :: Pull => STREAMING_PULL_BUFFER . with ( callback) ,
61+ Self :: Push => STREAMING_PUSH_BUFFER . with ( callback) ,
62+ }
63+ }
64+ }
65+
66+ struct CachedStreamingChunkBuffer {
67+ size : usize ,
68+ array : StreamingChunkBuffer ,
69+ checked_out : bool ,
70+ }
71+
72+ // Released explicitly only after the streaming future returns normally. If a
73+ // panic unwinds through a bidirectional dispatch while the request producer may
74+ // still be in `InputStream.read`, the cache stays checked out and future
75+ // dispatches allocate fresh buffers instead of aliasing the Java array.
76+ struct StreamingChunkBufferLease {
77+ role : StreamingBufferRole ,
78+ }
79+
80+ impl StreamingChunkBufferLease {
81+ const fn new ( role : StreamingBufferRole ) -> Self {
82+ Self { role }
83+ }
84+
85+ fn mark_reusable ( self ) {
86+ self . role . with_cache ( |cache| {
87+ if let Some ( cached) = cache. borrow_mut ( ) . as_mut ( ) {
88+ cached. checked_out = false ;
89+ }
90+ } ) ;
91+ }
92+ }
93+
94+ fn new_streaming_chunk_buffer (
95+ env : & mut jni:: Env < ' _ > ,
96+ size : usize ,
97+ ) -> jni:: errors:: Result < StreamingChunkBuffer > {
98+ let local = env. new_byte_array ( size) ?;
99+ env. new_global_ref ( & local)
100+ }
101+
102+ fn checkout_streaming_chunk_buffer (
103+ env : & mut jni:: Env < ' _ > ,
104+ role : StreamingBufferRole ,
105+ ) -> jni:: errors:: Result < ( StreamingChunkBuffer , Option < StreamingChunkBufferLease > ) > {
106+ let size = streaming_chunk_size ( ) ;
107+ role. with_cache ( |cache| {
108+ let mut slot = cache. borrow_mut ( ) ;
109+ let replace_cached = slot
110+ . as_ref ( )
111+ . is_none_or ( |cached| cached. size != size && !cached. checked_out ) ;
112+
113+ if replace_cached {
114+ * slot = Some ( CachedStreamingChunkBuffer {
115+ size,
116+ array : new_streaming_chunk_buffer ( env, size) ?,
117+ checked_out : false ,
118+ } ) ;
119+ }
120+
121+ let Some ( cached) = slot. as_mut ( ) else {
122+ return Ok ( ( new_streaming_chunk_buffer ( env, size) ?, None ) ) ;
123+ } ;
124+
125+ if cached. size != size || cached. checked_out {
126+ return Ok ( ( new_streaming_chunk_buffer ( env, size) ?, None ) ) ;
127+ }
128+
129+ let cached_array: & JByteArray < ' static > = cached. array . as_ref ( ) ;
130+ let dispatch_array = env. new_global_ref ( cached_array) ?;
131+ cached. checked_out = true ;
132+ Ok ( ( dispatch_array, Some ( StreamingChunkBufferLease :: new ( role) ) ) )
133+ } )
134+ }
135+
136+ fn mark_streaming_buffer_reusable ( lease : Option < StreamingChunkBufferLease > ) {
137+ if let Some ( lease) = lease {
138+ lease. mark_reusable ( ) ;
139+ }
42140}
43141
44142fn attach_async_daemon_thread ( jvm : & jni:: JavaVM ) -> jni:: errors:: Result < * mut jni:: sys:: JNIEnv > {
@@ -509,18 +607,23 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchStr
509607 let stream_global: Global < JObject < ' static > > = env. new_global_ref ( & output_stream) ?;
510608 let jvm = env. get_java_vm ( ) ?;
511609
512- // One reusable Java chunk buffer for the whole stream.
513- let push_buf_local = env. new_byte_array ( streaming_chunk_size ( ) ) ?;
514- let push_buf: Global < jni:: objects:: JByteArray < ' static > > =
515- env. new_global_ref ( & push_buf_local) ?;
610+ // One per-thread reusable Java chunk buffer for the whole stream.
611+ let ( push_buf, push_buf_lease) =
612+ checkout_streaming_chunk_buffer ( env, StreamingBufferRole :: Push ) ?;
516613
517614 let header_bytes = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
518615 RUNTIME . block_on ( vespera_inprocess:: dispatch_streaming_async (
519616 input,
520617 make_push_closure ( jvm, stream_global, push_buf) ,
521618 ) )
522- } ) )
523- . unwrap_or_else ( |_| vespera_inprocess:: error_wire ( 500 , "panic in Rust engine" ) ) ;
619+ } ) ) ;
620+ let header_bytes = header_bytes. map_or_else (
621+ |_| vespera_inprocess:: error_wire ( 500 , "panic in Rust engine" ) ,
622+ |header_bytes| {
623+ mark_streaming_buffer_reusable ( push_buf_lease) ;
624+ header_bytes
625+ } ,
626+ ) ;
524627
525628 Ok ( env. byte_array_from_slice ( & header_bytes) ?. into ( ) )
526629 } )
@@ -576,15 +679,18 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchFul
576679 let output_global: Global < JObject < ' static > > = env. new_global_ref ( & output_stream) ?;
577680 let jvm = env. get_java_vm ( ) ?;
578681
579- let chunk_size = streaming_chunk_size ( ) ;
580682 // Pull and push run concurrently on different threads, so each
581- // direction owns its own global-ref'd buffer.
582- let pull_buf_local = env. new_byte_array ( chunk_size) ?;
583- let pull_buf: Global < jni:: objects:: JByteArray < ' static > > =
584- env. new_global_ref ( & pull_buf_local) ?;
585- let push_buf_local = env. new_byte_array ( chunk_size) ?;
586- let push_buf: Global < jni:: objects:: JByteArray < ' static > > =
587- env. new_global_ref ( & push_buf_local) ?;
683+ // direction checks out its own per-thread cached buffer.
684+ let ( pull_buf, pull_buf_lease) =
685+ checkout_streaming_chunk_buffer ( env, StreamingBufferRole :: Pull ) ?;
686+ let ( push_buf, push_buf_lease) =
687+ match checkout_streaming_chunk_buffer ( env, StreamingBufferRole :: Push ) {
688+ Ok ( checked_out) => checked_out,
689+ Err ( err) => {
690+ mark_streaming_buffer_reusable ( pull_buf_lease) ;
691+ return Err ( err) ;
692+ }
693+ } ;
588694
589695 // Closures capture clones of the JavaVM and Globals;
590696 // both types are Send+Sync.
@@ -604,8 +710,15 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchFul
604710 // Runs on the tokio worker driving the dispatch.
605711 make_push_closure ( push_jvm, push_global, push_buf) ,
606712 ) )
607- } ) )
608- . unwrap_or_else ( |_| vespera_inprocess:: error_wire ( 500 , "panic in Rust engine" ) ) ;
713+ } ) ) ;
714+ let header_response = header_response. map_or_else (
715+ |_| vespera_inprocess:: error_wire ( 500 , "panic in Rust engine" ) ,
716+ |header_response| {
717+ mark_streaming_buffer_reusable ( pull_buf_lease) ;
718+ mark_streaming_buffer_reusable ( push_buf_lease) ;
719+ header_response
720+ } ,
721+ ) ;
609722
610723 Ok ( env. byte_array_from_slice ( & header_response) ?. into ( ) )
611724 } )
@@ -649,10 +762,9 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchStr
649762 let stream_global: Global < JObject < ' static > > = env. new_global_ref ( & output_stream) ?;
650763 let jvm = env. get_java_vm ( ) ?;
651764
652- // One reusable Java chunk buffer for the whole stream.
653- let push_buf_local = env. new_byte_array ( streaming_chunk_size ( ) ) ?;
654- let push_buf: Global < jni:: objects:: JByteArray < ' static > > =
655- env. new_global_ref ( & push_buf_local) ?;
765+ // One per-thread reusable Java chunk buffer for the whole stream.
766+ let ( push_buf, push_buf_lease) =
767+ checkout_streaming_chunk_buffer ( env, StreamingBufferRole :: Push ) ?;
656768
657769 // Panic safety: catch_unwind absorbs Rust panics so the
658770 // JVM never sees an unwinding stack across the FFI
@@ -664,7 +776,7 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchStr
664776 // rare — e.g. wire parse), the Java side will see a
665777 // dangling controller; document that follow-up callers
666778 // should set a timeout.
667- let _panic_result = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
779+ let panic_result = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
668780 let header_for_cb = header_global;
669781 let jvm_for_cb = jvm. clone ( ) ;
670782 let push = make_push_closure ( jvm, stream_global, push_buf) ;
@@ -680,6 +792,9 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchStr
680792 push,
681793 ) ) ;
682794 } ) ) ;
795+ if panic_result. is_ok ( ) {
796+ mark_streaming_buffer_reusable ( push_buf_lease) ;
797+ }
683798
684799 Ok ( ( ) )
685800 } ) ;
@@ -718,14 +833,17 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchFul
718833 let output_global: Global < JObject < ' static > > = env. new_global_ref ( & output_stream) ?;
719834 let jvm = env. get_java_vm ( ) ?;
720835
721- let chunk_size = streaming_chunk_size ( ) ;
722836 // Pull and push run concurrently on different threads.
723- let pull_buf_local = env. new_byte_array ( chunk_size) ?;
724- let pull_buf: Global < jni:: objects:: JByteArray < ' static > > =
725- env. new_global_ref ( & pull_buf_local) ?;
726- let push_buf_local = env. new_byte_array ( chunk_size) ?;
727- let push_buf: Global < jni:: objects:: JByteArray < ' static > > =
728- env. new_global_ref ( & push_buf_local) ?;
837+ let ( pull_buf, pull_buf_lease) =
838+ checkout_streaming_chunk_buffer ( env, StreamingBufferRole :: Pull ) ?;
839+ let ( push_buf, push_buf_lease) =
840+ match checkout_streaming_chunk_buffer ( env, StreamingBufferRole :: Push ) {
841+ Ok ( checked_out) => checked_out,
842+ Err ( err) => {
843+ mark_streaming_buffer_reusable ( pull_buf_lease) ;
844+ return Err ( err) ;
845+ }
846+ } ;
729847
730848 let pull_jvm = jvm. clone ( ) ;
731849 let pull_global = input_global;
@@ -737,7 +855,7 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchFul
737855 // See dispatchStreamingWithHeader: panic absorbed silently,
738856 // recovery semantics depend on which side of the header
739857 // callback the panic landed.
740- let _panic_result = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
858+ let panic_result = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
741859 RUNTIME . block_on (
742860 vespera_inprocess:: dispatch_bidirectional_streaming_with_header (
743861 header_input,
@@ -753,6 +871,10 @@ pub extern "system" fn Java_com_devfive_vespera_bridge_VesperaBridge_dispatchFul
753871 ) ,
754872 ) ;
755873 } ) ) ;
874+ if panic_result. is_ok ( ) {
875+ mark_streaming_buffer_reusable ( pull_buf_lease) ;
876+ mark_streaming_buffer_reusable ( push_buf_lease) ;
877+ }
756878
757879 Ok ( ( ) )
758880 } ) ;
0 commit comments