From 8dcfe0a67611040f0da80ce7f5ed7708839b7d19 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Tue, 30 Jun 2026 15:59:29 -0700 Subject: [PATCH 1/3] docs(CLAUDE): add Performance hot-path rules --- CLAUDE.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 2ec5f97e7..a75eab201 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,6 +40,17 @@ Every bound that protects a shared resource — memory/heap, CPU/wall-clock, fd/ - **Clear, typed error on breach.** Fail with a typed error that names the limit and the observed-vs-cap value **with units**, plus how to raise it: `" exceeded: > (raise via limits.)"`. Map consistently — errno for kernel limits (but attach the limit name; no bare/opaque `EAGAIN`), `ExecutionAbortReason` for runtime kills, `SidecarError`/codec errors for config/protocol. No generic "invalid"/silent failure that hides which limit fired. - **No catastrophic reaction to transient fullness.** A full bounded queue/buffer applies **backpressure** (block the producer until the consumer drains) or returns the named error — never silently drop, silently evict, destroy the session, or crash the process. Raising a capacity is not a fix by itself; the warning + typed error must exist first. See PR #123 (event channel + stdout frame queue) for the reference pattern; audit every other channel/`VecDeque`/buffer against it. +## Performance + +- **No expensive objects per-call.** Build once, reuse via a pool/persistent worker. Never construct per-operation: Tokio runtime, OS thread, V8 isolate/snapshot, DNS resolver, HTTP client, connection pool. Construct-then-teardown every call IS the bug. +- **No serialize→deserialize in-process.** Pass the typed struct directly; wire encoding is for the wire only. Don't encode a frame to bytes only to re-parse it into a command. +- **No whole-buffer copies per I/O.** Use chunked `Vec` + `extend_from_slice`, not byte-by-byte fills; move/`Arc`/slice payloads — never clone a record that carries its full buffer on each read/write. +- **No per-call allocs/locks/clones** on the sync hot path. +- **Avoid polling**, prefer readiness/event-driven. But a read-probe can be load-bearing for protocol correctness — measure before removing one, and keep its semantic test. +- **No baseline, no merge.** Capture native + unoptimized numbers BEFORE touching code, gate every change on a measured before/after delta, and keep it measure-gated. +- **Revert no-wins.** A change with a flat or negative delta is a liability, not a win. +- **Perf must not regress correctness.** Respect existing caps/bounds and land the regression test in the same change as the optimization. + ## Project Boundaries - Keep the secure-exec runtime Agent OS-agnostic: no ACP, sessions, `agentos-protocol`, `agentos-client`, or `agentos-sidecar` dependencies in runtime code. From 12fb56615f9a4236cb529456e09df635b65ede23 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 1 Jul 2026 13:12:22 -0700 Subject: [PATCH 2/3] perf(v8-runtime): pass V8-serialized args through without double copy --- crates/v8-runtime/src/bridge.rs | 102 +++----------------- crates/v8-runtime/src/execution.rs | 147 ----------------------------- crates/v8-runtime/src/session.rs | 11 +-- crates/v8-runtime/src/snapshot.rs | 13 +-- 4 files changed, 17 insertions(+), 256 deletions(-) diff --git a/crates/v8-runtime/src/bridge.rs b/crates/v8-runtime/src/bridge.rs index 5701bcb16..4fbae2cf9 100644 --- a/crates/v8-runtime/src/bridge.rs +++ b/crates/v8-runtime/src/bridge.rs @@ -3,7 +3,7 @@ use std::cell::{Cell, RefCell}; use std::collections::{HashMap, HashSet}; use std::ffi::c_void; -use std::mem::{self, MaybeUninit}; +use std::mem::MaybeUninit; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::OnceLock; @@ -123,22 +123,6 @@ pub fn serialize_v8_wire_value( Ok(serializer.release()) } -/// Serialize a V8 value into a pre-allocated buffer. -/// -/// The buffer is cleared (not deallocated) before use, preserving capacity. -/// V8's serializer allocates internally; the result is copied into the buffer -/// so the buffer grows to high-water mark across calls. -pub fn serialize_v8_value_into( - scope: &mut v8::HandleScope, - value: v8::Local, - buf: &mut Vec, -) -> Result<(), String> { - let released = serialize_v8_value(scope, value)?; - buf.clear(); - buf.extend_from_slice(&released); - Ok(()) -} - /// Deserialize bytes back to a V8 value using V8's built-in ValueDeserializer. /// The bytes must have been produced by serialize_v8_value() or node:v8.serialize(). pub fn deserialize_v8_value<'s>( @@ -648,32 +632,10 @@ pub fn deserialize_cbor_value<'s>( cbor_to_v8(scope, &cbor_val) } -/// Pre-allocated serialization buffers reused across bridge calls within a session. -/// Grows to high-water mark; cleared (not deallocated) between calls via buf.clear(). -pub struct SessionBuffers { - /// Buffer for V8 ValueSerializer output (args serialization) - pub ser_buf: Vec, -} - -impl SessionBuffers { - pub fn new() -> Self { - SessionBuffers { - ser_buf: Vec::with_capacity(256), - } - } -} - -impl Default for SessionBuffers { - fn default() -> Self { - Self::new() - } -} - /// Data attached to each sync bridge function via v8::External. /// BridgeFnStore keeps these heap allocations alive for the session. struct SyncBridgeFnData { ctx: *const BridgeCallContext, - buffers: *const RefCell, method: String, } @@ -689,7 +651,6 @@ pub struct BridgeFnStore { struct AsyncBridgeFnData { ctx: *const BridgeCallContext, pending: *const PendingPromises, - buffers: *const RefCell, method: String, } @@ -1763,7 +1724,6 @@ fn handle_local_bridge_call<'s>( pub fn register_sync_bridge_fns( scope: &mut v8::HandleScope, ctx: *const BridgeCallContext, - buffers: *const RefCell, methods: &[&str], ) -> BridgeFnStore { let context = scope.get_current_context(); @@ -1773,7 +1733,6 @@ pub fn register_sync_bridge_fns( for &method_name in methods { let boxed = Box::new(SyncBridgeFnData { ctx, - buffers, method: method_name.to_string(), }); // Pointer to heap allocation — stable while Box exists in data vec @@ -1815,7 +1774,6 @@ fn sync_bridge_callback<'s>( // SAFETY: pointer is valid while BridgeFnStore is alive (same session lifetime) let data = unsafe { &*(external.value() as *const SyncBridgeFnData) }; let ctx = unsafe { &*data.ctx }; - let buffers = unsafe { &*data.buffers }; { let tc = &mut v8::TryCatch::new(scope); @@ -1842,8 +1800,8 @@ fn sync_bridge_callback<'s>( } } - // Serialize V8 arguments into reusable buffer (avoids per-call allocation) - let encoded_args = match serialize_v8_args_with_session_buffer(scope, &args, buffers) { + // Serialize V8 arguments using the Vec released by V8's serializer directly. + let encoded_args = match serialize_v8_args(scope, &args) { Ok(encoded_args) => encoded_args, Err(err) => { let msg = @@ -1897,7 +1855,6 @@ pub fn register_async_bridge_fns( scope: &mut v8::HandleScope, ctx: *const BridgeCallContext, pending: *const PendingPromises, - buffers: *const RefCell, methods: &[&str], ) -> AsyncBridgeFnStore { let context = scope.get_current_context(); @@ -1908,7 +1865,6 @@ pub fn register_async_bridge_fns( let boxed = Box::new(AsyncBridgeFnData { ctx, pending, - buffers, method: method_name.to_string(), }); // Pointer to heap allocation — stable while Box exists in data vec @@ -1962,26 +1918,6 @@ fn build_bridge_apply_wrapper<'s>( .and_then(|value| v8::Local::::try_from(value).ok()) } -fn serialize_v8_args_with_session_buffer( - scope: &mut v8::HandleScope, - args: &v8::FunctionCallbackArguments, - buffers: &RefCell, -) -> Result, String> { - let mut ser_buf = { - let mut bufs = buffers.borrow_mut(); - mem::take(&mut bufs.ser_buf) - }; - - let result = serialize_v8_args_into(scope, args, &mut ser_buf).map(|()| ser_buf.clone()); - - { - let mut bufs = buffers.borrow_mut(); - bufs.ser_buf = ser_buf; - } - - result -} - fn reject_promise_with_error( scope: &mut v8::HandleScope, resolver: v8::Local, @@ -2020,7 +1956,6 @@ fn async_bridge_callback( let data = unsafe { &*(external.value() as *const AsyncBridgeFnData) }; let ctx = unsafe { &*data.ctx }; let pending = unsafe { &*data.pending }; - let buffers = unsafe { &*data.buffers }; // Create PromiseResolver let resolver = match v8::PromiseResolver::new(scope) { @@ -2050,8 +1985,8 @@ fn async_bridge_callback( } }; - // Serialize V8 arguments into reusable buffer (avoids per-call allocation) - let encoded_args = match serialize_v8_args_with_session_buffer(scope, &args, buffers) { + // Serialize V8 arguments using the Vec released by V8's serializer directly. + let encoded_args = match serialize_v8_args(scope, &args) { Ok(encoded_args) => encoded_args, Err(err) => { let msg = @@ -2081,7 +2016,7 @@ fn async_bridge_callback( /// Replace stub bridge functions on a snapshot-restored context with real /// session-local bridge functions. Overwrites the 38 stub globals with -/// functions backed by session-local BridgeCallContext and SessionBuffers. +/// functions backed by session-local BridgeCallContext. /// /// Returns (BridgeFnStore, AsyncBridgeFnStore) that must be kept alive /// for the lifetime of the V8 context. @@ -2089,7 +2024,6 @@ pub fn replace_bridge_fns( scope: &mut v8::HandleScope, ctx: *const BridgeCallContext, pending: *const PendingPromises, - buffers: *const RefCell, sync_fns: &[&str], async_fns: &[&str], ) -> (BridgeFnStore, AsyncBridgeFnStore) { @@ -2100,8 +2034,8 @@ pub fn replace_bridge_fns( // accumulate across executions toward `MAX_VM_CONTEXTS`. The session should // also hold a `VmContextRegistryGuard` to evict its own slots at teardown. reset_vm_context_registry(); - let sync_store = register_sync_bridge_fns(scope, ctx, buffers, sync_fns); - let async_store = register_async_bridge_fns(scope, ctx, pending, buffers, async_fns); + let sync_store = register_sync_bridge_fns(scope, ctx, sync_fns); + let async_store = register_async_bridge_fns(scope, ctx, pending, async_fns); (sync_store, async_store) } @@ -2140,19 +2074,17 @@ pub fn register_stub_bridge_fns( } } -/// Serialize V8 function arguments into a pre-allocated buffer. -/// The buffer is cleared and reused across calls (grows to high-water mark). -fn serialize_v8_args_into( +/// Serialize V8 function arguments as an array. +fn serialize_v8_args( scope: &mut v8::HandleScope, args: &v8::FunctionCallbackArguments, - buf: &mut Vec, -) -> Result<(), String> { +) -> Result, String> { let count = args.length(); let array = v8::Array::new(scope, count); for i in 0..count { array.set_index(scope, i as u32, args.get(i)); } - serialize_v8_value_into(scope, array.into(), buf) + serialize_v8_value(scope, array.into()) } /// Resolve or reject a pending async bridge promise by call_id. @@ -2241,13 +2173,12 @@ mod tests { fill_vm_context_registry_for_test, register_async_bridge_fns, register_sync_bridge_fns, reserve_vm_context_slot, reset_vm_context_registry, serialize_cbor_value, vm_context_capacity_error, vm_context_registry_len_for_test, PendingPromises, - SessionBuffers, VmContextRegistryGuard, MAX_CBOR_BRIDGE_CONTAINER_ITEMS, - MAX_CBOR_BRIDGE_DEPTH, MAX_PENDING_PROMISES, MAX_VM_CONTEXTS, + VmContextRegistryGuard, MAX_CBOR_BRIDGE_CONTAINER_ITEMS, MAX_CBOR_BRIDGE_DEPTH, + MAX_PENDING_PROMISES, MAX_VM_CONTEXTS, }; use crate::host_call::BridgeCallContext; use crate::ipc_binary::{self, BinaryFrame}; use crate::isolate; - use std::cell::RefCell; use std::io::{Cursor, Write}; use std::process::Command; use std::sync::{Arc, Mutex}; @@ -2392,11 +2323,9 @@ mod tests { Box::new(Cursor::new(Vec::new())), String::from("test-session"), ); - let session_buffers = RefCell::new(SessionBuffers::new()); let _bridge_fns = register_sync_bridge_fns( scope, &bridge_ctx as *const BridgeCallContext, - &session_buffers as *const RefCell, &["_vmCreateContext"], ); @@ -2507,7 +2436,6 @@ mod tests { scope, &async_bridge_ctx as *const BridgeCallContext, &async_pending as *const PendingPromises, - &session_buffers as *const RefCell, &["_asyncFn"], ); let source = format!( @@ -2557,7 +2485,6 @@ mod tests { scope, &reentrant_bridge_ctx as *const BridgeCallContext, &reentrant_pending as *const PendingPromises, - &session_buffers as *const RefCell, &["_asyncFn"], ); let source = format!( @@ -2617,7 +2544,6 @@ mod tests { scope, &buffer_reentry_bridge_ctx as *const BridgeCallContext, &buffer_reentry_pending as *const PendingPromises, - &session_buffers as *const RefCell, &["_asyncFn"], ); let source = r#" diff --git a/crates/v8-runtime/src/execution.rs b/crates/v8-runtime/src/execution.rs index faa848a54..cf7ececa6 100644 --- a/crates/v8-runtime/src/execution.rs +++ b/crates/v8-runtime/src/execution.rs @@ -4126,7 +4126,6 @@ export const file = new File([], "empty.txt"); "test-session".into(), ); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4135,7 +4134,6 @@ export const file = new File([], "empty.txt"); _fn_store = bridge::register_sync_bridge_fns( scope, &bridge_ctx as *const BridgeCallContext, - &session_buffers as *const std::cell::RefCell, &["_testBridge"], ); } @@ -4166,7 +4164,6 @@ export const file = new File([], "empty.txt"); "test-session".into(), ); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4175,7 +4172,6 @@ export const file = new File([], "empty.txt"); _fn_store = bridge::register_sync_bridge_fns( scope, &bridge_ctx as *const BridgeCallContext, - &session_buffers as *const std::cell::RefCell, &["_testBridge"], ); } @@ -4220,7 +4216,6 @@ export const file = new File([], "empty.txt"); "test-session".into(), ); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4229,7 +4224,6 @@ export const file = new File([], "empty.txt"); _fn_store = bridge::register_sync_bridge_fns( scope, &bridge_ctx as *const BridgeCallContext, - &session_buffers as *const std::cell::RefCell, &["_fn1", "_fn2"], ); } @@ -4261,7 +4255,6 @@ export const file = new File([], "empty.txt"); "test-session".into(), ); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4270,7 +4263,6 @@ export const file = new File([], "empty.txt"); _fn_store = bridge::register_sync_bridge_fns( scope, &bridge_ctx as *const BridgeCallContext, - &session_buffers as *const std::cell::RefCell, &["_testBridge"], ); } @@ -4291,7 +4283,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4301,7 +4292,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -4370,7 +4360,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4380,7 +4369,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -4439,7 +4427,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4449,7 +4436,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_fetch", "_dns"], ); } @@ -4520,7 +4506,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4530,7 +4515,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -4571,7 +4555,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -4581,7 +4564,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5137,7 +5119,6 @@ export const file = new File([], "empty.txt"); let pending = bridge::PendingPromises::new(); // Register async bridge function - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5147,7 +5128,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5206,7 +5186,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5216,7 +5195,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_fetch", "_dns"], ); } @@ -5285,7 +5263,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5295,7 +5272,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5340,7 +5316,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5350,7 +5325,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5409,7 +5383,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5419,7 +5392,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5502,7 +5474,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5512,7 +5483,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5562,7 +5532,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5572,7 +5541,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5658,7 +5626,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5668,7 +5635,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5742,7 +5708,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5752,7 +5717,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5821,7 +5785,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5831,7 +5794,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -5893,7 +5855,6 @@ export const file = new File([], "empty.txt"); ); let pending = bridge::PendingPromises::new(); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _fn_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -5903,7 +5864,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_asyncFn"], ); } @@ -6047,7 +6007,6 @@ export const file = new File([], "empty.txt"); Box::new(Cursor::new(Vec::new())), // unused for async "test-session".into(), ); - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); let _async_store; { let scope = &mut v8::HandleScope::new(&mut iso); @@ -6057,7 +6016,6 @@ export const file = new File([], "empty.txt"); scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers as *const std::cell::RefCell, &["_slowFn"], ); } @@ -7886,110 +7844,5 @@ export const file = new File([], "empty.txt"); assert_eq!(eval(&mut iso, &ctx, "String(globalThis.__depValue)"), "42"); clear_module_state(); } - - // --- Part 57: serialize_v8_value_into reuses buffer capacity --- - { - let mut iso = isolate::create_isolate(None); - let ctx = isolate::create_context(&mut iso); - - let mut buf = Vec::new(); - - // First serialization grows the buffer - { - let scope = &mut v8::HandleScope::new(&mut iso); - let local = v8::Local::new(scope, &ctx); - let scope = &mut v8::ContextScope::new(scope, local); - let val = v8::String::new(scope, "hello world").unwrap(); - bridge::serialize_v8_value_into(scope, val.into(), &mut buf).expect("serialize"); - } - assert!(!buf.is_empty()); - let cap_after_first = buf.capacity(); - - // Second serialization (smaller value) reuses capacity - { - let scope = &mut v8::HandleScope::new(&mut iso); - let local = v8::Local::new(scope, &ctx); - let scope = &mut v8::ContextScope::new(scope, local); - let val = v8::Integer::new(scope, 42); - bridge::serialize_v8_value_into(scope, val.into(), &mut buf).expect("serialize"); - } - assert_eq!( - buf.capacity(), - cap_after_first, - "capacity should stay at high-water mark" - ); - - // Third serialization (larger value) grows buffer - { - let scope = &mut v8::HandleScope::new(&mut iso); - let local = v8::Local::new(scope, &ctx); - let scope = &mut v8::ContextScope::new(scope, local); - let long_str = "x".repeat(1024); - let val = v8::String::new(scope, &long_str).unwrap(); - bridge::serialize_v8_value_into(scope, val.into(), &mut buf).expect("serialize"); - } - assert!( - buf.capacity() >= cap_after_first, - "capacity should grow for larger values" - ); - let cap_after_large = buf.capacity(); - - // Fourth serialization (small again) stays at high-water mark - { - let scope = &mut v8::HandleScope::new(&mut iso); - let local = v8::Local::new(scope, &ctx); - let scope = &mut v8::ContextScope::new(scope, local); - let val = v8::Boolean::new(scope, true); - bridge::serialize_v8_value_into(scope, val.into(), &mut buf).expect("serialize"); - } - assert_eq!( - buf.capacity(), - cap_after_large, - "capacity stays at high-water mark" - ); - - // Verify the serialized data is correct (round-trip) - { - let scope = &mut v8::HandleScope::new(&mut iso); - let local = v8::Local::new(scope, &ctx); - let scope = &mut v8::ContextScope::new(scope, local); - let deserialized = bridge::deserialize_v8_value(scope, &buf).expect("deserialize"); - assert!(deserialized.is_true(), "should deserialize to true"); - } - } - - // --- Part 58: SessionBuffers ser_buf grows to high-water mark across bridge calls --- - { - let mut iso = isolate::create_isolate(None); - let ctx = isolate::create_context(&mut iso); - - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); - assert!( - session_buffers.borrow().ser_buf.capacity() >= 256, - "initial capacity should be >= 256" - ); - - // Simulate multiple serializations through SessionBuffers - for i in 0..5 { - let scope = &mut v8::HandleScope::new(&mut iso); - let local = v8::Local::new(scope, &ctx); - let scope = &mut v8::ContextScope::new(scope, local); - - // Create varying-size values - let val_str = "a".repeat(100 * (i + 1)); - let val = v8::String::new(scope, &val_str).unwrap(); - let mut bufs = session_buffers.borrow_mut(); - bridge::serialize_v8_value_into(scope, val.into(), &mut bufs.ser_buf) - .expect("serialize"); - } - - // Buffer capacity should be at least as large as the last (largest) serialization - let bufs = session_buffers.borrow(); - assert!(!bufs.ser_buf.is_empty(), "should contain serialized data"); - - // Verify the buffer hasn't been dropped/reallocated to smaller size - let final_cap = bufs.ser_buf.capacity(); - assert!(final_cap >= bufs.ser_buf.len(), "capacity >= len"); - } } } diff --git a/crates/v8-runtime/src/session.rs b/crates/v8-runtime/src/session.rs index 6d0a292b2..27363ee6e 100644 --- a/crates/v8-runtime/src/session.rs +++ b/crates/v8-runtime/src/session.rs @@ -810,10 +810,6 @@ fn session_thread( #[cfg(not(test))] let mut isolate_userland_code: Option = None; - // Pre-allocated serialization buffers for V8 ValueSerializer output - #[cfg(not(test))] - let session_buffers = std::cell::RefCell::new(bridge::SessionBuffers::new()); - // Process commands until shutdown or channel close loop { let next_command = if let Some(command) = deferred_commands.pop_front() { @@ -1048,8 +1044,6 @@ fn session_thread( scope, &bridge_ctx as *const BridgeCallContext, &pending as *const bridge::PendingPromises, - &session_buffers - as *const std::cell::RefCell, sync_bridge_fns, async_bridge_fns, ); @@ -2399,10 +2393,9 @@ mod tests { /// the cleanup happens, it does not saturate `MAX_PENDING_PROMISES`. #[test] fn reset_pending_promises_drops_resolver_globals_before_isolate_teardown() { - use crate::bridge::{register_async_bridge_fns, PendingPromises, SessionBuffers}; + use crate::bridge::{register_async_bridge_fns, PendingPromises}; use crate::host_call::BridgeCallContext; use crate::isolate; - use std::cell::RefCell; use std::process::Command; // V8 isolates must be created in an isolated process: doing it inline in a @@ -2436,7 +2429,6 @@ mod tests { let context = v8::Local::new(scope, &context); let scope = &mut v8::ContextScope::new(scope, context); - let session_buffers = RefCell::new(SessionBuffers::new()); let bridge_ctx = BridgeCallContext::new( Box::new(std::io::sink()), Box::new(std::io::empty()), @@ -2453,7 +2445,6 @@ mod tests { scope, &bridge_ctx as *const BridgeCallContext, &pending as *const PendingPromises, - &session_buffers as *const RefCell, &["_asyncFn"], ); let source = format!("for (let i = 0; i < {REGISTERED}; i++) {{ _asyncFn(i); }}"); diff --git a/crates/v8-runtime/src/snapshot.rs b/crates/v8-runtime/src/snapshot.rs index 0f7931e64..401a8c4c6 100644 --- a/crates/v8-runtime/src/snapshot.rs +++ b/crates/v8-runtime/src/snapshot.rs @@ -721,11 +721,8 @@ pub fn run_snapshot_consolidated_checks() { // Verifies that FunctionTemplates registered on a restored isolate // correctly dispatch to Rust bridge callbacks via external_refs(). { - use crate::bridge::{ - register_async_bridge_fns, register_sync_bridge_fns, PendingPromises, SessionBuffers, - }; + use crate::bridge::{register_async_bridge_fns, register_sync_bridge_fns, PendingPromises}; use crate::host_call::BridgeCallContext; - use std::cell::RefCell; let bridge_code = "(function() { globalThis.__ext_ref_test = true; })();"; let blob = create_snapshot(bridge_code).expect("snapshot creation"); @@ -750,7 +747,6 @@ pub fn run_snapshot_consolidated_checks() { call_id_router, Arc::new(std::sync::atomic::AtomicU64::new(1)), ); - let session_buffers = RefCell::new(SessionBuffers::new()); let pending = PendingPromises::new(); let scope = &mut v8::HandleScope::new(&mut isolate); @@ -761,14 +757,12 @@ pub fn run_snapshot_consolidated_checks() { let _sync_store = register_sync_bridge_fns( scope, &bridge_ctx as *const BridgeCallContext, - &session_buffers as *const RefCell, &["_testSync"], ); let _async_store = register_async_bridge_fns( scope, &bridge_ctx as *const BridgeCallContext, &pending as *const PendingPromises, - &session_buffers as *const RefCell, &["_testAsync"], ); @@ -1104,9 +1098,8 @@ pub fn run_snapshot_consolidated_checks() { // stubs, restore, replace stubs with real bridge functions, verify the // replaced functions dispatch to the real Rust callbacks. { - use crate::bridge::{replace_bridge_fns, PendingPromises, SessionBuffers}; + use crate::bridge::{replace_bridge_fns, PendingPromises}; use crate::host_call::BridgeCallContext; - use std::cell::RefCell; // Create snapshot with stubs + simple bridge IIFE let bridge_code = r#" @@ -1139,7 +1132,6 @@ pub fn run_snapshot_consolidated_checks() { call_id_router, Arc::new(std::sync::atomic::AtomicU64::new(1)), ); - let session_buffers = RefCell::new(SessionBuffers::new()); let pending = PendingPromises::new(); // Restore context and replace bridge functions @@ -1151,7 +1143,6 @@ pub fn run_snapshot_consolidated_checks() { scope, &bridge_ctx as *const BridgeCallContext, &pending as *const PendingPromises, - &session_buffers as *const RefCell, &["_log", "_fsReadFile"], &["_scheduleTimer"], ); From c0d7db60588c0f99d5abe58491b82284f80919a7 Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Wed, 1 Jul 2026 13:26:06 -0700 Subject: [PATCH 3/3] perf(sidecar): move stdio write buffers into execution events instead of cloning --- crates/sidecar/src/filesystem.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sidecar/src/filesystem.rs b/crates/sidecar/src/filesystem.rs index c82273bc2..1eb84522f 100644 --- a/crates/sidecar/src/filesystem.rs +++ b/crates/sidecar/src/filesystem.rs @@ -1084,9 +1084,9 @@ pub(crate) fn service_javascript_fs_sync_rpc( position.is_none() && kernel_fd_surfaces_stdio_event(kernel, kernel_pid, fd)?; if surfaces_stdio { let event = if fd == 1 { - ActiveExecutionEvent::Stdout(contents.clone()) + ActiveExecutionEvent::Stdout(contents) } else { - ActiveExecutionEvent::Stderr(contents.clone()) + ActiveExecutionEvent::Stderr(contents) }; process.queue_pending_execution_event(event)?; } else {