Skip to content

perf: cut create-session latency via agent-SDK heap snapshot#124

Merged
NathanFlurry merged 8 commits into
mainfrom
perf-regression
Jun 25, 2026
Merged

perf: cut create-session latency via agent-SDK heap snapshot#124
NathanFlurry merged 8 commits into
mainfrom
perf-regression

Conversation

@NathanFlurry

@NathanFlurry NathanFlurry commented Jun 24, 2026

Copy link
Copy Markdown
Member

Cuts agentOS create-session latency by snapshotting the agent SDK so it is evaluated once per sidecar and reused across sessions, instead of re-evaluated on every `createSession`.

Results (Pi agent, llmock)

  • loadPiSdkRuntime: 615ms → 0ms (the SDK is in the snapshot; module-loading round-trips eliminated)
  • sessionCreate p50: 1498ms → 636–764ms (warm), with the first session warm via pre-warm
  • build-once per sidecar; per-session config + isolation preserved; real prompt-turn (write-tool round-trip) verified

What's here

  • iters 1–9 — diagnosis (sync-RPC round-trip counters, module-load read/compile/evaluate split) + the reader-relocation work that reads guest module source directly on the V8 session thread.
  • Snapshot (2b+2d):
    • `create_snapshot_with_userland` extends the bridge snapshot to also evaluate the agent-SDK bundle into the same context; `SnapshotCache` is process-wide and dep-keyed by `sha256(bridge + bundle)`.
    • Bridge exposes a namespaced `__secureExecGuestCreateRequire` so a raw-IIFE SDK bundle can bind `require` for node builtins.
    • Protocol wiring: `BinaryFrame::Execute`/`WarmSnapshot` carry `userland_code` (both IPC layers); `JsRuntimeConfig.snapshotUserlandCode` → `GuestRuntimeConfig` → session restore.
    • Pre-warm at VM-create so the first session is warm; V8 is initialized on the persistent main thread at startup so pre-warm never runs on a transient thread (which corrupts the platform).
  • Tests: snapshot Parts 21 (primitive, zero re-eval), 22 (real pi bundle, env-gated), 23 (cross-thread build→restore), 24 (session-level isolation: global + SDK object + `Array.prototype`).

Notes

  • The agent-os client side (resolving an agent's snapshot bundle, the `agent.snapshot` opt-in flag, and the adapter eval/entry split) is a separate change in the agent-os repo; it consumes the wire field added here.
  • Follow-up: ship the bundle as a VFS path-ref (it is currently inlined in the trusted VM config, which is why `DEFAULT_MAX_FRAME_BYTES` was raised 1MB→16MB).

…YNC_RPC_TRACE). Finding: ~50 fs RPCs/session, not a storm; bulk tax is 855-file module loading via the inline module-reader path.
…TRACE). Finding: loadPiSdkRuntime is evaluate-dominated (~510ms run vs 115ms resolve/load/format/compile for 500 mods) — code-cache buys only ~38ms; real lever is isolate reuse / post-init V8 snapshot.
…CRPC_LAT). ROOT CAUSE: 5,100 round-trips x 139us = 709ms = all of loadPiSdkRuntime. VM tax is IPC round-trips (frame I/O over pipe per host_call), not V8 compute. Lever: cut round-trip count (combine per-module resolve/load/format) and/or per-call latency.
…ModuleReader trait (owned in v8-runtime, no circular dep) + ModuleResolveState.guest_reader + Phase-5 load fast-path that reads source directly when a reader is present, else bridge IPC. Inert until the higher crate plumbs the reader (iter6); compiles, gate green, sessionCreate unchanged. (iter4 structural verdict folded in; see ~/.agents notes.)
… reads module source DIRECTLY (step 1 LOAD). ModuleReaderHandle newtype rides RuntimeCommand -> dispatch -> SessionCommand::SetModuleReader -> session thread; sidecar impls GuestModuleReader over HostDirModuleReader (cloned per call site). VALIDATED: round-trips 5100->4600, sessionCreate 1498->1463, gate green, session creates correctly. Resolve+format (the bulk) is iter9.
…wraps reader+cache, exact bridge ModuleResolver semantics). Round-trips 4600->2725, sessionCreate 1463->1444, gate GREEN (resolution correct). Finding: 139us/call was mostly the resolution/read WORK not IPC overhead, so wall-clock barely moved (loadPiSdkRuntime 633->615). Cumulative steps1+2: 1498->1444 (~3.6%), loadPiSdkRuntime 708->615 (~13%). VM tax is largely intrinsic (compile/evaluate/openat2), not IPC.
@railway-app
railway-app Bot temporarily deployed to secure-exec / secure-exec-pr-124 June 24, 2026 23:12 Destroyed
@railway-app
railway-app Bot temporarily deployed to rivet-frontend / secure-exec-pr-124 June 24, 2026 23:12 Destroyed
@railway-app

railway-app Bot commented Jun 24, 2026

Copy link
Copy Markdown

🚅 Deployed to the secure-exec-pr-124 environment in rivet-frontend

Service Status Web Updated (UTC)
secure-exec 😴 Sleeping (View Logs) Jun 24, 2026 at 11:22 pm

🚅 Deployed to the secure-exec-pr-124 environment in secure-exec

Service Status Web Updated (UTC)
secure-exec 😴 Sleeping (View Logs) Web Jun 24, 2026 at 11:18 pm

…tocol + VM-config wiring

Review hardening:
- H-1: process.versions wrapper in SNAPSHOT_USERLAND_PREP now defers to the
  bridge's live getter post-restore (falls back to the static identity only
  while it throws during snapshot creation) instead of a permanent static pin,
  so restored sessions read real bridge-provided versions, not frozen constants.
- M-1: bounded-by-default heap cap (DEFAULT_HEAP_LIMIT_MB=128) applied in both
  create_isolate and create_isolate_from_snapshot when no explicit limit is
  given; the OOM guard is always installed so a guest heap bomb terminates its
  own isolate instead of fatal-aborting the shared process.
- Part 25 regression test: per-session process.versions flows through post-restore.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@railway-app
railway-app Bot temporarily deployed to secure-exec / secure-exec-pr-124 June 25, 2026 08:04 Destroyed
@railway-app
railway-app Bot temporarily deployed to rivet-frontend / secure-exec-pr-124 June 25, 2026 08:04 Destroyed
@NathanFlurry
NathanFlurry merged commit 2e95d51 into main Jun 25, 2026
11 of 12 checks passed
NathanFlurry added a commit that referenced this pull request Jun 25, 2026
…t ctors + clippy) (#127)

#124 (perf: agent-SDK heap snapshot) landed while its CI was red, leaving main
failing on fmt, clippy, and 13 missing-field/unused test-compile errors. Repair:

- rustfmt 1.96.0: reformat embedded_runtime.rs, snapshot.rs, sidecar/execution.rs,
  javascript_v8.rs (chain/closure collapse the pinned toolchain expects).
- userland_code: add the WarmSnapshot/Execute snapshot field (#124) to 11
  ipc_binary BinaryFrame test constructors + 2 embedded_runtime_session frames
  (empty = bridge-only, the documented default); update the hand-built
  WarmSnapshot trailing-byte test for the new 2-field wire layout.
- clippy -D warnings: checked_div in queue_tracker, is_multiple_of in
  sidecar/execution, while-let in the js backpressure test, and allow(dead_code)
  on the host_dir test's source-included module (SessionModuleReader is used
  elsewhere in the crate).

Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant