Skip to content

Commit a9d5d3a

Browse files
NathanFlurryclaude
andcommitted
feat: US-055 - Wire SnapshotCache into session thread and connection handler
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1303bf1 commit a9d5d3a

2 files changed

Lines changed: 75 additions & 25 deletions

File tree

crates/v8-runtime/src/main.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use std::sync::{Arc, Mutex};
2424
use host_call::CallIdRouter;
2525
use ipc_binary::BinaryFrame;
2626
use session::SessionManager;
27+
use snapshot::SnapshotCache;
2728

2829
/// Close all file descriptors > 2 (stdin/stdout/stderr preserved).
2930
/// Called at process startup to prevent the parent from leaking FDs into the V8 runtime.
@@ -150,6 +151,7 @@ fn handle_connection(
150151
mut stream: UnixStream,
151152
connection_id: u64,
152153
session_mgr: Arc<Mutex<SessionManager>>,
154+
snapshot_cache: Arc<SnapshotCache>,
153155
) {
154156
loop {
155157
// Read next binary frame from connection
@@ -239,6 +241,15 @@ fn handle_connection(
239241
);
240242
}
241243
}
244+
// Handle WarmSnapshot: pre-warm the snapshot cache (fire-and-forget, no response)
245+
BinaryFrame::WarmSnapshot { bridge_code } => {
246+
if let Err(e) = snapshot_cache.get_or_create(&bridge_code) {
247+
eprintln!(
248+
"connection {}: WarmSnapshot failed: {}",
249+
connection_id, e
250+
);
251+
}
252+
}
242253
_ => {
243254
eprintln!("connection {}: unexpected frame type", connection_id);
244255
}
@@ -257,6 +268,9 @@ fn main() {
257268
// Initialize V8 platform on the main thread before any session threads
258269
isolate::init_v8_platform();
259270

271+
// Shared snapshot cache for fast isolate creation across all connections/sessions
272+
let snapshot_cache = Arc::new(SnapshotCache::new(4));
273+
260274
// Read auth token from environment
261275
let auth_token = std::env::var("SECURE_EXEC_V8_TOKEN")
262276
.expect("SECURE_EXEC_V8_TOKEN environment variable must be set");
@@ -379,14 +393,16 @@ fn main() {
379393
max_concurrency,
380394
ipc_tx,
381395
call_id_router,
396+
Arc::clone(&snapshot_cache),
382397
)));
383398

384399
// Authenticated — spawn connection handler thread
385400
let mgr = Arc::clone(&session_mgr);
401+
let snap = Arc::clone(&snapshot_cache);
386402
std::thread::Builder::new()
387403
.name(format!("conn-{}", conn_id))
388404
.spawn(move || {
389-
handle_connection(stream, conn_id, mgr);
405+
handle_connection(stream, conn_id, mgr, snap);
390406
})
391407
.expect("failed to spawn connection handler");
392408
}

crates/v8-runtime/src/session.rs

Lines changed: 58 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,13 @@ use crossbeam_channel::{Receiver, Sender};
88

99
use crate::host_call::CallIdRouter;
1010
use crate::ipc_binary::BinaryFrame;
11+
use crate::snapshot::SnapshotCache;
1112
#[cfg(not(test))]
1213
use crate::host_call::{BridgeCallContext, ChannelFrameSender};
1314
#[cfg(not(test))]
1415
use crate::ipc_binary::{self, ExecutionErrorBin};
1516
#[cfg(not(test))]
16-
use crate::{bridge, execution, isolate, stream};
17+
use crate::{bridge, execution, isolate, snapshot, stream};
1718

