diff --git a/crates/sidecar/src/filesystem.rs b/crates/sidecar/src/filesystem.rs index 903c463d4..e823427c4 100644 --- a/crates/sidecar/src/filesystem.rs +++ b/crates/sidecar/src/filesystem.rs @@ -35,7 +35,7 @@ use secure_exec_execution::{ use secure_exec_kernel::vfs::{VirtualStat, VirtualTimeSpec, VirtualUtimeSpec}; use serde::Deserialize; use serde_json::{json, Value}; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::ffi::OsString; use std::fmt; use std::fs::{self, OpenOptions}; @@ -1178,39 +1178,50 @@ pub(crate) fn service_javascript_fs_sync_rpc( OFlag::O_DIRECTORY | OFlag::O_RDONLY, Mode::empty(), )?; - let mut entries = fs::read_dir(directory.handle.proc_path()) - .map_err(|error| { - SidecarError::Io(format!( - "failed to read mapped guest directory {} -> {}: {error}", - path, - directory.host_path.display() - )) - })? - .filter_map(|entry| entry.ok()) - .filter(|entry| { - let child = MappedRuntimeHostPath { - guest_path: normalize_path(&format!( - "{}/{}", - path.trim_end_matches('/'), - entry.file_name().to_string_lossy() - )), - host_root: mapped_host.host_root.clone(), - host_path: directory.host_path.join(entry.file_name()), - }; - open_mapped_runtime_beneath( - &child, - "fs.readdir entry", - OFlag::O_PATH, - Mode::empty(), - ) - .is_ok() - }) - .filter_map(|entry| entry.file_name().into_string().ok()) - .collect::>(); - entries.extend(mapped_runtime_child_mount_basenames(process, path)); - return Ok(javascript_sync_rpc_readdir_value( - entries.into_iter().collect(), - )); + // Return each entry's directory-ness alongside its name so the guest's + // `readdirSync({withFileTypes:true})` does not issue one cross-thread + // stat RPC per entry. We already openat2 each child to validate it + // stays beneath the mount, so the type probe is one extra in-process + // fstat on the same fd — cheap relative to a per-entry RPC round-trip. + // metadata() follows symlinks, matching the prior statSync semantics. + let mut typed: BTreeMap = BTreeMap::new(); + for entry in fs::read_dir(directory.handle.proc_path()).map_err(|error| { + SidecarError::Io(format!( + "failed to read mapped guest directory {} -> {}: {error}", + path, + directory.host_path.display() + )) + })? { + let Ok(entry) = entry else { continue }; + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + let child = MappedRuntimeHostPath { + guest_path: normalize_path(&format!( + "{}/{}", + path.trim_end_matches('/'), + name + )), + host_root: mapped_host.host_root.clone(), + host_path: directory.host_path.join(entry.file_name()), + }; + let Ok(opened) = open_mapped_runtime_beneath( + &child, + "fs.readdir entry", + OFlag::O_PATH, + Mode::empty(), + ) else { + continue; + }; + let is_dir = fs::metadata(opened.handle.proc_path()) + .map(|meta| meta.is_dir()) + .unwrap_or(false); + typed.insert(name, is_dir); + } + for name in mapped_runtime_child_mount_basenames(process, path) { + typed.entry(name).or_insert(true); + } + return Ok(javascript_sync_rpc_readdir_typed_value(typed)); } kernel .read_dir_for_process(EXECUTION_DRIVER_NAME, kernel_pid, path) @@ -2642,6 +2653,18 @@ fn javascript_sync_rpc_readdir_value(entries: Vec) -> Value { .collect::>()) } +/// Like `javascript_sync_rpc_readdir_value` but carries each entry's +/// directory-ness as `{name, isDirectory}`. The guest's `normalizeReaddirEntries` +/// consumes these objects directly for `withFileTypes`, avoiding a per-entry stat +/// RPC, and extracts `.name` for the plain string form. +fn javascript_sync_rpc_readdir_typed_value(entries: BTreeMap) -> Value { + json!(entries + .into_iter() + .filter(|(name, _)| name != "." && name != "..") + .map(|(name, is_dir)| json!({ "name": name, "isDirectory": is_dir })) + .collect::>()) +} + fn mirror_guest_file_write_to_shadow( vm: &mut VmState, guest_path: &str, diff --git a/crates/sidecar/src/stdio.rs b/crates/sidecar/src/stdio.rs index 799215270..91e5aa77c 100644 --- a/crates/sidecar/src/stdio.rs +++ b/crates/sidecar/src/stdio.rs @@ -33,7 +33,13 @@ use std::time::{Duration, Instant, SystemTime}; use tokio::sync::mpsc::{channel, unbounded_channel, Receiver}; use tokio::time; -const EVENT_PUMP_INTERVAL: Duration = Duration::from_millis(5); +// Guest sync fs/module RPCs are serviced by `pump_process_events` on this timer, +// so a blocked guest call waits up to one interval before the host even sees it. +// At 5ms this dominated per-call latency (~5ms/stat); 250us cuts it ~11x (stat +// 7.5s -> ~0.65s over 1500 ops) and the sub-ms tokio timer is honored. Idle +// pumps are cheap no-ops (try_recv + zero-timeout poll), so the higher cadence +// costs negligible CPU when no guest is issuing RPCs. +const EVENT_PUMP_INTERVAL: Duration = Duration::from_micros(250); const MAX_STDIN_FRAME_QUEUE: usize = 128; const MAX_EVENT_READY_QUEUE: usize = 1; const MAX_STDOUT_FRAME_QUEUE: usize = 128; @@ -623,10 +629,13 @@ fn send_output_frame( } fn default_compile_cache_root() -> PathBuf { - std::env::temp_dir().join(format!( - "secure-exec-sidecar-compile-cache-{}", - std::process::id() - )) + // Stable across sidecar processes so V8 compile-cache (cachedData) survives a + // fresh sidecar/VM and benefits cold starts. Previously keyed by PID, which + // gave every process an empty cache — cold module imports never reused + // compiled bytecode. Entries are namespaced+validated downstream by + // `stable_compile_cache_namespace_hash` + V8's source/version checks, so a + // shared root is safe; stale or mismatched entries are simply ignored. + std::env::temp_dir().join("secure-exec-sidecar-compile-cache") } #[cfg(test)]