mirror of
https://github.com/wirenboard/agent-vm.git
synced 2026-07-09 16:00:54 +00:00
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>
This commit is contained in:
parent
13850a81aa
commit
6513050a31
9 changed files with 358 additions and 78 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -21,7 +21,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "agent-vm"
|
||||
version = "0.1.18"
|
||||
version = "0.1.20"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ members = ["crates/agent-vm"]
|
|||
exclude = ["vendor/microsandbox"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.18"
|
||||
version = "0.1.20"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/wirenboard/agent-vm"
|
||||
|
|
|
|||
135
LAUNCH-PROFILE.md
Normal file
135
LAUNCH-PROFILE.md
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
# 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.26–1.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 1–4 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.68–1.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.
|
||||
```
|
||||
|
|
@ -350,12 +350,18 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
// image available — the user runs `agent-vm pull` explicitly to
|
||||
// fetch it.
|
||||
if !args.no_update_check {
|
||||
// Give the banner a baseline to compare against on *this* launch,
|
||||
// not just future ones: if the image is already cached but we have
|
||||
// no recorded pulled-digest yet, seed the marker from the cache
|
||||
// first, then probe.
|
||||
seed_pulled_marker_if_absent(image.as_str()).await;
|
||||
notify_if_update_available(image.as_str()).await;
|
||||
// The update banner is purely informational, so keep it OFF the
|
||||
// launch critical path: seed the baseline pulled-digest marker
|
||||
// (so the banner has something to compare against on this launch,
|
||||
// not just future ones) and probe the registry in the background.
|
||||
// The banner prints if the ~0.9s ghcr.io round-trip resolves
|
||||
// during boot; otherwise it's simply skipped and the next launch
|
||||
// catches up. Previously this was awaited and blocked every boot.
|
||||
let img = image.clone();
|
||||
tokio::spawn(async move {
|
||||
seed_pulled_marker_if_absent(&img).await;
|
||||
notify_if_update_available(&img).await;
|
||||
});
|
||||
}
|
||||
|
||||
// Snapshot host credentials into per-project token files and place
|
||||
|
|
@ -951,67 +957,20 @@ pub async fn launch(agent: Agent, args: Args) -> Result<i32> {
|
|||
// docker compose up, env-var exports). Runs once per launch with
|
||||
// PWD set to the project dir; non-zero exit aborts the launch
|
||||
// with the same exit code.
|
||||
// 3. Adds the microsandbox MITM CA to the `chrome` user's NSS DB
|
||||
// so chromium (launched by chrome-devtools-mcp under that user
|
||||
// via /usr/local/bin/agent-vm-chrome-mcp) verifies the
|
||||
// intercepted TLS chain instead of failing every HTTPS page
|
||||
// with ERR_CERT_AUTHORITY_INVALID. The CA file
|
||||
// `/usr/local/share/ca-certificates/microsandbox-ca.crt` is
|
||||
// written into the guest by agentd at boot, so this can't be
|
||||
// baked into the image — the per-boot CA is what we have to
|
||||
// inject. Trust string is `-t C,,` (trusted issuer of *server*
|
||||
// certs only; the leading `T` would also mark it as a trusted
|
||||
// issuer of client certs, which we don't want). Only injected
|
||||
// when the chrome MCP is enabled: gating both here and in
|
||||
// `secrets::write_default_claude_root_state` on the same env
|
||||
// var keeps the opt-out actually opt-out — no sudo, no
|
||||
// certutil fork.
|
||||
// (Importing the microsandbox MITM CA into the `chrome` user's NSS
|
||||
// DB — needed because chromium on Linux ignores the system CA bundle
|
||||
// and honours only its per-user NSS DB, so the chrome-devtools MCP
|
||||
// would otherwise fail every HTTPS page with ERR_CERT_AUTHORITY_INVALID
|
||||
// — used to run here, a ~270ms `certutil` fork on *every* launch's
|
||||
// critical path. It now lives in the in-image `agent-vm-chrome-mcp`
|
||||
// wrapper, so it runs once when the chrome MCP actually starts: off
|
||||
// the launch path, and skipped entirely when chrome is unused. The CA
|
||||
// is per-install (not bakeable into the shared image); see
|
||||
// images/Dockerfile.)
|
||||
let project_guest_path_escaped = shell_escape(&project_guest_path);
|
||||
// Env-var semantics: `AGENT_VM_NO_CHROME_MCP` set to *any* value
|
||||
// (including empty) opts out. Matches `AGENT_VM_PROFILE` /
|
||||
// `AGENT_VM_DEBUG_CONFIG` in the same codebase. Unconventional vs
|
||||
// "VAR=0 means off" — documented here so the next reader doesn't
|
||||
// re-flag it.
|
||||
let chrome_mcp_prelude = if std::env::var_os("AGENT_VM_NO_CHROME_MCP").is_some() {
|
||||
String::new()
|
||||
} else {
|
||||
// Image-fresh NSS DB starts empty on every launch (the sandbox
|
||||
// name carries the launcher PID — see session.rs — so every
|
||||
// invocation creates a fresh rootfs upper layer rather than
|
||||
// reusing a prior one), so `certutil -A` always runs against
|
||||
// the same baseline; no chance of accumulating duplicate trust
|
||||
// entries across boots.
|
||||
//
|
||||
// Failure modes worth surfacing (vs silently swallowing with
|
||||
// `|| true` like the original Phase 7 patch): sudoers
|
||||
// dropped/world-writable, NSS DB corrupted, CA file missing
|
||||
// because agentd's TLS init didn't run, or chrome user
|
||||
// removed in a downstream image. Without a warning, every
|
||||
// chrome MCP HTTPS request would return
|
||||
// `ERR_CERT_AUTHORITY_INVALID` with no breadcrumb back to the
|
||||
// launcher. Stderr is captured in a temp file and tail'd on
|
||||
// failure so the user sees the actual certutil/sudo error
|
||||
// rather than a generic "non-zero exit".
|
||||
String::from(
|
||||
"if [ -f /usr/local/share/ca-certificates/microsandbox-ca.crt ] \\\n\
|
||||
&& [ -d /home/chrome/.pki/nssdb ]; then\n\
|
||||
\t_cu_err=$(mktemp)\n\
|
||||
\tif ! sudo -u chrome -n -- certutil -d sql:/home/chrome/.pki/nssdb -A \\\n\
|
||||
\t\t-t C,, -n microsandbox \\\n\
|
||||
\t\t-i /usr/local/share/ca-certificates/microsandbox-ca.crt 2>\"$_cu_err\"; then\n\
|
||||
\t\techo \"==> warning: failed to install microsandbox CA into chrome NSS DB;\" >&2\n\
|
||||
\t\techo \"==> HTTPS in chrome-devtools MCP will fail with ERR_CERT_AUTHORITY_INVALID.\" >&2\n\
|
||||
\t\techo \"==> certutil/sudo stderr:\" >&2\n\
|
||||
\t\tsed 's/^/==> /' \"$_cu_err\" >&2\n\
|
||||
\tfi\n\
|
||||
\trm -f \"$_cu_err\"\n\
|
||||
fi\n",
|
||||
)
|
||||
};
|
||||
let prelude = format!(
|
||||
"sed -i '/^nameserver .*:/d' /etc/resolv.conf 2>/dev/null || true\n\
|
||||
[ -t 0 ] || exec < /dev/null\n\
|
||||
{chrome_mcp_prelude}\
|
||||
_hook={path}/.agent-vm.runtime.sh\n\
|
||||
if [ -f \"$_hook\" ]; then\n\
|
||||
\techo \"==> sourcing $_hook\" >&2\n\
|
||||
|
|
|
|||
|
|
@ -311,10 +311,76 @@ pub struct HostGitIdentity {
|
|||
/// which makes git refuse to commit rather than silently attribute to
|
||||
/// `agent-vm`.
|
||||
pub fn discover_host_git_identity() -> Option<HostGitIdentity> {
|
||||
if let Some(id) = gh_api_user_identity() {
|
||||
// `gh api user` is an HTTPS round-trip to api.github.com that costs
|
||||
// ~0.3–1.3s and runs on the *pre-boot critical path* of every
|
||||
// launch — yet the answer (your name/email/login) almost never
|
||||
// changes. Cache the resolved identity with a short TTL so only the
|
||||
// first launch in the window pays the network cost; subsequent
|
||||
// launches resolve it instantly. The cache holds only display-level
|
||||
// strings (already validated by `is_config_safe`), never tokens.
|
||||
if let Some(id) = read_identity_cache() {
|
||||
return Some(id);
|
||||
}
|
||||
host_git_config_identity()
|
||||
let id = gh_api_user_identity().or_else(host_git_config_identity);
|
||||
if let Some(ref id) = id {
|
||||
write_identity_cache(id);
|
||||
}
|
||||
id
|
||||
}
|
||||
|
||||
/// TTL for the cached host git identity. Long enough to take the
|
||||
/// `gh api user` round-trip off essentially every launch, short enough
|
||||
/// that switching `gh` accounts / editing `git config user.*` is
|
||||
/// reflected the same day.
|
||||
const GIT_IDENTITY_CACHE_TTL: std::time::Duration = std::time::Duration::from_secs(24 * 3600);
|
||||
|
||||
fn identity_cache_path() -> Option<PathBuf> {
|
||||
Some(
|
||||
crate::host_paths::state_root()?
|
||||
.join("cache")
|
||||
.join("host-git-identity"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Read the cached identity if present and fresher than
|
||||
/// [`GIT_IDENTITY_CACHE_TTL`]. Re-validates with [`is_config_safe`] so a
|
||||
/// tampered cache file can't smuggle gitconfig sections into the guest.
|
||||
fn read_identity_cache() -> Option<HostGitIdentity> {
|
||||
let p = identity_cache_path()?;
|
||||
let age = std::fs::metadata(&p).ok()?.modified().ok()?.elapsed().ok()?;
|
||||
if age > GIT_IDENTITY_CACHE_TTL {
|
||||
return None;
|
||||
}
|
||||
let data = std::fs::read_to_string(&p).ok()?;
|
||||
let mut lines = data.lines();
|
||||
let name = lines.next()?.to_string();
|
||||
let email = lines.next()?.to_string();
|
||||
let gh = lines.next().unwrap_or("");
|
||||
if name.is_empty() || email.is_empty() || !is_config_safe(&name) || !is_config_safe(&email) {
|
||||
return None;
|
||||
}
|
||||
Some(HostGitIdentity {
|
||||
name,
|
||||
email,
|
||||
gh_login: (!gh.is_empty()).then(|| gh.to_string()),
|
||||
})
|
||||
}
|
||||
|
||||
/// Persist the resolved identity (best-effort; cache misses are cheap).
|
||||
fn write_identity_cache(id: &HostGitIdentity) {
|
||||
let Some(p) = identity_cache_path() else {
|
||||
return;
|
||||
};
|
||||
if let Some(parent) = p.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let body = format!(
|
||||
"{}\n{}\n{}\n",
|
||||
id.name,
|
||||
id.email,
|
||||
id.gh_login.as_deref().unwrap_or("")
|
||||
);
|
||||
let _ = atomic_write(&p, body.as_bytes(), 0o600);
|
||||
}
|
||||
|
||||
/// Cap on how long we'll wait for `gh api user`. The call is an HTTPS
|
||||
|
|
|
|||
|
|
@ -134,6 +134,21 @@ RUN echo 'chrome:x:9999:9999:Chrome MCP:/home/chrome:/bin/bash' >> /etc/passwd \
|
|||
' echo "agent-vm-chrome-mcp: cannot cd to /home/chrome -- image misconfigured?" >&2' \
|
||||
' exit 1' \
|
||||
'}' \
|
||||
'# Import the microsandbox MITM CA into the chrome-owned NSS DB so' \
|
||||
'# chromium trusts the intercept proxy. chromium on Linux ignores' \
|
||||
'# the system CA bundle (which curl/node use), honouring only its' \
|
||||
'# built-in roots + this per-user NSS DB. Done here, once, at MCP' \
|
||||
'# startup -- NOT on every agent-vm launch (previously a ~270ms' \
|
||||
'# certutil fork in the launcher prelude) -- and skipped when' \
|
||||
'# chrome is unused (the MCP server is only spawned when enabled).' \
|
||||
'# The DB is chrome-owned (UID 9999), so drop to chrome to write it.' \
|
||||
'# Non-fatal: a failure should warn, not kill the MCP.' \
|
||||
'_ca=/usr/local/share/ca-certificates/microsandbox-ca.crt' \
|
||||
'if [ -f "$_ca" ]; then' \
|
||||
' sudo -u chrome -n -- certutil -d sql:/home/chrome/.pki/nssdb -A \' \
|
||||
' -t C,, -n microsandbox -i "$_ca" \' \
|
||||
' || echo "agent-vm-chrome-mcp: warning: failed to import microsandbox CA (HTTPS may fail ERR_CERT_AUTHORITY_INVALID)" >&2' \
|
||||
'fi' \
|
||||
'exec sudo -u chrome -H -n \' \
|
||||
' --preserve-env=NODE_EXTRA_CA_CERTS,SSL_CERT_FILE,CURL_CA_BUNDLE,REQUESTS_CA_BUNDLE \' \
|
||||
' --preserve-env=PATH,HTTP_PROXY,HTTPS_PROXY,NO_PROXY,http_proxy,https_proxy,no_proxy \' \
|
||||
|
|
@ -219,17 +234,6 @@ RUN apt-get update \
|
|||
&& printf '%s\n' '{' ' "storage-driver": "fuse-overlayfs"' '}' \
|
||||
> /etc/docker/daemon.json
|
||||
|
||||
# Cache-bust for the three agent installers below. The workflow
|
||||
# passes the hourly timestamp tag as the value so each scheduled
|
||||
# build invalidates exactly these RUN layers (and the sanity check
|
||||
# that follows) while leaving the heavy apt/chromium/docker/node
|
||||
# layers cached. Without this the GHA layer cache reuses the
|
||||
# `curl … install.sh | bash` layers indefinitely, so hourly cron
|
||||
# runs never picked up new claude-code/codex/opencode releases.
|
||||
# Local `images/build.sh` builds leave the default empty value and
|
||||
# get the normal Docker layer-cache behaviour.
|
||||
ARG AGENT_INSTALL_CACHEBUST=
|
||||
|
||||
# Claude Code official installer.
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
|
|
|
|||
23
profiling/build_legacy.sh
Executable file
23
profiling/build_legacy.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/bin/bash
|
||||
set -u
|
||||
cd "$(dirname "$0")"
|
||||
# wait for the main matrix to finish so we don't thrash CPU
|
||||
until grep -q "ALL VARIANTS BUILT\|ABORTING" /tmp/variants_build.log 2>/dev/null; do sleep 10; done
|
||||
SRC=linux-6.12.68; OUT=/tmp/variants
|
||||
echo "### BUILD heavy_legacy ($(date +%T))"
|
||||
cp /tmp/cfg_heavy_legacy "$SRC/.config"
|
||||
( cd "$SRC" && make olddefconfig >/dev/null 2>&1 )
|
||||
printf " effective: NETFILTER=%s NF_CONNTRACK=%s NF_NAT=%s IP_NF_IPTABLES=%s NF_TABLES=%s BRIDGE=%s IP6_NF=%s\n" \
|
||||
"$(grep -c '^CONFIG_NETFILTER=y' $SRC/.config)" "$(grep -c '^CONFIG_NF_CONNTRACK=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_NF_NAT=y' $SRC/.config)" "$(grep -c '^CONFIG_IP_NF_IPTABLES=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_NF_TABLES=y' $SRC/.config)" "$(grep -c '^CONFIG_BRIDGE=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_IP6_NF_IPTABLES=y' $SRC/.config)"
|
||||
t0=$(date +%s)
|
||||
( cd "$SRC" && rm -f .version && make -j14 KBUILD_BUILD_TIMESTAMP="Tue Feb 17 16:15:12 CET 2026" KBUILD_BUILD_USER=root KBUILD_BUILD_HOST=libkrunfw vmlinux ) >"$OUT/build-heavy_legacy.log" 2>&1
|
||||
rc=$?; t1=$(date +%s)
|
||||
[ $rc -ne 0 ] && { echo "BUILD FAILED rc=$rc"; tail -5 "$OUT/build-heavy_legacy.log"; exit 1; }
|
||||
python3 bin2cbundle.py -t vmlinux "$SRC/vmlinux" kernel.c
|
||||
cc -fPIC -DABI_VERSION=5 -shared -Wl,-soname,libkrunfw.so.5 -o "$OUT/libkrunfw-heavy_legacy.so.5.2.1" kernel.c
|
||||
strip "$OUT/libkrunfw-heavy_legacy.so.5.2.1"
|
||||
echo " -> heavy_legacy built in $((t1-t0))s, $(du -h $OUT/libkrunfw-heavy_legacy.so.5.2.1|cut -f1)"
|
||||
echo "### HEAVY_LEGACY DONE + ALL BUILDS COMPLETE ($(date +%T))"
|
||||
45
profiling/build_variants.sh
Executable file
45
profiling/build_variants.sh
Executable file
|
|
@ -0,0 +1,45 @@
|
|||
#!/bin/bash
|
||||
# Build libkrunfw .so variants for a boot-time A/B. One shared tree, serial
|
||||
# incremental builds. Each .so saved to /tmp/variants/.
|
||||
set -u
|
||||
cd "$(dirname "$0")"
|
||||
SRC=linux-6.12.68
|
||||
OUT=/tmp/variants
|
||||
mkdir -p "$OUT"
|
||||
JOBS=14
|
||||
KFLAGS=(KBUILD_BUILD_TIMESTAMP="Tue Feb 17 16:15:12 CET 2026" KBUILD_BUILD_USER=root KBUILD_BUILD_HOST=libkrunfw)
|
||||
|
||||
build_one() {
|
||||
local name="$1" seed="$2"
|
||||
echo "===================================================================="
|
||||
echo "### BUILD $name ($(date +%T))"
|
||||
cp "$seed" "$SRC/.config" || return 1
|
||||
( cd "$SRC" && make olddefconfig >/dev/null 2>&1 ) || { echo "olddefconfig FAILED"; return 1; }
|
||||
# report effective symbols
|
||||
printf " effective: KVM=%s KVM_INTEL=%s KVM_AMD=%s NETFILTER=%s NF_CONNTRACK=%s NF_TABLES=%s BRIDGE=%s POSIX_MQUEUE=%s DEFERRED=%s\n" \
|
||||
"$(grep -c '^CONFIG_KVM=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_KVM_INTEL=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_KVM_AMD=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_NETFILTER=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_NF_CONNTRACK=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_NF_TABLES=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_BRIDGE=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_POSIX_MQUEUE=y' $SRC/.config)" \
|
||||
"$(grep -c '^CONFIG_DEFERRED_STRUCT_PAGE_INIT=y' $SRC/.config)"
|
||||
local t0=$(date +%s)
|
||||
( cd "$SRC" && rm -f .version && make -j$JOBS "${KFLAGS[@]}" vmlinux ) >"$OUT/build-$name.log" 2>&1
|
||||
local rc=$?
|
||||
local t1=$(date +%s)
|
||||
if [ $rc -ne 0 ]; then echo " BUILD FAILED rc=$rc (see $OUT/build-$name.log)"; tail -5 "$OUT/build-$name.log"; return 1; fi
|
||||
echo " vmlinux built in $((t1-t0))s"
|
||||
python3 bin2cbundle.py -t vmlinux "$SRC/vmlinux" kernel.c || { echo "bin2cbundle FAILED"; return 1; }
|
||||
cc -fPIC -DABI_VERSION=5 -shared -Wl,-soname,libkrunfw.so.5 -o "$OUT/libkrunfw-$name.so.5.2.1" kernel.c || { echo "link FAILED"; return 1; }
|
||||
strip "$OUT/libkrunfw-$name.so.5.2.1"
|
||||
echo " -> $OUT/libkrunfw-$name.so.5.2.1 ($(du -h "$OUT/libkrunfw-$name.so.5.2.1" | cut -f1)) total $((t1-t0))s+bundle"
|
||||
}
|
||||
|
||||
for v in stock stock_kvm heavy_nonf heavy heavy_deferred; do
|
||||
build_one "$v" "/tmp/cfg_$v" || { echo "ABORTING at $v"; exit 1; }
|
||||
done
|
||||
echo "### ALL VARIANTS BUILT ($(date +%T))"
|
||||
ls -la "$OUT"/*.so.5.2.1
|
||||
48
profiling/measure_variants.sh
Executable file
48
profiling/measure_variants.sh
Executable file
|
|
@ -0,0 +1,48 @@
|
|||
#!/bin/bash
|
||||
# Wall-clock `create` per libkrunfw variant. Interleaved across variants to
|
||||
# cancel host drift; drop_caches between rounds; mean/stdev. QUIET host only.
|
||||
set -u
|
||||
export PATH="/home/boger/work/board/tmp/agent-vm/.agent-vm-rust/rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin:$PATH"
|
||||
export AGENT_VM_NO_CHROME_MCP=1 # strip the run-phase certutil; we measure create only
|
||||
export MSB_PATH=/home/boger/work/board/tmp/agent-vm/vendor/microsandbox/target/release/msb
|
||||
AGENTVM=/home/boger/work/board/tmp/agent-vm/target/release/agent-vm
|
||||
LIBSO=/home/boger/work/board/tmp/agent-vm/vendor/microsandbox/target/release/libkrunfw.so.5.2.1
|
||||
VAR=/tmp/variants
|
||||
PROJ=/root/profproj
|
||||
VARIANTS=(stock stock_kvm heavy_nonf heavy_legacy heavy heavy_deferred)
|
||||
ROUNDS=${1:-10}
|
||||
|
||||
cd "$PROJ" || exit 1
|
||||
swap() { cp "$VAR/libkrunfw-$1.so.5.2.1" "$LIBSO" || exit 1; }
|
||||
create() { local mem="$1"; AGENT_VM_PROFILE=1 "$AGENTVM" shell --memory "$mem" true 2>&1 \
|
||||
| sed 's/\x1b\[[0-9;]*m//g' | grep -oE 'create:[[:space:]]+[0-9.]+s' | grep -oE '[0-9.]+'; }
|
||||
mstat() { tr ' ' '\n' <<<"$1" | awk 'NF{n++;v=$1;s+=v;ss+=v*v;if(mn==""||v<mn)mn=v;if(v>mx)mx=v} END{if(!n){print "no data";exit} m=s/n;d=ss/n-m*m;if(d<0)d=0;printf "mean=%.3f sd=%.3f min=%.3f max=%.3f n=%d",m,sqrt(d),mn,mx,n}'; }
|
||||
|
||||
echo "### boot check (each variant must run a command) ###"
|
||||
for v in "${VARIANTS[@]}"; do
|
||||
swap "$v"; o=$(AGENT_VM_PROFILE=1 "$AGENTVM" shell echo OK_$v 2>&1)
|
||||
echo "$o" | grep -q "OK_$v" && echo " $v: boots OK" || { echo " $v: *** FAILED ***"; echo "$o"|tail -3; }
|
||||
done
|
||||
|
||||
declare -A S
|
||||
echo; echo "### interleaved, 1 GiB / 2 vCPU, $ROUNDS rounds, drop_caches per round ###"
|
||||
for c in $(seq 1 "$ROUNDS"); do
|
||||
sync; echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
|
||||
for v in "${VARIANTS[@]}"; do swap "$v"; S[$v]="${S[$v]:-} $(create 1)"; done
|
||||
echo " round $c done"
|
||||
done
|
||||
|
||||
declare -A S4
|
||||
echo; echo "### heavy vs heavy_deferred @ 4 GiB, 6 rounds (does deferred help at more RAM?) ###"
|
||||
for c in $(seq 1 6); do
|
||||
sync; echo 3 > /proc/sys/vm/drop_caches 2>/dev/null
|
||||
for v in heavy heavy_deferred; do swap "$v"; S4[$v]="${S4[$v]:-} $(create 4)"; done
|
||||
done
|
||||
|
||||
echo; echo "================= RESULTS: create wall-seconds @1GiB ================="
|
||||
for v in "${VARIANTS[@]}"; do printf "%-16s %s\n" "$v" "$(mstat "${S[$v]}")"; done
|
||||
echo "------- raw @1GiB -------"
|
||||
for v in "${VARIANTS[@]}"; do printf "%-16s%s\n" "$v" "${S[$v]}"; done
|
||||
echo; echo "================= @4GiB heavy vs deferred ================="
|
||||
for v in heavy heavy_deferred; do printf "%-16s %s\n" "$v" "$(mstat "${S4[$v]}")"; done
|
||||
swap heavy; echo "(restored heavy .so)"
|
||||
Loading…
Add table
Add a link
Reference in a new issue