cocoonbox

Bench host measurements

Measurements and lessons from the bench host (GCP c3-standard-22-lssd).

Clone timing

Measured on 2026-09-30 on the cocoon-cb stack: provisioning about 30 s on a warm apt cache; snapshot save 48–64 s (13 GB, and ext4 has no reflink, so each clone copies its disk in full); clone to Ready 37 s for v1 (probe 22) and 20 s for v2 (probe 7443); hibernate 144–154 s (uncompressed push); wake 18–19 s.

History: the cocoon-cb store and CocoonSet clones, removed 2026-10-01. Measured 2026-09-30 over four v2 clones with a host tracer (1 s samples of the new CH process and of nvme0n1), in-guest timings through vm exec, and a page-cache residency probe.

Storage: snapshot cocoonbox-desktop-v2 keeps memory-ranges (8.0 GiB) and cow.raw (40 GiB sparse, 4.4 GiB used). That is on / (ext4) on nvme0n1, the GCP persistent disk. The two unused local NVMe drives are not used for this.

Quiet host (run 4, Ready at +23 s):

t (s) Event Evidence
0–6 cocoon vm clone copies cow.raw in full, because ext4 has no reflink 837 MiB read from the disk; host Dirty+Writeback rises to 4.5 GiB
7 CH runs; vk starts the post-clone vm exec vk log post-clone setup starting
7–22 the guest waits on disk I/O while the host flushes the copy CH is flat: minflt 28.3k, majflt 25, read_bytes 116 MB, RSS 504 MB, ~3 CPU ticks/s. Host writeback drains at ~290 MB/s. Guest drop_caches takes 14,670 ms and guest io pressure goes from avg10 11 to 76
22 host writeback reaches 11 MiB; both drop_caches calls (vk’s and the probe’s) finish in the same second guest kernel drop_caches: 3 at 08:42:13.17
22–23 the rest of the script and readiness networkd restart ~20 ms, cold read 16 ms, sync 101 ms

Guest memory page-in is not the cost. memory-ranges is mostly in the host page cache, and the guest touches about 0.5 GiB of it. vm exec over vsock works 0.66 s after restore (strace: CONNECT 1024 answered in 54 ms). The stall comes from direct I/O: CH opens writable disks with O_DIRECT, and direct I/O on a file whose page cache is still dirty waits until that range is written back.

Under other host writes (run 3, while another box was hibernating, with host Dirty already at 7.4 GiB), the copy itself was throttled: 78 s from CreatePod to CH start, and 91 s to Ready.

Run 5 (quiet host, the requested single instrumented clone). The memory file is the snapshot’s memory-ranges, 8 GiB, on /dev/nvme0n1p1 (ext4, nvme_card-pd), 36% resident in the host page cache at start. CH majflt stays at 0, read_bytes at 15.7 MB and RSS at 574 MB. Post-clone ran from 08:47:19.59 to 08:47:35.48 (15.9 s). Guest drop_caches finished at 35.40 and networkd reported Started at 35.48, while host dirty+writeback went from 4.3 GiB to 1 MiB over the same window. Run warm right after Ready, the same three commands take 0.010 s (drop_caches), 0.004 s (rewrite the networkd file) and 0.137 s (systemctl restart systemd-networkd).

After the move (2026-09-30, 10:04Z)

Metric Persistent disk (ext4) Local NVMe (XFS reflink)
Disk use per clone (df delta on /srv/cb) ~4.4 GiB copy 4 MiB and 5 MiB
cow.raw extents private copy 3913 extents with 0x2000 shared (xfs_io -c 'fiemap -v'), filefrag -v reports shared
Clone to Ready, quiet host 20–37 s 10.1 s cold (memory file not yet in page cache: CH read 2.4 GB, majflt 216), 3.9 s warm
vk post-clone … attempt_dur 15.9–16.3 s 4.45 s cold, 2.39 s warm
Hibernate (suspend to Suspended) 144–159 s 80 s
Wake to Ready 18–19 s 4.6 s (dev-agent and smoke)

The marker /etc/cocoonbox/nvme-move-marker survived hibernate, move and wake on dev-agent, and hibernate and wake on smoke.