1819
/// Commands sent to a session thread
1920
pub enum SessionCommand {
@@ -54,19 +55,27 @@ pub struct SessionManager {
5455
ipc_tx: IpcSender,
5556
/// Call_id → session_id routing table for BridgeResponse dispatch
5657
call_id_router: CallIdRouter,
58+
/// Shared snapshot cache for fast isolate creation from pre-compiled bridge code
59+
snapshot_cache: Arc<SnapshotCache>,
5760
}
5861

5962
impl SessionManager {
60-
pub fn new(max_concurrency: usize, ipc_tx: IpcSender, call_id_router: CallIdRouter) -> Self {
63+
pub fn new(max_concurrency: usize, ipc_tx: IpcSender, call_id_router: CallIdRouter, snapshot_cache: Arc<SnapshotCache>) -> Self {
6164
SessionManager {
6265
sessions: HashMap::new(),
6366
max_concurrency,
6467
slot_control: Arc::new((Mutex::new(0), Condvar::new())),
6568
ipc_tx,
6669
call_id_router,
70+
snapshot_cache,
6771
}
6872
}
6973

74+
/// Get the snapshot cache for pre-warming from WarmSnapshot messages.
75+
pub fn snapshot_cache(&self) -> &Arc<SnapshotCache> {
76+
&self.snapshot_cache
77+
}
78+
7079
/// Create a new session bound to the given connection.
7180
/// Spawns a dedicated thread with a V8 isolate. If max concurrency is
7281
/// reached, the session thread will block until a slot becomes available.
@@ -86,6 +95,7 @@ impl SessionManager {
8695
let max = self.max_concurrency;
8796
let ipc_tx = self.ipc_tx.clone();
8897
let router = Arc::clone(&self.call_id_router);
98+
let snap_cache = Arc::clone(&self.snapshot_cache);
8999

90100
let name_prefix = if session_id.len() > 8 {
91101
&session_id[..8]
@@ -95,7 +105,7 @@ impl SessionManager {
95105
let join_handle = thread::Builder::new()
96106
.name(format!("session-{}", name_prefix))
97107
.spawn(move || {
98-
session_thread(heap_limit_mb, cpu_time_limit_ms, rx, slot_control, max, ipc_tx, router);
108+
session_thread(heap_limit_mb, cpu_time_limit_ms, rx, slot_control, max, ipc_tx, router, snap_cache);
99109
})
100110
.map_err(|e| format!("failed to spawn session thread: {}", e))?;
101111

@@ -221,7 +231,8 @@ fn send_message(ipc_tx: &IpcSender, frame: &BinaryFrame, frame_buf: &mut Vec<u8>
221231
}
222232
}
223233

224-
/// Session thread: acquires a concurrency slot, creates a V8 isolate, and
234+
/// Session thread: acquires a concurrency slot, defers V8 isolate creation
235+
/// to first Execute (when bridge code is known for snapshot lookup), and
225236
/// processes commands until shutdown.
226237
fn session_thread(
227238
#[cfg_attr(test, allow(unused_variables))] heap_limit_mb: Option<u32>,
@@ -231,6 +242,7 @@ fn session_thread(
231242
max_concurrency: usize,
232243
#[cfg_attr(test, allow(unused_variables))] ipc_tx: IpcSender,
233244
#[cfg_attr(test, allow(unused_variables))] call_id_router: CallIdRouter,
245+
#[cfg_attr(test, allow(unused_variables))] snapshot_cache: Arc<SnapshotCache>,
234246
) {
235247
// Acquire concurrency slot (blocks if at capacity)
236248
{
@@ -242,17 +254,13 @@ fn session_thread(
242254
*count += 1;
243255
}
244256

245-
// Create V8 isolate and context
246-
// In test mode, skip V8 to avoid inter-test SIGSEGV (V8 lifecycle tested in isolate::tests)
257+
// Isolate creation is deferred to first Execute (when bridge code is known
258+
// for snapshot cache lookup). This avoids creating an isolate that may never
259+
// be used and enables snapshot-based fast creation.
260+
#[cfg(not(test))]
261+
let mut v8_isolate: Option<v8::OwnedIsolate> = None;
247262
#[cfg(not(test))]
248-
let (mut v8_isolate, _v8_context) = {
249-
isolate::init_v8_platform();
250-
let mut iso = isolate::create_isolate(heap_limit_mb);
251-
// Disable WASM compilation before any code execution
252-
execution::disable_wasm(&mut iso);
253-
let ctx = isolate::create_context(&mut iso);
254-
(iso, ctx)
255-
};
263+
let mut _v8_context: Option<v8::Global<v8::Context>> = None;
256264

257265
#[cfg(not(test))]
258266
let pending = bridge::PendingPromises::new();
@@ -306,12 +314,37 @@ fn session_thread(
306314
bridge_code
307315
};
308316

317+
// Deferred isolate creation: create on first Execute using snapshot cache
318+
if v8_isolate.is_none() {
319+
isolate::init_v8_platform();
320+
let mut iso = if !effective_bridge_code.is_empty() {
321+
match snapshot_cache.get_or_create(&effective_bridge_code) {
322+
Ok(blob) => {
323+
snapshot::create_isolate_from_snapshot((*blob).clone(), heap_limit_mb)
324+
}
325+
Err(e) => {
326+
eprintln!("snapshot creation failed, falling back to fresh isolate: {}", e);
327+
isolate::create_isolate(heap_limit_mb)
328+
}
329+
}
330+
} else {
331+
isolate::create_isolate(heap_limit_mb)
332+
};
333+
// Must re-apply WASM disable after every restore (not captured in snapshot)
334+
execution::disable_wasm(&mut iso);
335+
let ctx = isolate::create_context(&mut iso);
336+
_v8_context = Some(ctx);
337+
v8_isolate = Some(iso);
338+
}
339+
340+
let iso = v8_isolate.as_mut().unwrap();
341+
309342
// Create a fresh V8 context per execution (clean global scope)
310-
let exec_context = isolate::create_context(&mut v8_isolate);
343+
let exec_context = isolate::create_context(iso);
311344

312345
// Inject globals from last InjectGlobals payload into the fresh context
313346
if let Some(ref payload) = last_globals_payload {
314-
let scope = &mut v8::HandleScope::new(&mut v8_isolate);
347+
let scope = &mut v8::HandleScope::new(iso);
315348
let ctx = v8::Local::new(scope, &exec_context);
316349
let scope = &mut v8::ContextScope::new(scope, ctx);
317350
execution::inject_globals_from_payload(scope, payload);
@@ -341,7 +374,7 @@ fn session_thread(
341374
let _sync_store;
342375
let _async_store;
343376
{
344-
let scope = &mut v8::HandleScope::new(&mut v8_isolate);
377+
let scope = &mut v8::HandleScope::new(iso);
345378
let ctx = v8::Local::new(scope, &exec_context);
346379
let scope = &mut v8::ContextScope::new(scope, ctx);
347380

@@ -364,7 +397,7 @@ fn session_thread(
364397
// Start timeout guard before execution
365398
let mut timeout_guard = match (cpu_time_limit_ms, maybe_abort_tx) {
366399
(Some(ms), Some(abort_tx)) => {
367-
let handle = v8_isolate.thread_safe_handle();
400+
let handle = iso.thread_safe_handle();
368401
Some(crate::timeout::TimeoutGuard::new(ms, handle, abort_tx))
369402
}
370403
_ => None,
@@ -373,14 +406,14 @@ fn session_thread(
373406
// Execute code (fresh context per execution)
374407
let file_path_opt = if file_path.is_empty() { None } else { Some(file_path.as_str()) };
375408
let (code, exports, error) = if mode == 0 {
376-
let scope = &mut v8::HandleScope::new(&mut v8_isolate);
409+
let scope = &mut v8::HandleScope::new(iso);
377410
let ctx = v8::Local::new(scope, &exec_context);
378411
let scope = &mut v8::ContextScope::new(scope, ctx);
379412
let (c, e) =
380413
execution::execute_script(scope, &effective_bridge_code, &user_code, &mut bridge_cache);
381414
(c, None, e)
382415
} else {
383-
let scope = &mut v8::HandleScope::new(&mut v8_isolate);
416+
let scope = &mut v8::HandleScope::new(iso);
384417
let ctx = v8::Local::new(scope, &exec_context);
385418
let scope = &mut v8::ContextScope::new(scope, ctx);
386419
execution::execute_module(
@@ -395,7 +428,7 @@ fn session_thread(
395428

396429
// Run event loop if there are pending async promises
397430
let terminated = if pending.len() > 0 {
398-
let scope = &mut v8::HandleScope::new(&mut v8_isolate);
431+
let scope = &mut v8::HandleScope::new(iso);
399432
let ctx = v8::Local::new(scope, &exec_context);
400433
let scope = &mut v8::ContextScope::new(scope, ctx);
401434
!run_event_loop(scope, &rx, &pending, maybe_abort_rx.as_ref())
@@ -464,8 +497,8 @@ fn session_thread(
464497
// Drop V8 resources (only present in non-test mode)
465498
#[cfg(not(test))]
466499
{
467-
drop(_v8_context);
468-
drop(v8_isolate);
500+
drop(_v8_context.take());
501+
drop(v8_isolate.take());
469502
}
470503

471504
// Release concurrency slot
@@ -686,7 +719,8 @@ mod tests {
686719
fn test_manager(max: usize) -> SessionManager {
687720
let (tx, _rx) = crossbeam_channel::unbounded();
688721
let router: CallIdRouter = Arc::new(Mutex::new(HashMap::new()));
689-
SessionManager::new(max, tx, router)
722+
let snap_cache = Arc::new(SnapshotCache::new(4));
723+
SessionManager::new(max, tx, router, snap_cache)
690724
}
691725

692726
#[test]

0 commit comments

Comments
 (0)