Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions scripts/benchmarks/families/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { controlFamily } from "./control.js";
import { dnsFamily } from "./dns.js";
import { fsFamily } from "./fs.js";
import { netFamily } from "./net.js";
import { perfFindingsFamily } from "./perf-findings.js";
import { pipesFamily } from "./pipes.js";
import { processFamily } from "./process.js";

Expand All @@ -12,6 +13,7 @@ export const allFamilies = [
dnsFamily,
pipesFamily,
controlFamily,
perfFindingsFamily,
];

export const allOps = allFamilies.flat();
80 changes: 80 additions & 0 deletions scripts/benchmarks/families/perf-findings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import type { BenchmarkOp } from "../lib/layers.js";

/**
* Perf-finding regression ops.
*
* Each op exercises one hot-path anti-pattern surfaced by the 2026-06-30
* perf-slop scan (see ~/progress/secure-exec/2026-06-30-fuzz-perf-optimization-report/
* perf-slop-scan.md). The `program` runs identically under host Node and inside
* the guest VM, so `tax.emulation = guest.p50 / node.p50` is the guest overhead
* the corresponding fix is expected to shrink. Capture these numbers BEFORE the
* fix lands (baseline) and re-run AFTER (regression guard).
*
* Findings already covered by existing benches (no op here):
* - host_call.rs:429/435/452/534 (per-syscall lock + String clone) -> sync-bridge-floor
* - bridge.rs:1978 (args buffer clone) -> sync-bridge-floor --payload-bytes=65536
* - execution.rs:897 (loopback peer-pairing 50x10ms retry) -> net/tcp_connect_close
*
* Findings that could NOT be turned into a clean guest-vs-node op (documented so
* the gap is explicit, not silently dropped):
* - v8_host.rs:296 (serialize->deserialize per stream event): stdout/stderr IS
* the harness data channel, so flooding the stdio event path overruns the
* 4096 sidecar event buffer and hangs instead of timing. Needs a
* sidecar-internal per-event counter.
* - execution.rs:962 (byte-by-byte loopback-TLS read): a guest HTTPS loopback
* request currently ERRORS (`ERR_AGENTOS_NODE_SYNC_RPC: loopback TLS transport
* read`), so there is no successful transfer to time. The error on that exact
* path is itself worth a follow-up.
* - execution.rs:19315/19895 (HTTP/2 accept + backpressure polls): node:http2
* inside the guest hangs (unsupported), so no request completes to time.
*/

export const perfFindingsFamily: BenchmarkOp[] = [
{
// crates/sidecar/src/filesystem.rs:1284 — fs.write stdio handler clones the
// full write buffer into the Stdout/Stderr event. fs.writeSync(2, ...) forces
// the write synchronously through the sync bridge so the per-call clone cost
// is actually on the measured path (async stream .write() buffers and reads
// as ~0). 8 x 64KiB per iter stays well under the 4096 event-buffer bound.
family: "perf-finding",
name: "stdio_writeSync_8x64k",
nativeOp: "pipe_throughput",
fileLine: "crates/sidecar/src/filesystem.rs:1284",
reproducer: "8 synchronous fs.writeSync(2, 64KiB) inside VM (stdio buffer clone)",
program: `async () => {
const fs = await import("node:fs");
const buf = Buffer.alloc(64 * 1024, 7);
for (let k = 0; k < 8; k++) fs.writeSync(2, buf);
}`,
},
{
// crates/sidecar/src/execution.rs:2213 — guest Unix-socket accept loop sleeps
// a fixed 10ms on WouldBlock instead of async readiness. One connect+close
// pays one accept-latency tick. This is the clearest signal in the set
// (baseline ~30x guest/node) — the 10ms poll dominates a sub-ms host accept.
family: "perf-finding",
name: "unix_accept_latency",
nativeOp: "tcp_connect",
fileLine: "crates/sidecar/src/execution.rs:2213",
reproducer: "connect+close one Unix-domain socket inside VM (10ms accept poll)",
program: `async () => {
const net = await import("node:net");
const os = await import("node:os");
const path = await import("node:path");
const sock = path.join(
os.tmpdir(),
"perf-unix-" + process.pid + "-" + Math.random().toString(16).slice(2) + ".sock",
);
await new Promise((resolve, reject) => {
const server = net.createServer((s) => s.end());
server.on("error", reject);
server.listen(sock, () => {
const c = net.connect(sock);
c.on("error", reject);
c.on("close", () => server.close(() => resolve()));
c.end();
});
});
}`,
},
];
10 changes: 10 additions & 0 deletions scripts/benchmarks/run-benchmarks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,16 @@ run "sync-bridge-floor-phases" \
--sync-rpc-latency \
--bridge-phases

# Large-args sync bridge lane. Same no-op bridge RPC, but with a 64KiB payload so
# the per-call argument-buffer clone (bridge.rs:1978) is material. This is the
# standing regression guard for the args-serialization buffer reuse.
run "sync-bridge-floor-bigargs" \
scripts/benchmarks/sync-bridge-floor.bench.ts \
--iterations="${BENCH_SYNC_BRIDGE_BIGARGS_ITERATIONS:-10}" \
--warmup="${BENCH_SYNC_BRIDGE_BIGARGS_WARMUP:-2}" \
--call-counts="${BENCH_SYNC_BRIDGE_BIGARGS_CALL_COUNTS:-1,8,32}" \
--payload-bytes="${BENCH_SYNC_BRIDGE_BIGARGS_PAYLOAD_BYTES:-65536}"

# Focused DNS lookup floor. Splits the broad dns rows into warm, repeated,
# concurrent, and fresh-process lookup shapes.
run "dns-lookup-floor" \
Expand Down
Loading