Phase timings: hibernate both boxes 60 s + 159 s (serial), stop 4 s, re-check + mkfs + mount 3 s, rsync of 49 GB allocated (159 GB apparent) 287 s, verify (248-entry tree diff, cmp of meta.db and the v2 memory file) about 30 s, swap + fstab + quota + VK_NODE_STORAGE=320Gi 1 s, restart 32 s, wake dev-agent 4.6 s. The two boxes were unavailable for about 12 min.

VNC server: x11vnc (v2) vs TigerVNC (v3)

Measured on 2026-09-30 on the golden (clone of v2), real Xorg :0 with the dummy driver at 1280x800. The client is an RFB benchmark script on the host, through ssh -L to the guest’s 127.0.0.1:5900 (Raw encoding, 32 bpp). It answers fences and runs notify/request/provide for the extended clipboard. The driver script runs 45 s idle, a first connect, 20 s of static desktop, then 20 s full-frame and 20 s incremental against a fullscreen xfce4-terminal streaming base64. CPU comes from sampling /proc in the guest, where 100% is one vCPU.

  x11vnc 0.9.16 (v2) TigerVNC Xorg module 1.13.1 (chosen) x0vncserver 1.13.1
connect → banner (after 45 s idle) 151–212 ms 49–53 ms 52 ms
connect → first full update (request → update) 1226–1290 ms (1030–1133 ms) 65–72 ms (13–15 ms) 86 ms (23 ms)
full-frame Raw, static desktop 25–27 fps, 98–104 MiB/s, p99 102–194 ms, max ~520 ms 75.9 fps, 296.5 MiB/s, p99 53 ms, max 100 ms 75.3 fps, 294.3 MiB/s, p99 54 ms, max 100 ms
full-frame Raw, animation 20.9 fps, 81.8 MiB/s 23.7 fps, 92.6 MiB/s 27.8 fps, 108.7 MiB/s
incremental, animation 18.8 upd/s, 42.6 MiB/s, max gap 164 ms 30.1 upd/s, 66.4 MiB/s, max gap 138 ms 31.2 upd/s, 70.2 MiB/s, max gap 151 ms
gaps > 1 s (all runs) 0 0 0
ContinuousUpdates (-313) / Fence (-312) no / no yes / yes yes / yes
ExtendedClipboard UTF-8 caps offered (0x17000001), but the server closes the connection on an extended provide works both ways: guest xclip -o reads héllo 世界 ✓ sent by the client; client receives guést 世界 ✓ set in the guest no clipboard in 1.13: guest sees no owner, client receives nothing
CPU idle, no client 0% 0% (Xorg) 0%
CPU during animation, one client x11vnc 15% (+ Xorg ~13%); guest 48% of 4 vCPUs Xorg 62% (includes the ~13% Xorg spends drawing without VNC); guest 53% X0tigervnc 49% (+ Xorg ~13%); guest 54%
CPU during animation, no client 0% Xorg 17–18% (~13% without the module) 0%

Both TigerVNC variants are about 3x x11vnc on raw throughput and 15–20x faster to the first update. The module is chosen because it has the only working UTF-8 clipboard, needs no extra process, and gets damage straight from the X server. Its cost is about 4–5% of a vCPU in Xorg while the screen changes and nobody is connected. The ~4.5 s x11vnc stall seen earlier through the gRPC relay did not reproduce with this client over ssh -L. x11vnc’s slow first update after idle (about 1.1 s from request to update) did reproduce.

cua-driver regression on the module:

v3 smoke clone (probePort 7443) on NVMe:

Instrumented hibernate (2026-10-02)

Guest RAM page map of an idle warm clone (2026-10-02)

