Skip to content

Sandbox backends: twelve-cell speed comparison, Docker vs the no-Docker VM route (sub-issue of #512) #516

Open

Description

@yifanxuaaa

Goal

Compare LayerFS's speed on the no-Docker VM route (#512, design in PR #515) with today's Docker route, using the twelve R7 cells (C01–C12) as the workload. Same bundle, same Sandbox image, same cell runner, and the same stand-in on every VM; only the platform changes.

Candidates

# Candidate Route
0 Docker Desktop today's Docker backend (baseline)
1 Microsandbox microVM (libkrun) no Docker: Sandboxes as namespaces through the #512 test-only Engine API stand-in
2 Apple Container VM no Docker, same stand-in
3 Lima vz VM (Ubuntu) no Docker, same stand-in

Not timed:

  • Firecracker: here it can only run nested inside a Lima VM, where page faults cross two hypervisors, so its timings would describe the nesting. It stays a pass/fail check.
  • Daytona open source: its sandboxes are privileged containers on Docker Desktop, so it would re-measure Docker Desktop.
  • Hosted providers: they need accounts and run on other hardware.

The twelve cells

The bodies are exactly those in benchmark/cluster-two/cells/workloads.py at the parent of 196c714b0b, the commit that removed the harness on 2026-10-20. The last baseline is TIMING-BASELINE-20261010.md.

Cell Command Base
C01 create 1,000 files empty
C02 create 1,000, stat 1,000 empty
C03 create 1,000, remove them empty
C04 100 directories, 1,000 files empty
C05 that tree, then find empty
C06 write 64 MiB empty
C07 write 64 MiB, copy it empty
C08 write 64 MiB, read it empty
C09 read a 64 MiB base file committed 64 MiB file
C10 copy a 64 MiB base file committed 64 MiB file
C11 overwrite a 64 MiB base file committed 64 MiB file
C12 git init, 100 files, add, commit empty

Method

  • A new runner, not the old one. The old R7 runner is pinned to the 2026-10-09 source, its own runtime binary and Docker CLI calls, so reviving it would measure old code. A small runner runs the same twelve bodies through the current SDK (workspace.exec). Its numbers compare across the four candidates here, not with the 2026-10-10 baseline.

  • Cache state, class B as before:

    • per cell, a fresh project and Store;
    • an untimed setup (the committed base for C09–C11);
    • one untimed identical run on an earlier mount, then its terminal unmount;
    • then a fresh mount for the timed run.

    Every phase pays for its own work, with no warm-cache credit beyond that declared state.

  • One sample per cell per candidate. No reruns, no best-of. Failed and ineligible cells are kept.

  • What is recorded per cell:

    • the command time on the Sandbox's own clock, and the workspace.exec wall on the SDK side;
    • mount, unmount and Commit times;
    • the verifier's result (files, sizes and contents as the cell expects);
    • the daemon's request and statement counts where the current telemetry exposes them;
    • the Store and overlay bytes.
  • Pinned identities:

    • the sealed bundle build-linux-bb5f636… (source 7ab54bcf1a, hashes checked against its manifest);
    • Sandbox image sha256:afa1ef9c…;
    • the runner's and the stand-in's sha256;
    • each VM's kernel, vCPUs and memory;
    • Disposable/WAL/OFF; LAYERFS_CONSTRUCTION_WORKERS=1.
  • Interference: sampling commands on this Mac take one host-wide lock, so no two candidates sample at once. The 1-minute load is recorded with every sample. Preparation on other candidates may overlap and is declared. The VMs get 4 vCPUs and 4 GiB each, so that three VMs and Docker Desktop fit in this Mac's 36 GB without swapping; Docker Desktop keeps its own 8 CPUs and 8 GB and is shared with idle containers, which is recorded.

Rules

  • Each command is bounded at 2 minutes, and each test invocation at 120 s.
  • One attempt per step.
  • Disposable Stores only, never the owner's.
  • Every VM, volume and image the run creates is removed afterwards.

Results

  • 0 Docker Desktop
  • 1 Microsandbox
  • 2 Apple Container
  • 3 Lima
  • Comparison table, with the exact counts first and timings second

Activity

  1. yifanxuaaa commented on Oct 30, 2026

    @yifanxuaaa
    ContributorAuthor

    Twelve-cell comparison: the no-Docker VM route is as fast as Docker Desktop or faster, and each exec call costs ~4 ms instead of ~42 ms

    All four candidates ran all twelve cells: 48 of 48 samples PASS their verifier, one sample each, no reruns. All samples used:

    • one frozen runner (cells.mjs f68e67b9…);
    • one sealed bundle (build-linux-bb5f636…, hashes matching its manifest);
    • Sandbox image sha256:afa1ef9c…;
    • the same stand-in (704bfa37…) on every VM;
    • a fresh project and Store per cell;
    • Disposable/WAL/OFF and LAYERFS_CONSTRUCTION_WORKERS=1.

    A host-wide lock ran one sample at a time: 10 of 13 recorded waits were 0 s, the longest 4 s, and the host's 1-minute load was 1.9–3.1.

    # Candidate Kernel CPUs / memory
    0 Docker Desktop (Docker backend) 6.12.76-linuxkit 8 / 7.9 GiB
    1 Microsandbox microVM, no Docker 6.12.113 4 / 4 GiB
    2 Apple Container VM, no Docker 6.18.35 5 visible, 4 by cgroup quota / 4 GiB
    3 Lima vz VM (Ubuntu), no Docker 6.8.0-147-generic 4 / 4 GiB

    1. Same work everywhere (counts first)

    These counts are equal across all four candidates in every cell:

    • FUSE requests for the body alone (the warm-up mount), with the two exceptions below;
    • Store SQL statements per Commit, except C12;
    • Commit kinds: C03, C09 and C11 are UpToDate.

    The exceptions are small and explained:

    • Lima's 6.8 kernel issued 680 requests instead of 516 for C09's 64 MiB read.
    • C12 differs slightly everywhere (3,078–3,282 requests, 879–883 statements), because git writes timestamps into its objects. Lima's 6.8 kernel and Docker Desktop's linuxkit kernel issued more requests there too.
    • Stored bytes per Commit differ by a few to 30 bytes in C01, C02, C04 and C05.

    So the timings below compare the same filesystem work.

    2. Command time on the Sandbox's own clock (ms; ratio to Docker Desktop)

    Cell Workload Docker Desktop Microsandbox Apple Lima
    C01 create 1,000 files 276.1 259.8 (0.94) 248.9 (0.90) 314.4 (1.14)
    C02 create 1,000, stat 1,000 825.3 715.0 (0.87) 692.4 (0.84) 875.8 (1.06)
    C03 create 1,000, remove them 466.8 448.5 (0.96) 402.9 (0.86) 522.0 (1.12)
    C04 100 dirs, 1,000 files 711.3 570.7 (0.80) 557.6 (0.78) 710.8 (1.00)
    C05 that tree, then find 730.8 597.5 (0.82) 586.9 (0.80) 739.9 (1.01)
    C06 write 64 MiB 147.4 134.9 (0.91) 136.2 (0.92) 154.6 (1.05)
    C07 write 64 MiB, copy it 327.6 300.8 (0.92) 293.4 (0.90) 336.7 (1.03)
    C08 write 64 MiB, read it 154.8 144.9 (0.94) 144.2 (0.93) 160.6 (1.04)
    C09 read a 64 MiB base file 45.1 41.2 (0.91) 41.0 (0.91) 49.2 (1.09)
    C10 copy a 64 MiB base file 181.6 175.7 (0.97) 180.3 (0.99) 197.3 (1.09)
    C11 overwrite a 64 MiB base file 94.4 86.1 (0.91) 83.5 (0.88) 96.1 (1.02)
    C12 git init, 100 files, add, commit 141.6 158.5 (1.12) 173.6 (1.23) 174.8 (1.23)
    sum 4,102.9 3,633.6 (0.89) 3,540.9 (0.86) 4,332.1 (1.06)
    geometric mean of ratios 0.92 0.91 1.07

    3. The other phases (geometric mean of the ratio to Docker Desktop, over twelve cells)

    Phase Microsandbox Apple Lima Notes
    workspace.exec wall (SDK side) 0.77 0.76 0.89 includes the fixed per-exec overhead below
    mount 0.60 0.71 0.71 Docker Desktop 9.7–17.3 ms; VMs 6.4–9.1 ms, apart from one Apple outlier (52.9 ms on C08, kept)
    Commit 0.93 0.92 0.98 Store work, so nearly identical everywhere
    unmount 0.79 0.79 1.06

    Fixed overhead per exec (the SDK's exec wall minus the command's own time; median over twelve cells):

    • Docker Desktop: 42.3 ms (35.7–49.7);
    • Microsandbox: 4.4 ms;
    • Apple: 4.4 ms;
    • Lima: 4.8 ms.

    That is the Docker Engine's exec create, attach and inspect against the stand-in's direct namespace launch. It's paid once per tool call, so it matters most for LayerFS's common per-tool-call case.

    4. How to read it

    • One sample per cell. The R7 baseline measured up to 23% between two samples of identical work, so a cell-level difference within about ±20% isn't a finding on its own. The steady patterns are:
      • VMs mount faster;
      • the per-exec overhead drops about tenfold;
      • Commit is unchanged;
      • command times within about ±15%, except C12.
    • The baseline isn't Docker on Linux. Candidate 0 is Docker Desktop on macOS: the SDK client runs in a container and reaches the daemons through host.docker.internal, with 8 CPUs, against 4 for the VMs. Part of its mount and exec cost belongs to Docker Desktop's setup, not to Docker itself.
    • Kernels differ (6.8 to 6.18), which shows in Lima's request counts and probably in its slightly slower command times.
    • The stand-in is test-only (Sandbox backends: which agent sandboxes can replace LayerFS's Docker container? Local stand-ins first #512). A real namespace backend (PR docs(512): propose a VM Sandbox backend (Sandboxes as namespaces in one VM per Store) #515) would do the same work in Rust, so this is indicative, not a product measurement.
    • Not eligible as a release or regression gate. These are diagnostic samples under the measurement workflow, not admission-eligible measurements. Overlay bytes weren't exposed by the current telemetry, and Store bytes are in the per-cell files.

    Conclusion: running LayerFS with no Docker, one VM per project and Sandboxes as namespaces, costs nothing in filesystem speed here. It makes each exec call about 38 ms cheaper and mount about 4 ms cheaper. Commit is unchanged. Lima is about 7% slower on command time, probably because of its older kernel. Git (C12) is 12–23% slower in every VM, the only cell where all three VMs lose.

    Suggested next step: Docker inside the same Lima VM as a fourth arm, giving the same kernel and the same CPUs with only the backend changed. That isolates the backend from Docker Desktop's own costs.

    All VMs, volumes, images and helper containers the run made were removed.

  2. yifanxuaaa commented on Oct 30, 2026

    @yifanxuaaa
    ContributorAuthor

    Extra reference, not the baseline: the same cells with no LayerFS

    Correction. An earlier version of this comment called this the owner's requested baseline. That was a misreading. The baseline for #516 is LayerFS on Docker Desktop, as in the comparison above. The no-LayerFS run below is extra context only: it shows what FUSE costs over plain container storage, not how the candidates compare.

    The four candidates above all run LayerFS. This adds a reference run: plain Docker Desktop, with no LayerFS and no FUSE. It ran:

    • the same twelve bodies, checked identical to the runner's;
    • the same image (sha256:afa1ef9c…), environment and in-container clock (date +%s%N);
    • the same identity: uid 501:20 through setpriv, with no capabilities and no-new-privs;
    • the same cache state: an untimed setup of the base file for C09–C11, an untimed warm-up run of the body in its own directory, then a fresh directory for the timed run;
    • a fresh container per cell (--network none), one sample per cell, under the same host lock (acquired after 0 s every time, load 2.15–2.30).

    There are two variants: a fresh Docker volume per cell (ext4 in Docker Desktop's VM), and the container's own overlayfs root. Every native cell left the expected end state, so all 24 pass.

    Command time on the in-container clock (ms; the ratio is to native on a volume)

    Cell Workload Native, volume Native, rootfs LayerFS Docker Desktop LayerFS Microsandbox LayerFS Apple LayerFS Lima
    C01 create 1,000 files 13.0 30.2 276.1 (21×) 259.8 (20×) 248.9 (19×) 314.4 (24×)
    C02 create 1,000, stat 1,000 351.3 365.5 825.3 (2.3×) 715.0 (2.0×) 692.4 (2.0×) 875.8 (2.5×)
    C03 create 1,000, remove them 17.2 32.0 466.8 (27×) 448.5 (26×) 402.9 (23×) 522.0 (30×)
    C04 100 dirs, 1,000 files 299.9 319.7 711.3 (2.4×) 570.7 (1.9×) 557.6 (1.9×) 710.8 (2.4×)
    C05 that tree, then find 303.4 318.0 730.8 (2.4×) 597.5 (2.0×) 586.9 (1.9×) 739.9 (2.4×)
    C06 write 64 MiB 10.5 16.2 147.4 (14×) 134.9 (13×) 136.2 (13×) 154.6 (15×)
    C07 write 64 MiB, copy it 22.5 21.3 327.6 (15×) 300.8 (13×) 293.4 (13×) 336.7 (15×)
    C08 write 64 MiB, read it 14.5 15.4 154.8 (11×) 144.9 (10×) 144.2 (9.9×) 160.6 (11×)
    C09 read a 64 MiB base file 4.0 4.2 45.1 (11×) 41.2 (10×) 41.0 (10×) 49.2 (12×)
    C10 copy a 64 MiB base file 10.9 10.8 181.6 (17×) 175.7 (16×) 180.3 (17×) 197.3 (18×)
    C11 overwrite a 64 MiB base file 7.7 7.3 94.4 (12×) 86.1 (11×) 83.5 (11×) 96.1 (13×)
    C12 git init, 100 files, add, commit 10.3 14.4 141.6 (14×) 158.5 (15×) 173.6 (17×) 174.8 (17×)
    sum 1,065.0 1,155.0 4,102.9 (3.9×) 3,633.6 (3.4×) 3,540.9 (3.3×) 4,332.1 (4.1×)
    geometric mean of ratios 9.5× 8.7× 8.6× 10.2×

    How to read it

    • The cost is per FUSE request, not per platform. Every LayerFS candidate sits at roughly the same multiple of native. The extra time divided by the body's FUSE requests:

      • ~45–60 µs per request for small-file cells (C01, C03, C04, C05, C12);
      • ~175–280 µs per request for the 64 MiB cells, whose writes go in 128 KiB requests.

      This matches the R7 records' ~60 µs per request. Removing Docker changes this by only ~10%. The per-request path through FUSE, the daemon and the Overlay is the cost, wherever it runs.

    • Process-heavy cells look better (C02, C04, C05, about 2×), because spawning ~1,000 stat, mkdir or touch processes takes about 300 ms even natively and dominates. Cells made of plain shell redirections or one large file show the full FUSE cost: 10–30×.

    • Native doesn't do LayerFS's other work. It has no mount, no Commit, no history and no isolation from the base. The LayerFS columns are command time only. Mount (~6–17 ms), Commit (3–630 ms) and the per-exec overhead come on top, as in the comparison above.

    • Same caveats as before:

      • one sample per cell;
      • kernels differ for the VM candidates (native and LayerFS Docker Desktop share 6.12.76-linuxkit);
      • the 64 MiB base file is in the page cache in both native and LayerFS runs, after the declared warm-up;
      • neither side fsyncs.

    Where the candidates differ from the LayerFS Docker Desktop baseline

    The same split over all twelve cells, from the warm-up run (the only phase whose FUSE counters cover just the command):

    Over all 12 cells Docker Desktop Microsandbox Apple Lima
    daemon handling FUSE requests 1,468 ms 1,366 ms 1,361 ms 1,451 ms
    everything else in the command 2,706 ms 2,402 ms 2,223 ms 2,783 ms
    • The shared path costs the same everywhere. The daemon's time is within 8% across the four, with the same request counts.

    • Most of the difference is in C02, C04 and C05, which start about 1,000 short processes each. Their time outside the daemon:

      • Docker Desktop 673 / 599 / 625 ms;
      • Lima 710 / 585 / 620 ms;
      • Microsandbox 580 / 497 / 494 ms;
      • Apple 545 / 462 / 501 ms.

      So the gap is process start-up in each VM's kernel, not FUSE.

    • Docker's seccomp filter is probably not the cause: Lima has none and is as slow as Docker Desktop. This is an inference and isn't isolated.

    • The C12 finding above is within noise. In the warm-up run of the same body, all three VMs were faster than Docker Desktop on C12 (151–169 ms against 171), the reverse of the timed run.

    Conclusion: against the LayerFS Docker Desktop baseline, the no-Docker VMs leave the filesystem path unchanged. Command time then follows how fast the VM's kernel starts processes: Apple and Microsandbox are about 8–9% faster, Lima about 7% slower. Separately, FUSE costs about 9–10× plain container storage on these cells, on every platform.

    Script: native_cell.py (sha256 recorded in each result) next to the runner, with the bodies read from workloads.py at 196c714b0b^. No containers or volumes are left.

  3. Mola-maker commented on Nov 1, 2026

    @Mola-maker
    Member

    感谢提出这个 issue,也感谢 AI 协作工具在整个排查过程中做的分析 🙏

    对 AI agent 的使用来说,这个问题的解决会直接改善 agent 选择 sandbox 后端时的量化依据——成本数据直接决定 agent 部署形态。

    两点建议:

    1. 动手修改前请先实测:在真实环境(Linux WSL2/ext4 或原生)复现 issue 描述的行为,把复现命令与输出贴出来,确认问题在最新 main 上仍然存在。
    2. 实现等 LayerFS 2.0 发布:2.0 roadmap(LayerFS 2.0 roadmap #446)已在推进,建议把修复/功能实现排进 2.0 的计划里,当前阶段先冻结讨论与实测证据,避免在旧基线上做出之后需要重做的改动。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions