wirenboard-agent-vm/LAUNCH-PROFILE.md
Evgeny Boger 6513050a31 v0.1.20: cut launch latency — cache gh identity, non-blocking update banner, chrome CA off the hot path
Profiling (LAUNCH-PROFILE.md) found the launch was dominated by PRE-BOOT host
work, not the VM boot. Three fixes, ~2.5s -> ~1.8s on a warm launch:

- secrets.rs: cache the host git identity (24h TTL, validated strings only).
  `gh api user` was an uncached ~1.29s HTTPS round-trip on every launch's
  critical path (added in v0.1.14) — the main 1.3s->2.5s regression.
- run.rs: spawn the ghcr.io update-banner check instead of awaiting it
  (~0.9s off the hot path; banner still fires, just doesn't block boot).
- run.rs + images/Dockerfile: move the chrome-MCP MITM-CA `certutil` import
  out of the per-launch in-guest prelude into the agent-vm-chrome-mcp wrapper.
  It now runs once at MCP startup (as the chrome user that owns the NSS DB),
  off the launch path and skipped when chrome is unused (~270ms). Verified
  end-to-end: the proxy serves a `microsandbox CA`-signed cert that validates
  against the CA the wrapper imports.

Also: kernel A/B showed the nested-virt libkrunfw rebuild adds only ~190ms
(KVM ~100, conntrack ~62); DEFERRED_STRUCT_PAGE_INIT and split_irqchip have no
wall-clock effect. Build/measure scripts under profiling/.

NOTE: the Dockerfile (wrapper) and binary (prelude removal) must ship together
— the image must be rebuilt to :latest with the new wrapper before this binary
is published, or chrome MCP loses its CA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:22:34 +00:00

135 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# agent-vm launch profiling — findings
Host: AMD EPYC, 16 vCPU, nested virt (`/dev/kvm`, `kvm_amd.nested=1`). Image cached.
All headline numbers are **wall-clock**, from interleaved A/Bs (host drift cancels),
`drop_caches` between rounds where noted. Measured with `AGENT_VM_PROFILE=1` plus
sub-timers added to `run.rs` (pre-boot phases + build/spawn+boot+relay) and the
runtime's own `runtime.log` wall timestamps.
## TL;DR
A launch in a GitHub-remote repo broke down (default 2 vCPU / 2 GiB) as:
```
pre: session+reap ............... 0.3 ms
pre: update-check (ghcr HEAD) ... 886 ms ← notify_if_update_available (banner only)
pre: repo-detect ................ 2 ms
pre: secrets (gh auth token) .... 40 ms
pre: gh api user ................ 1290 ms ← v0.1.14 author identity ← THE regression
pre: TOTAL pre-boot ............. 2220 ms ← LARGER than the actual VM boot
create (guest kernel boot) ..... 1500 ms
run (incl. chrome certutil) .... 270 ms
stop / remove .................. 50 ms
TOTAL ~4.1 s
```
**~2.2s of the launch happens before the kernel even starts, and it's two uncached
blocking GitHub/registry network round-trips.** The original `[profile] create` timer
started *after* all of this, which is why earlier profiling missed it entirely.
## The regression: `gh api user` (pre-boot), not the kernel
`discover_host_git_identity()` (added in commit `0c3bb51`, **v0.1.14** — "bake host
gh/git identity into the guest gitconfig", the exact regression window) calls
**`gh api user` first**, an HTTPS round-trip to api.github.com, falling back to the
instant local gitconfig only if it fails. No cache. Measured ~1.261.31s here, every
launch. Adding ~1.29s to a ~1.3s baseline ≈ the reported 2.5s.
The ghcr.io **update-check** (`notify_if_update_available`, commit `bfab9d3`) is a second
per-launch blocking HEAD (~0.89s) purely to print a "newer image available" banner.
### Fixes (implemented on this branch, measured)
1. **Cache the resolved git identity** (`secrets.rs`, 24h TTL, validated strings only,
never tokens; preserves the canonical `gh` identity incl. `gh_login`). Pays the
`gh api user` cost once per day instead of every launch.
2. **Make the update-check non-blocking** (`run.rs`): spawn it concurrently with boot
instead of awaiting it. Banner still prints (during boot); never delays launch.
| | pre-boot | total wall |
|---|---|---|
| before | ~2220 ms | ~4100 ms |
| after, run 1 (cold identity cache) | ~1300 ms | ~3280 ms |
| **after, warm cache** | **~32 ms** | **~1900 ms** |
`gh api user` 1290ms → 22µs; update-check 886ms → ~0. **~2.19s off every launch after
the first.** No downside, no kernel rebuild. This alone undoes the regression.
## Secondary: the ~1.5s guest-kernel boot floor
`create` ≈ guest kernel boot (`build()` is ~3µs; `entering VM → agentd core.ready`).
The console (`hvc0`) attaches ~1.3s in, so early boot isn't visible in `kernel.log`;
the A/B below is the real attribution. Five+1 kernels built from one tree
(configs grepped, not assumed), 10 interleaved rounds @1 GiB, `drop_caches` per round:
| kernel | config | create mean ± sd | Δ |
|---|---|---|---|
| stock | upstream libkrunfw (no KVM, no netfilter) | 1.488 ± 0.10 s | — |
| stock+KVM | + CONFIG_KVM/KVM_INTEL/KVM_AMD | 1.587 ± 0.15 s | **+99 ms (KVM)** |
| heavy_nonf | KVM, netfilter off (+mqueue) | 1.615 ± 0.12 s | ≈ stock+KVM ✓ |
| heavy_legacy | + conntrack/NAT/iptables-legacy/bridge | 1.677 ± 0.03 s | **+62 ms (conntrack)** |
| heavy (current) | + full nf_tables/XT/IPv6/VLAN | 1.680 ± 0.03 s | +3 ms (nf_tables ≈ 0) |
| heavy+deferred | heavy + DEFERRED_STRUCT_PAGE_INIT | 1.632 ± 0.03 s | no help |
**The nested-virt kernel rebuild adds only ~190ms total to boot** (KVM ~100ms,
conntrack/iptables ~62ms, nf_tables ~3ms). So it's a real but *minor* secondary cost:
- **KVM (~100ms)** is *required* for nested virt — irremovable.
- **conntrack/iptables-legacy (~62ms)** is the unavoidable cost of docker bridge+SNAT
(the conntrack hashtable auto-sizes from RAM). With `CONFIG_MODULES is not set` +
`nomodule`, it can't be made a module — it's built-in or absent. Drop it only if you
don't need docker networking by default.
- **nf_tables/XT/IPv6/VLAN (~3ms)** — droppable, but saves essentially nothing. Not
worth the docker-iptables-nft→legacy fallback risk.
### Two levers that do NOT work (verified, don't pursue)
- **`CONFIG_DEFERRED_STRUCT_PAGE_INIT=y`**: no help at 1 GiB, slightly *worse* at 4 GiB.
Its `defer_init()` heuristic only defers past a 128 MB section threshold after low
zones init; a 14 GiB single-node guest has nothing to defer (it targets TB-scale RAM).
- **`split_irqchip`**: ~515ms swing in the runtime's `boot_time_ms` metric but **zero
wall-clock effect** (1.91s vs 1.88s create). `boot_time_ms` excludes ~0.9s of early
boot and is a misleading proxy — rank kernels on wall-clock `create` only. vCPU count:
also negligible (1/2/4 ≈ 1.84/1.84/1.89s).
## Other real levers
- **Guest memory** (real wall-clock, but EPT/page-materialization under nested virt, not
struct-page init): create ≈ 1.49s @1G / 1.681.92s @2G / 2.9s @4G. Lower the default
(`AGENT_VM_MEMORY_GIB`, currently 2) for sessions that don't need 2 GiB (~0.2s+).
- **Chrome-MCP CA `certutil` (~270ms)** in the `run` phase — **fixed on this branch.**
`run.rs` used to run `sudo -u chrome certutil -A …` synchronously in the in-guest
prelude before exec'ing the agent, on *every* launch. But chromium ignores the system
CA bundle (which `update-ca-certificates` already populates at boot) and only honors
its per-user NSS DB, so the CA must be imported there or chrome-devtools-mcp fails
every HTTPS page with `ERR_CERT_AUTHORITY_INVALID`. The cert can't be baked into the
shared image (the CA is generated per-install on the host); the import repeated every
launch only because the NSS DB lives in the ephemeral rootfs. **Fix:** moved the import
into the in-image `agent-vm-chrome-mcp` wrapper (`images/Dockerfile`) — it runs once at
MCP startup, as the `chrome` user that owns the DB, off the launch critical path and
skipped entirely when chrome is unused. Measured: `run` phase 310ms → ~38ms.
**Coupling:** the `run.rs` and Dockerfile changes must ship together — until the
template image is rebuilt with the new wrapper, dropping the prelude import would leave
chrome MCP without the CA.
## Recommended order (by impact × safety)
1. **Cache the git identity** + **non-blocking update-check** — ~2.19s, implemented here,
zero downside, no rebuild. This *is* the regression fix.
2. **Chrome-MCP certutil moved into the image wrapper** — ~270ms off the `run` phase,
implemented on this branch (run.rs + images/Dockerfile; ship together).
3. **Lower default guest memory** if 2 GiB is more than agents need — ~0.2s+.
4. Kernel: leave it. The ~190ms it adds is mostly required (KVM, conntrack). Do **not**
enable deferred-page-init or chase split_irqchip. Optionally drop nf_tables/IPv6 NF to
shave ~3ms only if you also accept iptables-legacy.
## Reproduce
```bash
cd <github-remote repo>
AGENT_VM_PROFILE=1 agent-vm shell true # prints pre-boot phases + create/run/stop
# pre-boot network cost, in isolation:
time gh api user >/dev/null ; time curl -sI https://ghcr.io/v2/wirenboard/agent-vm-template/manifests/latest
# kernel A/B: build variants from libkrunfw-src (one tree, grep each .config), swap the
# .so next to msb, measure interleaved with drop_caches — see measure_variants.sh.
```