From c50eab8912a1903c2c1198f38d6215ddda865b8c Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 26 Jun 2026 15:14:20 -0700 Subject: [PATCH 1/2] feat(bench): add native-baseline host-timing crate --- Cargo.lock | 4 + Cargo.toml | 1 + crates/native-baseline/Cargo.toml | 16 + crates/native-baseline/src/main.rs | 584 +++++++++++++++++++++++++++++ 4 files changed, 605 insertions(+) create mode 100644 crates/native-baseline/Cargo.toml create mode 100644 crates/native-baseline/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 54a035d60..6f0db27d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2131,6 +2131,10 @@ dependencies = [ "uuid", ] +[[package]] +name = "native-baseline" +version = "0.3.0-rc.1" + [[package]] name = "ndk-context" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index 527044b11..cea0c2ed1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "crates/sidecar-browser", "crates/vm-config", "crates/v8-runtime", + "crates/native-baseline", ] [workspace.package] diff --git a/crates/native-baseline/Cargo.toml b/crates/native-baseline/Cargo.toml new file mode 100644 index 000000000..a28bd4e5b --- /dev/null +++ b/crates/native-baseline/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "native-baseline" +version.workspace = true +edition.workspace = true +license.workspace = true +publish = false + +# Native floor for the differential perf harness. std-only on purpose: this is the +# glibc fork/posix_spawn + execve baseline that the agent-os "emulation tax" divides +# against. No secure-exec deps — it must measure the host, not the emulator. + +[[bin]] +name = "native-baseline" +path = "src/main.rs" + +[dependencies] diff --git a/crates/native-baseline/src/main.rs b/crates/native-baseline/src/main.rs new file mode 100644 index 000000000..3c81d2bea --- /dev/null +++ b/crates/native-baseline/src/main.rs @@ -0,0 +1,584 @@ +//! Native floor for the differential perf harness. +//! +//! Runs one logical op many times, timing each with a monotonic clock, and emits +//! the raw per-iteration sample array (nanoseconds) as JSON to stdout. The TypeScript +//! harness reduces these samples with the SAME `stats()` it applies to the node and +//! guest layers, so the percentile math is identical across all three layers and the +//! "emulation tax" ratio is honest. +//! +//! Ops (held byte-identical to the node + guest layers): +//! spawn_exit -> /bin/sh -c 'exit 0' (fork/posix_spawn + execve + reap) +//! exec_capture -> /bin/sh -c 'printf hi' (same, plus stdout capture) +//! fs_stat -> stat a small host file +//! fs_write -> overwrite a small host file +//! fs_read -> read a 64 KiB host file +//! dns_lookup -> resolve localhost +//! tcp_connect -> localhost TCP connect+close +//! tcp_echo -> localhost TCP connect+echo +//! pipe_echo -> shell pipe echo through cat +//! cpu_loop -> bounded integer loop +//! alloc_free -> allocate/drop a 64 KiB Vec +//! +//! Usage: native-baseline --op spawn_exit|exec_capture --iters N --warmup W + +use std::fs::File; +use std::io::{Read, Write}; +use std::net::{TcpListener, TcpStream, ToSocketAddrs, UdpSocket}; +#[cfg(unix)] +use std::os::unix::net::UnixStream; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::Instant; + +#[derive(Clone, Copy)] +enum Op { + SpawnExit, + ExecCapture, + NodeStdoutDiscard2b, + NodeStdoutCapture2b, + NodeStdoutListenerOnly2b, + NodeExit, + NodeFanout, + NodeReapStorm, + PipeChain, + FsStat, + FsWrite, + FsRead, + FsOpenClose, + FsMkdirRmdir, + FsRename, + FsReaddir, + FsFsync, + DnsLookup, + DnsConcurrent, + TcpConnect, + TcpEcho, + TcpConcurrent, + TcpThroughput, + TcpTinyWrites, + UdpEcho, + PipeEcho, + PipeThroughput, + PipeBackpressure, + CpuLoop, + AllocFree, +} + +fn run_once(op: Op, iter: usize) { + match op { + Op::SpawnExit => { + let status = Command::new("/bin/sh") + .args(["-c", "exit 0"]) + .status() + .expect("spawn /bin/sh failed"); + assert!(status.success(), "expected exit 0, got {status:?}"); + } + Op::ExecCapture => { + let out = Command::new("/bin/sh") + .args(["-c", "printf hi"]) + .output() + .expect("spawn /bin/sh failed"); + assert_eq!(out.stdout, b"hi", "unexpected stdout"); + } + Op::NodeStdoutDiscard2b => { + let status = Command::new("node") + .args(["-e", "process.stdout.write('hi')"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .expect("spawn node failed"); + assert!(status.success(), "expected exit 0, got {status:?}"); + } + Op::NodeStdoutCapture2b => { + let out = Command::new("node") + .args(["-e", "process.stdout.write('hi')"]) + .output() + .expect("spawn node failed"); + assert!( + out.status.success(), + "expected exit 0, got {:?}", + out.status + ); + assert_eq!(out.stdout, b"hi", "unexpected stdout"); + } + Op::NodeStdoutListenerOnly2b => { + let mut child = Command::new("node") + .args(["-e", "process.stdout.write('hi')"]) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .spawn() + .expect("spawn node failed"); + let mut stdout = child.stdout.take().expect("stdout pipe"); + let mut bytes = Vec::new(); + stdout.read_to_end(&mut bytes).expect("read stdout"); + let status = child.wait().expect("wait node child"); + assert!(status.success(), "expected exit 0, got {status:?}"); + assert_eq!(bytes.len(), 2, "unexpected stdout byte count"); + } + // Real host node process that immediately exits. This is the apples-to-apples + // floor for the guest layer, where the same logical op spins a V8 isolate. + Op::NodeExit => { + let status = Command::new("node") + .args(["-e", "process.exit(0)"]) + .status() + .expect("spawn node failed"); + assert!(status.success(), "expected exit 0, got {status:?}"); + } + Op::NodeFanout | Op::NodeReapStorm => { + let mut children = Vec::new(); + for _ in 0..8 { + children.push( + Command::new("node") + .args(["-e", "process.exit(0)"]) + .spawn() + .expect("spawn node failed"), + ); + } + for mut child in children { + let status = child.wait().expect("wait node child"); + assert!(status.success(), "expected exit 0, got {status:?}"); + } + } + Op::PipeChain => { + let out = Command::new("/bin/sh") + .args(["-c", "printf hello | cat | cat >/dev/null"]) + .output() + .expect("run pipe chain"); + assert!(out.status.success(), "pipe chain failed: {out:?}"); + } + Op::FsStat => { + let path = std::env::temp_dir().join("secure-exec-native-fs-stat.txt"); + std::fs::write(&path, b"hi").expect("write stat fixture"); + let meta = std::fs::metadata(&path).expect("stat fixture"); + assert!(meta.len() >= 2); + } + Op::FsWrite => { + let path = std::env::temp_dir().join("secure-exec-native-fs-write.txt"); + std::fs::write(path, format!("hello-{iter:08}")).expect("write fixture"); + } + Op::FsRead => { + let path = std::env::temp_dir().join("secure-exec-native-fs-read.bin"); + if !path.exists() { + std::fs::write(&path, vec![7_u8; 64 * 1024]).expect("write read fixture"); + } + let data = std::fs::read(path).expect("read fixture"); + assert_eq!(data.len(), 64 * 1024); + } + Op::FsOpenClose => { + let path = std::env::temp_dir().join("secure-exec-native-fs-open-close.txt"); + std::fs::write(&path, b"hi").expect("write open fixture"); + let file = File::open(path).expect("open fixture"); + drop(file); + } + Op::FsMkdirRmdir => { + let path = std::env::temp_dir().join(format!("secure-exec-native-dir-{iter}")); + std::fs::create_dir(&path).expect("create dir"); + std::fs::remove_dir(&path).expect("remove dir"); + } + Op::FsRename => { + let base = std::env::temp_dir(); + let from = base.join(format!("secure-exec-native-rename-{iter}.a")); + let to = base.join(format!("secure-exec-native-rename-{iter}.b")); + std::fs::write(&from, b"hi").expect("write rename fixture"); + std::fs::rename(&from, &to).expect("rename fixture"); + std::fs::remove_file(&to).expect("remove rename fixture"); + } + Op::FsReaddir => { + let dir = std::env::temp_dir().join("secure-exec-native-readdir"); + std::fs::create_dir_all(&dir).expect("create readdir dir"); + for i in 0..32 { + let path = dir.join(format!("{i}.txt")); + if !path.exists() { + std::fs::write(&path, b"hi").expect("write readdir fixture"); + } + } + let count = std::fs::read_dir(dir).expect("read dir").count(); + assert!(count >= 32); + } + Op::FsFsync => { + let path = std::env::temp_dir().join("secure-exec-native-fsync.txt"); + let mut file = File::create(path).expect("create fsync fixture"); + file.write_all(b"hello").expect("write fsync fixture"); + file.sync_all().expect("fsync fixture"); + } + Op::DnsLookup => { + let addrs: Vec<_> = ("localhost", 80) + .to_socket_addrs() + .expect("resolve localhost") + .collect(); + assert!(!addrs.is_empty()); + } + Op::DnsConcurrent => { + let threads: Vec<_> = (0..4) + .map(|_| { + thread::spawn(|| { + let addrs: Vec<_> = ("localhost", 80) + .to_socket_addrs() + .expect("resolve localhost") + .collect(); + assert!(!addrs.is_empty()); + }) + }) + .collect(); + for handle in threads { + handle.join().expect("join resolver"); + } + } + Op::TcpConnect => { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind tcp listener"); + let addr = listener.local_addr().expect("listener addr"); + let server = thread::spawn(move || { + let _ = listener.accept().expect("accept tcp connect"); + }); + let _stream = TcpStream::connect(addr).expect("connect tcp listener"); + server.join().expect("join tcp server"); + } + Op::TcpEcho => { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind tcp listener"); + let addr = listener.local_addr().expect("listener addr"); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept tcp echo"); + let mut buf = [0_u8; 16]; + let n = stream.read(&mut buf).expect("read tcp echo"); + stream.write_all(&buf[..n]).expect("write tcp echo"); + }); + let mut stream = TcpStream::connect(addr).expect("connect tcp echo"); + stream.write_all(b"hello").expect("write client echo"); + let mut buf = [0_u8; 5]; + stream.read_exact(&mut buf).expect("read client echo"); + assert_eq!(&buf, b"hello"); + server.join().expect("join tcp server"); + } + Op::TcpConcurrent => { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind tcp listener"); + let addr = listener.local_addr().expect("listener addr"); + let server = thread::spawn(move || { + for _ in 0..4 { + let (mut stream, _) = listener.accept().expect("accept tcp connect"); + let mut buf = [0_u8; 1]; + let _ = stream.read(&mut buf); + } + }); + let mut clients = Vec::new(); + for _ in 0..4 { + clients.push(TcpStream::connect(addr).expect("connect tcp listener")); + } + for mut client in clients { + client.write_all(b"x").expect("write connect byte"); + } + server.join().expect("join tcp server"); + } + Op::TcpThroughput => { + let payload = vec![7_u8; 64 * 1024]; + let listener = TcpListener::bind("127.0.0.1:0").expect("bind tcp listener"); + let addr = listener.local_addr().expect("listener addr"); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept tcp throughput"); + let mut buf = vec![0_u8; 64 * 1024]; + stream.read_exact(&mut buf).expect("read tcp throughput"); + stream.write_all(&buf).expect("write tcp throughput"); + }); + let mut stream = TcpStream::connect(addr).expect("connect tcp throughput"); + stream.write_all(&payload).expect("write client throughput"); + let mut out = vec![0_u8; payload.len()]; + stream.read_exact(&mut out).expect("read client throughput"); + assert_eq!(out.len(), payload.len()); + server.join().expect("join tcp server"); + } + Op::TcpTinyWrites => { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind tcp listener"); + let addr = listener.local_addr().expect("listener addr"); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept tcp tiny"); + let mut buf = [0_u8; 16]; + stream.read_exact(&mut buf).expect("read tcp tiny"); + stream.write_all(&buf).expect("write tcp tiny"); + }); + let mut stream = TcpStream::connect(addr).expect("connect tcp tiny"); + for _ in 0..16 { + stream.write_all(b"x").expect("write tiny byte"); + } + let mut out = [0_u8; 16]; + stream.read_exact(&mut out).expect("read tiny echo"); + server.join().expect("join tcp server"); + } + Op::UdpEcho => { + let server = UdpSocket::bind("127.0.0.1:0").expect("bind udp server"); + let addr = server.local_addr().expect("udp addr"); + let handle = thread::spawn(move || { + let mut buf = [0_u8; 32]; + let (n, peer) = server.recv_from(&mut buf).expect("recv udp"); + server.send_to(&buf[..n], peer).expect("send udp"); + }); + let client = UdpSocket::bind("127.0.0.1:0").expect("bind udp client"); + client.send_to(b"hello", addr).expect("send udp client"); + let mut buf = [0_u8; 5]; + let (n, _) = client.recv_from(&mut buf).expect("recv udp client"); + assert_eq!(n, 5); + handle.join().expect("join udp server"); + } + Op::PipeEcho => { + let out = Command::new("/bin/sh") + .args(["-c", "printf hello | cat >/dev/null"]) + .output() + .expect("run pipe echo"); + assert!(out.status.success(), "pipe command failed: {out:?}"); + } + Op::PipeThroughput | Op::PipeBackpressure => { + #[cfg(unix)] + { + let (mut left, mut right) = UnixStream::pair().expect("unix stream pair"); + let payload = vec![9_u8; 64 * 1024]; + let expected_len = payload.len(); + let reader = thread::spawn(move || { + let mut out = vec![0_u8; expected_len]; + right.read_exact(&mut out).expect("pipe read"); + out + }); + left.write_all(&payload).expect("pipe write"); + let out = reader.join().expect("join pipe reader"); + assert_eq!(out.len(), payload.len()); + } + #[cfg(not(unix))] + { + let out = Command::new("/bin/sh") + .args(["-c", "printf hello | cat >/dev/null"]) + .output() + .expect("run pipe fallback"); + assert!(out.status.success(), "pipe fallback failed: {out:?}"); + } + } + Op::CpuLoop => { + let mut acc = 0_u64; + for i in 0..2_000_000_u64 { + acc = acc.wrapping_add(i ^ (acc.rotate_left(7))); + } + std::hint::black_box(acc); + } + Op::AllocFree => { + let mut data = vec![0_u8; 4 * 1024 * 1024]; + for (i, byte) in data.iter_mut().enumerate() { + *byte = (i % 251) as u8; + } + std::hint::black_box(data); + } + } +} + +fn arg_value(args: &[String], flag: &str) -> Option { + args.iter() + .position(|a| a == flag) + .and_then(|i| args.get(i + 1).cloned()) +} + +fn run_node_exit_phases() -> Vec<(&'static str, u128)> { + let total_start = Instant::now(); + let spawn_start = Instant::now(); + let mut child = Command::new("node") + .args(["-e", "process.exit(0)"]) + .spawn() + .expect("spawn node failed"); + let spawn_ns = spawn_start.elapsed().as_nanos(); + + let wait_start = Instant::now(); + let status = child.wait().expect("wait node child"); + let wait_ns = wait_start.elapsed().as_nanos(); + assert!(status.success(), "expected exit 0, got {status:?}"); + + vec![ + ("total", total_start.elapsed().as_nanos()), + ("spawn", spawn_ns), + ("wait_reap", wait_ns), + ] +} + +fn run_node_fanout_phases() -> Vec<(&'static str, u128)> { + let total_start = Instant::now(); + let spawn_start = Instant::now(); + let mut children = Vec::new(); + for _ in 0..8 { + children.push( + Command::new("node") + .args(["-e", "process.exit(0)"]) + .spawn() + .expect("spawn node failed"), + ); + } + let spawn_ns = spawn_start.elapsed().as_nanos(); + + let wait_start = Instant::now(); + for mut child in children { + let status = child.wait().expect("wait node child"); + assert!(status.success(), "expected exit 0, got {status:?}"); + } + let wait_ns = wait_start.elapsed().as_nanos(); + + vec![ + ("total", total_start.elapsed().as_nanos()), + ("spawn_batch", spawn_ns), + ("wait_reap_batch", wait_ns), + ] +} + +fn run_phases_once(op: Op) -> Option> { + match op { + Op::NodeExit => Some(run_node_exit_phases()), + Op::NodeFanout | Op::NodeReapStorm => Some(run_node_fanout_phases()), + _ => None, + } +} + +fn write_phase_json(op_name: &str, samples: &[(String, Vec)]) { + let mut out = String::with_capacity(1024); + out.push_str("{\"layer\":\"native\",\"op\":\""); + out.push_str(op_name); + out.push_str("\",\"unit\":\"ns\",\"phases\":{"); + for (phase_index, (phase, values)) in samples.iter().enumerate() { + if phase_index > 0 { + out.push(','); + } + out.push('"'); + out.push_str(phase); + out.push_str("\":["); + for (sample_index, value) in values.iter().enumerate() { + if sample_index > 0 { + out.push(','); + } + out.push_str(&value.to_string()); + } + out.push(']'); + } + out.push_str("}}"); + println!("{out}"); +} + +fn main() { + let args: Vec = std::env::args().skip(1).collect(); + + let op = match arg_value(&args, "--op").as_deref() { + Some("spawn_exit") => Op::SpawnExit, + Some("exec_capture") => Op::ExecCapture, + Some("node_stdout_discard_2b") => Op::NodeStdoutDiscard2b, + Some("node_stdout_capture_2b") => Op::NodeStdoutCapture2b, + Some("node_stdout_listener_only_2b") => Op::NodeStdoutListenerOnly2b, + Some("node_exit") => Op::NodeExit, + Some("node_fanout") => Op::NodeFanout, + Some("node_reap_storm") => Op::NodeReapStorm, + Some("pipe_chain") => Op::PipeChain, + Some("fs_stat") => Op::FsStat, + Some("fs_write") => Op::FsWrite, + Some("fs_read") => Op::FsRead, + Some("fs_open_close") => Op::FsOpenClose, + Some("fs_mkdir_rmdir") => Op::FsMkdirRmdir, + Some("fs_rename") => Op::FsRename, + Some("fs_readdir") => Op::FsReaddir, + Some("fs_fsync") => Op::FsFsync, + Some("dns_lookup") => Op::DnsLookup, + Some("dns_concurrent") => Op::DnsConcurrent, + Some("tcp_connect") => Op::TcpConnect, + Some("tcp_echo") => Op::TcpEcho, + Some("tcp_concurrent") => Op::TcpConcurrent, + Some("tcp_throughput") => Op::TcpThroughput, + Some("tcp_tiny_writes") => Op::TcpTinyWrites, + Some("udp_echo") => Op::UdpEcho, + Some("pipe_echo") => Op::PipeEcho, + Some("pipe_throughput") => Op::PipeThroughput, + Some("pipe_backpressure") => Op::PipeBackpressure, + Some("cpu_loop") => Op::CpuLoop, + Some("alloc_free") => Op::AllocFree, + other => { + eprintln!("unknown --op {other:?}"); + std::process::exit(2); + } + }; + let op_name = match op { + Op::SpawnExit => "spawn_exit", + Op::ExecCapture => "exec_capture", + Op::NodeStdoutDiscard2b => "node_stdout_discard_2b", + Op::NodeStdoutCapture2b => "node_stdout_capture_2b", + Op::NodeStdoutListenerOnly2b => "node_stdout_listener_only_2b", + Op::NodeExit => "node_exit", + Op::NodeFanout => "node_fanout", + Op::NodeReapStorm => "node_reap_storm", + Op::PipeChain => "pipe_chain", + Op::FsStat => "fs_stat", + Op::FsWrite => "fs_write", + Op::FsRead => "fs_read", + Op::FsOpenClose => "fs_open_close", + Op::FsMkdirRmdir => "fs_mkdir_rmdir", + Op::FsRename => "fs_rename", + Op::FsReaddir => "fs_readdir", + Op::FsFsync => "fs_fsync", + Op::DnsLookup => "dns_lookup", + Op::DnsConcurrent => "dns_concurrent", + Op::TcpConnect => "tcp_connect", + Op::TcpEcho => "tcp_echo", + Op::TcpConcurrent => "tcp_concurrent", + Op::TcpThroughput => "tcp_throughput", + Op::TcpTinyWrites => "tcp_tiny_writes", + Op::UdpEcho => "udp_echo", + Op::PipeEcho => "pipe_echo", + Op::PipeThroughput => "pipe_throughput", + Op::PipeBackpressure => "pipe_backpressure", + Op::CpuLoop => "cpu_loop", + Op::AllocFree => "alloc_free", + }; + let iters: usize = arg_value(&args, "--iters") + .and_then(|s| s.parse().ok()) + .unwrap_or(300); + let warmup: usize = arg_value(&args, "--warmup") + .and_then(|s| s.parse().ok()) + .unwrap_or(30); + let phases = args.iter().any(|arg| arg == "--phases"); + + let total = warmup + iters; + if phases { + let Some(first) = run_phases_once(op) else { + eprintln!("--phases is not supported for --op {op_name}"); + std::process::exit(2); + }; + let mut phase_samples = first + .into_iter() + .map(|(name, _)| (name.to_string(), Vec::with_capacity(iters))) + .collect::>(); + for i in 0..total { + let phase_values = run_phases_once(op).expect("checked phase support"); + if i >= warmup { + for (phase_name, value) in phase_values { + if let Some((_, values)) = phase_samples + .iter_mut() + .find(|(name, _)| name == phase_name) + { + values.push(value); + } + } + } + } + write_phase_json(op_name, &phase_samples); + return; + } + + let mut samples: Vec = Vec::with_capacity(iters); + for i in 0..total { + let t = Instant::now(); + run_once(op, i); + let ns = t.elapsed().as_nanos(); + if i >= warmup { + samples.push(ns); + } + } + + // Hand-built JSON (no serde dep): {"layer":"native","op":..,"unit":"ns","samples":[..]} + let mut out = String::with_capacity(samples.len() * 8 + 64); + out.push_str("{\"layer\":\"native\",\"op\":\""); + out.push_str(op_name); + out.push_str("\",\"unit\":\"ns\",\"samples\":["); + for (i, s) in samples.iter().enumerate() { + if i > 0 { + out.push(','); + } + out.push_str(&s.to_string()); + } + out.push_str("]}"); + println!("{out}"); +} From 4a49b42799f7d27ee9f7ccf60e4839311344928c Mon Sep 17 00:00:00 2001 From: Nathan Flurry Date: Fri, 26 Jun 2026 15:14:20 -0700 Subject: [PATCH 2/2] feat(sidecar): add GetResourceSnapshot resource-introspection wire hook --- .../protocol/secure_exec_sidecar_v1.bare | 35 +++- crates/sidecar/src/protocol.rs | 29 ++++ packages/core/src/generated-protocol.ts | 153 ++++++++++++++++-- packages/core/src/request-payloads.ts | 5 + packages/core/src/response-payloads.ts | 101 ++++++++++++ packages/core/src/sidecar-process.ts | 73 +++++++++ packages/core/tests/request-payloads.test.ts | 8 + packages/core/tests/response-payloads.test.ts | 60 +++++++ 8 files changed, 446 insertions(+), 18 deletions(-) diff --git a/crates/sidecar/protocol/secure_exec_sidecar_v1.bare b/crates/sidecar/protocol/secure_exec_sidecar_v1.bare index 2cc288415..c1e71bdcc 100644 --- a/crates/sidecar/protocol/secure_exec_sidecar_v1.bare +++ b/crates/sidecar/protocol/secure_exec_sidecar_v1.bare @@ -334,6 +334,8 @@ type KillProcessRequest struct { type GetProcessSnapshotRequest void +type GetResourceSnapshotRequest void + type FindListenerRequest struct { host: optional port: optional @@ -413,7 +415,8 @@ type RequestPayload union { PersistenceLoadRequest | PersistenceFlushRequest | VmFetchRequest | - ExtEnvelope + ExtEnvelope | + GetResourceSnapshotRequest } type RequestFrame struct { @@ -558,6 +561,33 @@ type ProcessSnapshotResponse struct { processes: list } +type QueueSnapshotEntry struct { + name: str + category: str + depth: u64 + highWater: u64 + capacity: u64 + fillPercent: u64 +} + +type ResourceSnapshotResponse struct { + runningProcesses: u64 + exitedProcesses: u64 + fdTables: u64 + openFds: u64 + pipes: u64 + pipeBufferedBytes: u64 + ptys: u64 + ptyBufferedInputBytes: u64 + ptyBufferedOutputBytes: u64 + sockets: u64 + socketListeners: u64 + socketConnections: u64 + socketBufferedBytes: u64 + socketDatagramQueueLen: u64 + queueSnapshots: list +} + type SocketStateEntry struct { processId: str host: optional @@ -656,7 +686,8 @@ type ResponsePayload union { PersistenceFlushedResponse | RejectedResponse | VmFetchResponse | - ExtEnvelope + ExtEnvelope | + ResourceSnapshotResponse } type ResponseFrame struct { diff --git a/crates/sidecar/src/protocol.rs b/crates/sidecar/src/protocol.rs index 55f0d8425..9613aa6e6 100644 --- a/crates/sidecar/src/protocol.rs +++ b/crates/sidecar/src/protocol.rs @@ -441,6 +441,9 @@ fn to_generated_request_payload( RequestPayload::GetProcessSnapshot(_) => { generated_protocol::RequestPayload::GetProcessSnapshotRequest } + RequestPayload::GetResourceSnapshot(_) => { + generated_protocol::RequestPayload::GetResourceSnapshotRequest + } RequestPayload::FindListener(inner) => { generated_protocol::RequestPayload::FindListenerRequest(inner.clone()) } @@ -541,6 +544,9 @@ fn from_generated_request_payload( generated_protocol::RequestPayload::GetProcessSnapshotRequest => { RequestPayload::GetProcessSnapshot(GetProcessSnapshotRequest {}) } + generated_protocol::RequestPayload::GetResourceSnapshotRequest => { + RequestPayload::GetResourceSnapshot(GetResourceSnapshotRequest {}) + } generated_protocol::RequestPayload::FindListenerRequest(inner) => { RequestPayload::FindListener(inner) } @@ -668,6 +674,9 @@ fn to_generated_response_payload( }, ) } + ResponsePayload::ResourceSnapshot(inner) => { + generated_protocol::ResponsePayload::ResourceSnapshotResponse(inner.clone()) + } ResponsePayload::ListenerSnapshot(inner) => { generated_protocol::ResponsePayload::ListenerSnapshotResponse(inner.clone()) } @@ -801,6 +810,9 @@ fn from_generated_response_payload( .collect(), }) } + generated_protocol::ResponsePayload::ResourceSnapshotResponse(inner) => { + ResponsePayload::ResourceSnapshot(inner) + } generated_protocol::ResponsePayload::ListenerSnapshotResponse(inner) => { ResponsePayload::ListenerSnapshot(inner) } @@ -1195,6 +1207,7 @@ pub enum RequestPayload { CloseStdin(CloseStdinRequest), KillProcess(KillProcessRequest), GetProcessSnapshot(GetProcessSnapshotRequest), + GetResourceSnapshot(GetResourceSnapshotRequest), FindListener(FindListenerRequest), FindBoundUdp(FindBoundUdpRequest), VmFetch(VmFetchRequest), @@ -1228,6 +1241,7 @@ pub enum ResponsePayload { StdinClosed(StdinClosedResponse), ProcessKilled(ProcessKilledResponse), ProcessSnapshot(ProcessSnapshotResponse), + ResourceSnapshot(ResourceSnapshotResponse), ListenerSnapshot(ListenerSnapshotResponse), BoundUdpSnapshot(BoundUdpSnapshotResponse), VmFetchResult(VmFetchResponse), @@ -1364,6 +1378,9 @@ pub type KillProcessRequest = crate::wire::KillProcessRequest; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct GetProcessSnapshotRequest {} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct GetResourceSnapshotRequest {} + pub type FindListenerRequest = crate::wire::FindListenerRequest; pub type FindBoundUdpRequest = crate::wire::FindBoundUdpRequest; @@ -1439,6 +1456,10 @@ pub type ProcessSnapshotEntry = crate::wire::ProcessSnapshotEntry; pub type ProcessSnapshotResponse = crate::wire::ProcessSnapshotResponse; +pub type QueueSnapshotEntry = crate::wire::QueueSnapshotEntry; + +pub type ResourceSnapshotResponse = crate::wire::ResourceSnapshotResponse; + pub type SocketStateEntry = crate::wire::SocketStateEntry; pub type ListenerSnapshotResponse = crate::wire::ListenerSnapshotResponse; @@ -1524,6 +1545,7 @@ impl_bare_newtype_union_enum!( PersistenceFlush(PersistenceFlushRequest) = 26, VmFetch(VmFetchRequest) = 27, Ext(ExtEnvelope) = 28, + GetResourceSnapshot(GetResourceSnapshotRequest) = 29, } ); @@ -1563,6 +1585,7 @@ impl_bare_newtype_union_enum!( Rejected(RejectedResponse) = 28, VmFetchResult(VmFetchResponse) = 29, ExtResult(ExtEnvelope) = 30, + ResourceSnapshot(ResourceSnapshotResponse) = 31, } ); @@ -2127,6 +2150,7 @@ enum ExpectedResponseKind { StdinClosed, ProcessKilled, ProcessSnapshot, + ResourceSnapshot, ListenerSnapshot, BoundUdpSnapshot, VmFetchResult, @@ -2172,6 +2196,7 @@ impl ExpectedResponseKind { Self::StdinClosed => "stdin_closed", Self::ProcessKilled => "process_killed", Self::ProcessSnapshot => "process_snapshot", + Self::ResourceSnapshot => "resource_snapshot", Self::ListenerSnapshot => "listener_snapshot", Self::BoundUdpSnapshot => "bound_udp_snapshot", Self::VmFetchResult => "vm_fetch_result", @@ -2231,6 +2256,7 @@ impl RequestPayload { | Self::CloseStdin(_) | Self::KillProcess(_) | Self::GetProcessSnapshot(_) + | Self::GetResourceSnapshot(_) | Self::FindListener(_) | Self::FindBoundUdp(_) | Self::VmFetch(_) @@ -2263,6 +2289,7 @@ impl RequestPayload { Self::CloseStdin(_) => ExpectedResponseKind::StdinClosed, Self::KillProcess(_) => ExpectedResponseKind::ProcessKilled, Self::GetProcessSnapshot(_) => ExpectedResponseKind::ProcessSnapshot, + Self::GetResourceSnapshot(_) => ExpectedResponseKind::ResourceSnapshot, Self::FindListener(_) => ExpectedResponseKind::ListenerSnapshot, Self::FindBoundUdp(_) => ExpectedResponseKind::BoundUdpSnapshot, Self::VmFetch(_) => ExpectedResponseKind::VmFetchResult, @@ -2315,6 +2342,7 @@ impl ResponsePayload { | Self::StdinClosed(_) | Self::ProcessKilled(_) | Self::ProcessSnapshot(_) + | Self::ResourceSnapshot(_) | Self::ListenerSnapshot(_) | Self::BoundUdpSnapshot(_) | Self::VmFetchResult(_) @@ -2348,6 +2376,7 @@ impl ResponsePayload { Self::StdinClosed(_) => "stdin_closed", Self::ProcessKilled(_) => "process_killed", Self::ProcessSnapshot(_) => "process_snapshot", + Self::ResourceSnapshot(_) => "resource_snapshot", Self::ListenerSnapshot(_) => "listener_snapshot", Self::BoundUdpSnapshot(_) => "bound_udp_snapshot", Self::VmFetchResult(_) => "vm_fetch_result", diff --git a/packages/core/src/generated-protocol.ts b/packages/core/src/generated-protocol.ts index b3c7540c6..fb8be727d 100644 --- a/packages/core/src/generated-protocol.ts +++ b/packages/core/src/generated-protocol.ts @@ -1781,6 +1781,8 @@ export function writeKillProcessRequest(bc: bare.ByteCursor, x: KillProcessReque export type GetProcessSnapshotRequest = null +export type GetResourceSnapshotRequest = null + function read22(bc: bare.ByteCursor): u16 | null { return bare.readBool(bc) ? bare.readU16(bc) : null } @@ -2020,6 +2022,7 @@ export type RequestPayload = | { readonly tag: "PersistenceFlushRequest"; readonly val: PersistenceFlushRequest } | { readonly tag: "VmFetchRequest"; readonly val: VmFetchRequest } | { readonly tag: "ExtEnvelope"; readonly val: ExtEnvelope } + | { readonly tag: "GetResourceSnapshotRequest"; readonly val: GetResourceSnapshotRequest } export function readRequestPayload(bc: bare.ByteCursor): RequestPayload { const offset = bc.offset @@ -2083,6 +2086,8 @@ export function readRequestPayload(bc: bare.ByteCursor): RequestPayload { return { tag: "VmFetchRequest", val: readVmFetchRequest(bc) } case 28: return { tag: "ExtEnvelope", val: readExtEnvelope(bc) } + case 29: + return { tag: "GetResourceSnapshotRequest", val: null } default: { bc.offset = offset throw new bare.BareError(offset, "invalid tag") @@ -2233,6 +2238,10 @@ export function writeRequestPayload(bc: bare.ByteCursor, x: RequestPayload): voi writeExtEnvelope(bc, x.val) break } + case "GetResourceSnapshotRequest": { + bare.writeU8(bc, 29) + break + } } } @@ -2793,6 +2802,110 @@ export function writeProcessSnapshotResponse(bc: bare.ByteCursor, x: ProcessSnap write27(bc, x.processes) } +export type QueueSnapshotEntry = { + readonly name: string + readonly category: string + readonly depth: u64 + readonly highWater: u64 + readonly capacity: u64 + readonly fillPercent: u64 +} + +export function readQueueSnapshotEntry(bc: bare.ByteCursor): QueueSnapshotEntry { + return { + name: bare.readString(bc), + category: bare.readString(bc), + depth: bare.readU64(bc), + highWater: bare.readU64(bc), + capacity: bare.readU64(bc), + fillPercent: bare.readU64(bc), + } +} + +export function writeQueueSnapshotEntry(bc: bare.ByteCursor, x: QueueSnapshotEntry): void { + bare.writeString(bc, x.name) + bare.writeString(bc, x.category) + bare.writeU64(bc, x.depth) + bare.writeU64(bc, x.highWater) + bare.writeU64(bc, x.capacity) + bare.writeU64(bc, x.fillPercent) +} + +function read28(bc: bare.ByteCursor): readonly QueueSnapshotEntry[] { + const len = bare.readUintSafe(bc) + if (len === 0) { + return [] + } + const result = [readQueueSnapshotEntry(bc)] + for (let i = 1; i < len; i++) { + result[i] = readQueueSnapshotEntry(bc) + } + return result +} + +function write28(bc: bare.ByteCursor, x: readonly QueueSnapshotEntry[]): void { + bare.writeUintSafe(bc, x.length) + for (let i = 0; i < x.length; i++) { + writeQueueSnapshotEntry(bc, x[i]) + } +} + +export type ResourceSnapshotResponse = { + readonly runningProcesses: u64 + readonly exitedProcesses: u64 + readonly fdTables: u64 + readonly openFds: u64 + readonly pipes: u64 + readonly pipeBufferedBytes: u64 + readonly ptys: u64 + readonly ptyBufferedInputBytes: u64 + readonly ptyBufferedOutputBytes: u64 + readonly sockets: u64 + readonly socketListeners: u64 + readonly socketConnections: u64 + readonly socketBufferedBytes: u64 + readonly socketDatagramQueueLen: u64 + readonly queueSnapshots: readonly QueueSnapshotEntry[] +} + +export function readResourceSnapshotResponse(bc: bare.ByteCursor): ResourceSnapshotResponse { + return { + runningProcesses: bare.readU64(bc), + exitedProcesses: bare.readU64(bc), + fdTables: bare.readU64(bc), + openFds: bare.readU64(bc), + pipes: bare.readU64(bc), + pipeBufferedBytes: bare.readU64(bc), + ptys: bare.readU64(bc), + ptyBufferedInputBytes: bare.readU64(bc), + ptyBufferedOutputBytes: bare.readU64(bc), + sockets: bare.readU64(bc), + socketListeners: bare.readU64(bc), + socketConnections: bare.readU64(bc), + socketBufferedBytes: bare.readU64(bc), + socketDatagramQueueLen: bare.readU64(bc), + queueSnapshots: read28(bc), + } +} + +export function writeResourceSnapshotResponse(bc: bare.ByteCursor, x: ResourceSnapshotResponse): void { + bare.writeU64(bc, x.runningProcesses) + bare.writeU64(bc, x.exitedProcesses) + bare.writeU64(bc, x.fdTables) + bare.writeU64(bc, x.openFds) + bare.writeU64(bc, x.pipes) + bare.writeU64(bc, x.pipeBufferedBytes) + bare.writeU64(bc, x.ptys) + bare.writeU64(bc, x.ptyBufferedInputBytes) + bare.writeU64(bc, x.ptyBufferedOutputBytes) + bare.writeU64(bc, x.sockets) + bare.writeU64(bc, x.socketListeners) + bare.writeU64(bc, x.socketConnections) + bare.writeU64(bc, x.socketBufferedBytes) + bare.writeU64(bc, x.socketDatagramQueueLen) + write28(bc, x.queueSnapshots) +} + export type SocketStateEntry = { readonly processId: string readonly host: string | null @@ -2816,11 +2929,11 @@ export function writeSocketStateEntry(bc: bare.ByteCursor, x: SocketStateEntry): write0(bc, x.path) } -function read28(bc: bare.ByteCursor): SocketStateEntry | null { +function read29(bc: bare.ByteCursor): SocketStateEntry | null { return bare.readBool(bc) ? readSocketStateEntry(bc) : null } -function write28(bc: bare.ByteCursor, x: SocketStateEntry | null): void { +function write29(bc: bare.ByteCursor, x: SocketStateEntry | null): void { bare.writeBool(bc, x != null) if (x != null) { writeSocketStateEntry(bc, x) @@ -2833,12 +2946,12 @@ export type ListenerSnapshotResponse = { export function readListenerSnapshotResponse(bc: bare.ByteCursor): ListenerSnapshotResponse { return { - listener: read28(bc), + listener: read29(bc), } } export function writeListenerSnapshotResponse(bc: bare.ByteCursor, x: ListenerSnapshotResponse): void { - write28(bc, x.listener) + write29(bc, x.listener) } export type BoundUdpSnapshotResponse = { @@ -2847,12 +2960,12 @@ export type BoundUdpSnapshotResponse = { export function readBoundUdpSnapshotResponse(bc: bare.ByteCursor): BoundUdpSnapshotResponse { return { - socket: read28(bc), + socket: read29(bc), } } export function writeBoundUdpSnapshotResponse(bc: bare.ByteCursor, x: BoundUdpSnapshotResponse): void { - write28(bc, x.socket) + write29(bc, x.socket) } export enum SignalDispositionAction { @@ -2915,7 +3028,7 @@ export function writeSignalHandlerRegistration(bc: bare.ByteCursor, x: SignalHan bare.writeU32(bc, x.flags) } -function read29(bc: bare.ByteCursor): ReadonlyMap { +function read30(bc: bare.ByteCursor): ReadonlyMap { const len = bare.readUintSafe(bc) const result = new Map() for (let i = 0; i < len; i++) { @@ -2930,7 +3043,7 @@ function read29(bc: bare.ByteCursor): ReadonlyMap): void { +function write30(bc: bare.ByteCursor, x: ReadonlyMap): void { bare.writeUintSafe(bc, x.size) for (const kv of x) { bare.writeU32(bc, kv[0]) @@ -2946,13 +3059,13 @@ export type SignalStateResponse = { export function readSignalStateResponse(bc: bare.ByteCursor): SignalStateResponse { return { processId: bare.readString(bc), - handlers: read29(bc), + handlers: read30(bc), } } export function writeSignalStateResponse(bc: bare.ByteCursor, x: SignalStateResponse): void { bare.writeString(bc, x.processId) - write29(bc, x.handlers) + write30(bc, x.handlers) } export type ZombieTimerCountResponse = { @@ -3106,6 +3219,7 @@ export type ResponsePayload = | { readonly tag: "RejectedResponse"; readonly val: RejectedResponse } | { readonly tag: "VmFetchResponse"; readonly val: VmFetchResponse } | { readonly tag: "ExtEnvelope"; readonly val: ExtEnvelope } + | { readonly tag: "ResourceSnapshotResponse"; readonly val: ResourceSnapshotResponse } export function readResponsePayload(bc: bare.ByteCursor): ResponsePayload { const offset = bc.offset @@ -3173,6 +3287,8 @@ export function readResponsePayload(bc: bare.ByteCursor): ResponsePayload { return { tag: "VmFetchResponse", val: readVmFetchResponse(bc) } case 30: return { tag: "ExtEnvelope", val: readExtEnvelope(bc) } + case 31: + return { tag: "ResourceSnapshotResponse", val: readResourceSnapshotResponse(bc) } default: { bc.offset = offset throw new bare.BareError(offset, "invalid tag") @@ -3337,6 +3453,11 @@ export function writeResponsePayload(bc: bare.ByteCursor, x: ResponsePayload): v writeExtEnvelope(bc, x.val) break } + case "ResourceSnapshotResponse": { + bare.writeU8(bc, 31) + writeResourceSnapshotResponse(bc, x.val) + break + } } } @@ -3707,11 +3828,11 @@ export function writeSidecarRequestFrame(bc: bare.ByteCursor, x: SidecarRequestF writeSidecarRequestPayload(bc, x.payload) } -function read30(bc: bare.ByteCursor): JsonUtf8 | null { +function read31(bc: bare.ByteCursor): JsonUtf8 | null { return bare.readBool(bc) ? readJsonUtf8(bc) : null } -function write30(bc: bare.ByteCursor, x: JsonUtf8 | null): void { +function write31(bc: bare.ByteCursor, x: JsonUtf8 | null): void { bare.writeBool(bc, x != null) if (x != null) { writeJsonUtf8(bc, x) @@ -3727,14 +3848,14 @@ export type HostCallbackResultResponse = { export function readHostCallbackResultResponse(bc: bare.ByteCursor): HostCallbackResultResponse { return { invocationId: bare.readString(bc), - result: read30(bc), + result: read31(bc), error: read0(bc), } } export function writeHostCallbackResultResponse(bc: bare.ByteCursor, x: HostCallbackResultResponse): void { bare.writeString(bc, x.invocationId) - write30(bc, x.result) + write31(bc, x.result) write0(bc, x.error) } @@ -3747,14 +3868,14 @@ export type JsBridgeResultResponse = { export function readJsBridgeResultResponse(bc: bare.ByteCursor): JsBridgeResultResponse { return { callId: bare.readString(bc), - result: read30(bc), + result: read31(bc), error: read0(bc), } } export function writeJsBridgeResultResponse(bc: bare.ByteCursor, x: JsBridgeResultResponse): void { bare.writeString(bc, x.callId) - write30(bc, x.result) + write31(bc, x.result) write0(bc, x.error) } diff --git a/packages/core/src/request-payloads.ts b/packages/core/src/request-payloads.ts index b3c7af339..4959c432b 100644 --- a/packages/core/src/request-payloads.ts +++ b/packages/core/src/request-payloads.ts @@ -170,6 +170,9 @@ export type LiveRequestPayload = | { type: "get_process_snapshot"; } + | { + type: "get_resource_snapshot"; + } | { type: "find_listener"; host?: string; @@ -408,6 +411,8 @@ export function toGeneratedRequestPayload( }; case "get_process_snapshot": return { tag: "GetProcessSnapshotRequest", val: null }; + case "get_resource_snapshot": + return { tag: "GetResourceSnapshotRequest", val: null }; case "find_listener": return { tag: "FindListenerRequest", diff --git a/packages/core/src/response-payloads.ts b/packages/core/src/response-payloads.ts index a145576b7..bae6aa87d 100644 --- a/packages/core/src/response-payloads.ts +++ b/packages/core/src/response-payloads.ts @@ -30,6 +30,33 @@ export interface LiveSignalHandlerRegistration { flags: number; } +export interface LiveQueueSnapshotEntry { + name: string; + category: string; + depth: number; + high_water: number; + capacity: number; + fill_percent: number; +} + +export interface LiveResourceSnapshot { + running_processes: number; + exited_processes: number; + fd_tables: number; + open_fds: number; + pipes: number; + pipe_buffered_bytes: number; + ptys: number; + pty_buffered_input_bytes: number; + pty_buffered_output_bytes: number; + sockets: number; + socket_listeners: number; + socket_connections: number; + socket_buffered_bytes: number; + socket_datagram_queue_len: number; + queue_snapshots: LiveQueueSnapshotEntry[]; +} + export type LiveResponsePayload = | { type: "authenticated"; @@ -128,6 +155,9 @@ export type LiveResponsePayload = type: "process_snapshot"; processes: LiveProcessSnapshotEntry[]; } + | ({ + type: "resource_snapshot"; + } & LiveResourceSnapshot) | { type: "listener_snapshot"; listener?: LiveSocketStateEntry; @@ -288,6 +318,77 @@ export function fromGeneratedResponsePayload( type: "process_snapshot", processes: payload.val.processes.map(fromGeneratedProcessSnapshotEntry), }; + case "ResourceSnapshotResponse": + return { + type: "resource_snapshot", + running_processes: bigIntToSafeNumber( + payload.val.runningProcesses, + "resource_snapshot.running_processes", + ), + exited_processes: bigIntToSafeNumber( + payload.val.exitedProcesses, + "resource_snapshot.exited_processes", + ), + fd_tables: bigIntToSafeNumber( + payload.val.fdTables, + "resource_snapshot.fd_tables", + ), + open_fds: bigIntToSafeNumber( + payload.val.openFds, + "resource_snapshot.open_fds", + ), + pipes: bigIntToSafeNumber(payload.val.pipes, "resource_snapshot.pipes"), + pipe_buffered_bytes: bigIntToSafeNumber( + payload.val.pipeBufferedBytes, + "resource_snapshot.pipe_buffered_bytes", + ), + ptys: bigIntToSafeNumber(payload.val.ptys, "resource_snapshot.ptys"), + pty_buffered_input_bytes: bigIntToSafeNumber( + payload.val.ptyBufferedInputBytes, + "resource_snapshot.pty_buffered_input_bytes", + ), + pty_buffered_output_bytes: bigIntToSafeNumber( + payload.val.ptyBufferedOutputBytes, + "resource_snapshot.pty_buffered_output_bytes", + ), + sockets: bigIntToSafeNumber( + payload.val.sockets, + "resource_snapshot.sockets", + ), + socket_listeners: bigIntToSafeNumber( + payload.val.socketListeners, + "resource_snapshot.socket_listeners", + ), + socket_connections: bigIntToSafeNumber( + payload.val.socketConnections, + "resource_snapshot.socket_connections", + ), + socket_buffered_bytes: bigIntToSafeNumber( + payload.val.socketBufferedBytes, + "resource_snapshot.socket_buffered_bytes", + ), + socket_datagram_queue_len: bigIntToSafeNumber( + payload.val.socketDatagramQueueLen, + "resource_snapshot.socket_datagram_queue_len", + ), + queue_snapshots: payload.val.queueSnapshots.map((queue) => ({ + name: queue.name, + category: queue.category, + depth: bigIntToSafeNumber(queue.depth, "resource_snapshot.queue.depth"), + high_water: bigIntToSafeNumber( + queue.highWater, + "resource_snapshot.queue.high_water", + ), + capacity: bigIntToSafeNumber( + queue.capacity, + "resource_snapshot.queue.capacity", + ), + fill_percent: bigIntToSafeNumber( + queue.fillPercent, + "resource_snapshot.queue.fill_percent", + ), + })), + }; case "ListenerSnapshotResponse": return { type: "listener_snapshot", diff --git a/packages/core/src/sidecar-process.ts b/packages/core/src/sidecar-process.ts index 1dd665984..7b9e635a5 100644 --- a/packages/core/src/sidecar-process.ts +++ b/packages/core/src/sidecar-process.ts @@ -123,6 +123,33 @@ export interface SidecarProcessSnapshotEntry { exitCode: number | null; } +export interface SidecarQueueSnapshotEntry { + name: string; + category: string; + depth: number; + highWater: number; + capacity: number; + fillPercent: number; +} + +export interface SidecarResourceSnapshot { + runningProcesses: number; + exitedProcesses: number; + fdTables: number; + openFds: number; + pipes: number; + pipeBufferedBytes: number; + ptys: number; + ptyBufferedInputBytes: number; + ptyBufferedOutputBytes: number; + sockets: number; + socketListeners: number; + socketConnections: number; + socketBufferedBytes: number; + socketDatagramQueueLen: number; + queueSnapshots: SidecarQueueSnapshotEntry[]; +} + export interface SidecarZombieTimerCount { count: number; } @@ -1140,6 +1167,52 @@ export class SidecarProcess { return response.payload.processes.map(toSidecarProcessSnapshotEntry); } + async getResourceSnapshot( + session: AuthenticatedSession, + vm: CreatedVm, + ): Promise { + const response = await this.sendRequest({ + ownership: { + scope: "vm", + connection_id: session.connectionId, + session_id: session.sessionId, + vm_id: vm.vmId, + }, + payload: { + type: "get_resource_snapshot", + }, + }); + if (response.payload.type !== "resource_snapshot") { + throw new Error( + `unexpected get_resource_snapshot response: ${response.payload.type}`, + ); + } + return { + runningProcesses: response.payload.running_processes, + exitedProcesses: response.payload.exited_processes, + fdTables: response.payload.fd_tables, + openFds: response.payload.open_fds, + pipes: response.payload.pipes, + pipeBufferedBytes: response.payload.pipe_buffered_bytes, + ptys: response.payload.ptys, + ptyBufferedInputBytes: response.payload.pty_buffered_input_bytes, + ptyBufferedOutputBytes: response.payload.pty_buffered_output_bytes, + sockets: response.payload.sockets, + socketListeners: response.payload.socket_listeners, + socketConnections: response.payload.socket_connections, + socketBufferedBytes: response.payload.socket_buffered_bytes, + socketDatagramQueueLen: response.payload.socket_datagram_queue_len, + queueSnapshots: response.payload.queue_snapshots.map((queue) => ({ + name: queue.name, + category: queue.category, + depth: queue.depth, + highWater: queue.high_water, + capacity: queue.capacity, + fillPercent: queue.fill_percent, + })), + }; + } + async findBoundUdp( session: AuthenticatedSession, vm: CreatedVm, diff --git a/packages/core/tests/request-payloads.test.ts b/packages/core/tests/request-payloads.test.ts index 4e6f67c99..6b9008c35 100644 --- a/packages/core/tests/request-payloads.test.ts +++ b/packages/core/tests/request-payloads.test.ts @@ -108,6 +108,14 @@ describe("request payload conversion", () => { }); }); + it("maps resource snapshot requests", () => { + expect( + toGeneratedRequestPayload({ + type: "get_resource_snapshot", + }), + ).toEqual({ tag: "GetResourceSnapshotRequest", val: null }); + }); + it("maps host callback registration JSON fields", () => { expect( toGeneratedRequestPayload({ diff --git a/packages/core/tests/response-payloads.test.ts b/packages/core/tests/response-payloads.test.ts index 6c3f3b012..3a47b908f 100644 --- a/packages/core/tests/response-payloads.test.ts +++ b/packages/core/tests/response-payloads.test.ts @@ -119,6 +119,66 @@ describe("response payload conversion", () => { }); }); + it("maps resource snapshots", () => { + expect( + fromGeneratedResponsePayload({ + tag: "ResourceSnapshotResponse", + val: { + runningProcesses: 2n, + exitedProcesses: 1n, + fdTables: 2n, + openFds: 6n, + pipes: 1n, + pipeBufferedBytes: 12n, + ptys: 0n, + ptyBufferedInputBytes: 0n, + ptyBufferedOutputBytes: 0n, + sockets: 3n, + socketListeners: 1n, + socketConnections: 2n, + socketBufferedBytes: 256n, + socketDatagramQueueLen: 4n, + queueSnapshots: [ + { + name: "pending_process_events", + category: "queue", + depth: 1n, + highWater: 3n, + capacity: 128n, + fillPercent: 2n, + }, + ], + }, + }), + ).toEqual({ + type: "resource_snapshot", + running_processes: 2, + exited_processes: 1, + fd_tables: 2, + open_fds: 6, + pipes: 1, + pipe_buffered_bytes: 12, + ptys: 0, + pty_buffered_input_bytes: 0, + pty_buffered_output_bytes: 0, + sockets: 3, + socket_listeners: 1, + socket_connections: 2, + socket_buffered_bytes: 256, + socket_datagram_queue_len: 4, + queue_snapshots: [ + { + name: "pending_process_events", + category: "queue", + depth: 1, + high_water: 3, + capacity: 128, + fill_percent: 2, + }, + ], + }); + }); + it("maps generated toolkit registration to host callback registration", () => { expect( fromGeneratedResponsePayload({