gen source extents (shared) FICLONE CH written / calls vm.snapshot hibernate new extents (shared) shared blocks wake
1 2 (0) 0.08 ms 264.8 MB / 2,527 0.716 s 0.834 s 5,052 (2,526) 96.9% 0.485 s
2 5,052 (2,526) 91.9 ms 32.0 MB / 1,419 0.210 s 0.300 s 6,848 (5,430) 99.6% 0.705 s
3 6,848 (2,658) 127.5 ms 33.6 MB / 1,411 0.251 s 0.326 s 7,392 (5,982) 99.6% 0.513 s
4 7,392 (2,731) 137.4 ms 33.0 MB / 1,403 0.256 s 0.340 s 7,654 (6,252) 99.6% 0.701 s
5 7,654 (2,741) 135.5 ms 32.3 MB / 1,394 0.254 s 0.337 s 7,833 (6,440) 99.6% 0.822 s
6 7,833 (2,749) 137.8 ms 34.2 MB / 1,420 0.271 s 0.353 s 7,902 (6,483) 99.6% 0.663 s
7 7,902 (2,755) 139.6 ms 59.6 MB / 1,912 0.327 s 0.416 s 7,980 (6,069) 99.3% 0.484 s
8 7,980 (2,824) 147.5 ms 32.3 MB / 1,398 0.261 s 0.343 s 8,581 (7,184) 99.6% 0.576 s
9 8,581 (2,829) 150.8 ms 35.6 MB / 1,432 0.275 s 0.359 s 8,628 (7,197) 99.6% 0.693 s
10 8,628 (2,834) 153.4 ms 32.5 MB / 1,422 0.273 s 0.355 s 8,840 (7,419) 99.6% 0.661 s
case snapshot memory-ranges extents (shared) wake read over the wake to first exec majflt / minflt
dense-hot 2 (0) 0.146 s 17 reads, 0.6 MB 0.213 s 1 / 4,804
dense-hot 2 (0) 0.145 s 923 reads, 5.2 MB 0.215 s 0 / 4,021
dense-cold 2 (0) 1.036 s 2,979 reads, 732.9 MB 1.284 s 69 / 3,288
dense-cold 2 (0) 1.124 s 961 reads, 774.9 MB 1.258 s 59 / 2,927
reflink-hot 4,775 (2,388) 0.444 s 3,316 reads, 292.3 MB 0.527 s 27 / 3,539
reflink-hot 4,919 (2,460) 0.432 s 3,569 reads, 280.9 MB 0.495 s 30 / 3,734
reflink-cold 5,149 (2,575) 0.908 s 5,450 reads, 637.3 MB 1.195 s 68 / 3,001
reflink-cold 4,975 (2,488) 0.951 s 5,825 reads, 659.3 MB 1.101 s 63 / 2,803
reflink-cold, gen 5 7,242 (6,247) 0.862 s 6,529 reads, 581.5 MB 1.013 s 63 / 2,564
run gen hibernate vm.snapshot of it, XFS close flush memory-ranges fsync CH FICLONE CH written / calls write_bytes delta new extents (shared) shared blocks wake
1413535b8 1 0.868 s 0.735 s 0.645 s 25 µs 77 µs 272.5 MB / 2,879 272,457,728 5,757 (2,879) 96.8% 0.386 s
1413535b8 2 0.319 s 0.243 s 0.104 s 19 µs 111.0 ms 35.1 MB / 1,432 35,106,816 7,573 (6,142) 99.6% 0.621 s
f1dcd9900 a 1 0.969 s 0.845 s 0.747 s 19 µs 61 µs 312.7 MB / 2,850 312,717,312 5,699 (2,850) 96.4% 0.493 s
f1dcd9900 a 2 0.345 s 0.268 s 0.128 s 21 µs 106.1 ms 52.9 MB / 1,636 52,879,360 7,574 (5,939) 99.4% 0.412 s
f1dcd9900 a2 1 0.951 s 0.809 s 0.682 s 18 µs 63 µs 286.8 MB / 2,875 286,760,960 5,749 (2,875) 96.7% 0.427 s
f1dcd9900 a2 2 0.299 s 0.224 s 0.088 s 19 µs 107.1 ms 34.5 MB / 1,404 34,549,760 7,566 (6,163) 99.6% 0.765 s
f1dcd9900 b, strace 1 1.104 s 0.967 s 0.701 s 8.3 ms 63 µs 294.5 MB / 2,879 294,477,824 5,757 (2,879) 96.6% 0.438 s
f1dcd9900 b, strace 2 0.408 s 0.313 s 0.093 s 16 µs 107.7 ms 33.3 MB / 1,417 33,288,192 7,712 (6,296) 99.6% 0.603 s

Balloon and the cocoon daemon (2026-10-02)

Lessons from image cuts

gw notes

Prompt caching on the openai-chat wire (2026-10-06): gw moved from the v0.2.7 release to cocoonstack/gateway main 13a3d906 (#148), the only commit on top of v0.2.7. A /v1/messages client’s cache_control breakpoints on system blocks, message blocks and tool_result now reach the openai-chat wire (OpenRouter) as content parts. Before, gw sent them as plain strings, so a long-prompt turn paid the full input rate on its whole prompt every time.

Daily counters: without Redis, gw’s daily counters are in process, so they restarted with gw and reset every 24 h from the process start, not at UTC midnight. The console’s “spent today” is the ledger since UTC midnight.

New image cut

  1. sudo image/cut.sh [-p] [-a <agent binary>] [-f <family>] [-r <runtime dir>]... cbdesk:vN-1 vN. -p reruns provision.sh (only when image/ changed); -a installs an agent binary; each -r adds a runtime directory whose provision.sh runs in the guest, in order; -f cbcore cuts cbcore:vN instead, adding image/desk when no -r is given (its cua-driver tarball goes into image/desk/vendor/ first) and running image/clean-gate.sh in the guest after the agent step. It uses only the cbsbx cocoon CLI and silkd’s egress relay: no claim, no api. The script stops at the first failure, keeping the VM for inspection (it prints the vm rm line). The steps:
    • cocoon-cbsbx vm run --name cbcut-vN --nics 0 from cbdesk:vN-1 (a cold boot, as the sandboxd golden does), then wait for Xorg and the session;
    • with -p, serve image/cut-proxy.py (an HTTP forward proxy, stdlib only, no allowlist) on the VM’s <vsock_socket>_2049 unix socket only, where the guest’s silkd relays 127.0.0.1:3128; copy provision.sh and rootfs/ in and run provision as root through vm exec with http(s)_proxy set (apt has its own proxy conf); the trap stops the proxy;
    • with -a, install the agent and enable cocoonbox-agent, run each runtime’s after-agent.sh, and require the toolhost’s shuttle service spec /etc/cocoonbox/tools.d/cocoonbox.json;
    • seal.sh through vm exec, then vm stop;
    • image/export-rootfs.sh on the stopped VM: it reads its layers and cow from vm inspect and overlays silkd, the proxy drop-ins, masked wait-online, an apt proxy conf, and a static agent.env (GW_API_KEY=unprovisioned, AGENT_LISTEN=127.0.0.1:7443, no EDGE_TOKEN; the export fails if the edge token would be in it); then vm rm;
    • cocoon-cbsbx image import cbdesk:vN /srv/cb/sbx/build/cocoonbox-desktop-vN.tar.gz.
  2. Pools are API-owned since 2026-10-01 17:50Z: /srv/cb/sbx/data/pools.json (written by the first PUT /v1/pools) seeds the warm targets at boot and overrides the pools warm values in sandboxd.json; deleting it returns them to the config. A pool’s egress, warmup, storage and capture_trim stay config-owned and load only at boot, so a new template key needs one restart today (cocoonstack/sandbox#288 asks for a runtime reload):
    • sudo cbsbx-conf cbdesk:vN-1=1 cbdesk:vN=0 … with every template that is still pooled, so sandboxd.json carries vN’s egress, warmup and 40G storage (its warm value no longer matters), then one sudo systemctl restart cbsbx-sandboxd, which keeps running claims;
    • then switch the warm targets online with PUT /v1/pools (root token, from /etc/cocoonbox/sandboxd-token), naming every pool to keep, since an omitted pool is drained: {"pools": [{"template": "cbdesk:vN", "net": "none", "size": "xlarge", "warm": 1}, {"template": "cbdesk:vN-1", "net": "none", "size": "xlarge", "warm": 0}, …]}. It answers the new GET /v1/info and needs no restart; no claim’s VM is touched. The body must not carry egress, warmup or storage (400). The golden builds within about 30 s of the restart even at warm 0; GET /v1/info then shows golden: true, warm: 1.
  3. sudo deploy/cbsbx/smoke cbdesk:vN, which must print PASS. It uses one claim (no cb_machine metadata) and a throwaway gw key, generated by gw’s admin API with the tenant token and deleted by its ak_id; both are removed at the end. It checks: guest units, the 40 GiB root, gw 200 through the claim secret, the store and the internet through the proxy, 10.203.0.1 :10260/:10250/:6443/:22 refused by the proxy (502; the CB-OUTPUT reset counter does not move), no key in the guest’s environ, /etc or /run, GET env hiding it, the agent answering through the relay, and gw 401 after PATCH env removes GW_KEY.
  4. Set image: cbdesk:vN in /etc/cocoonbox/api.yaml (the previous one into rebase_images if it is v14 or later; older agents cannot take the paged Restore) and restart the api.

Proof, 2026-10-01: cut.sh -p cbdesk:v11 v11b with no change to image/: boot 5 s, provision 30 s (apt and the Chrome and cua-driver downloads through cut-proxy.py), seal 3 s, export 38 s, import 55 s, 682 MB. Against v11’s tarball: the same dpkg package set (Chrome 154.0.8037.92-1), no ssh host keys, one mode change (xfwm4.xml, reinstalled by provision), and otherwise only runtime churn (journal, /tmp, random-seed, the agent’s WAL). deploy/cbsbx/smoke cbdesk:v11b: PASS. The template, its golden and its tarball were deleted afterwards. The first run lost three apt InRelease files: the guest’s overlay (metacopy=on) copies metadata-only changes up as metacopy entries, and the export mounted without metacopy, so those files vanished from the tarball. The export now mounts with metacopy=on; the v11 golden’s cow held one such entry (/var/lib/dpkg/diversions-old), so earlier templates lost only that backup file.

Safety rule for every command on a node: each sandboxd or cocoon invocation carries an explicit -config (--config for cocoon) or goes through cocoon-cbsbx. Check a version with sandboxd -version or go version -m, never with a bare binary or an unknown argument, and never pipe into head a command that can still act. sandboxd ignores positional arguments and starts with the default /etc/sandboxd/config.json, and its startup reconcile removes every sbx- VM no claim of that config owns. On 2026-10-01 a sandboxd version run this way removed two warm VMs of the host’s other sandboxd instance before head -3 closed its pipe.

Restoring a box from the store

A Rebase always saves the current machine first, then restores that save. Never Rebase a freshly created, empty machine to restore a box: the Rebase saves the empty state and restores it. Use restore-on-missing instead (api ≥ fda6d5d). When a box has no machine, Ensure creates one and restores the box’s head: PHASE_RESTORING, then “restored snapshot ". The new machine starts from the latest image of the family recorded in the head (`image`, since the 2026-10-07 api); a head without it, such as a hand-written one, restores onto the default image.

Deploying a binary into a box

Pipe the binary into the box’s VM as root over vsock. The VM name is the claim’s vm_name (GET /v1/sandboxes/{id} with the root token):

sudo /srv/cb/sbx/bin/cocoon-cbsbx vm exec -i <vm> -- sh -c 'cat > /tmp/cocoonbox-agent' < target/release/cocoonbox-agent
sudo /srv/cb/sbx/bin/cocoon-cbsbx vm exec <vm> -- sh -c 'install -m 0755 /tmp/cocoonbox-agent /usr/local/bin/ && systemctl restart cocoonbox-agent && systemctl --user -M user@ restart shuttle'

A change made this way lasts until the box’s next Rebase; ship it in the next template.

Switch to the sandboxd backend (M4.8, 2026-09-30)

Every provisioned user moved from CocoonSets to claims on the cbsbx instance (design §14): a quiesced SaveState on cocoonset, the api flipped to backend: sandboxd, then Ensure restored each box through restore-on-missing. Restore into the new claim: boot 1 s, restore 2 s; round trip on the new backend: hibernate 23 s, wake op DONE in 1.0 s.

On the old backend, the hibernate push spooled 8 GiB to /tmp on the root fs; the kubelet evicted the registry and cocoon-operator (DiskPressure) and the hibernate timed out. With TMPDIR on the NVMe the next hibernate took 106 s with the root fs flat.

Two defects surfaced on hardware; unit tests missed both:

Known gaps