diff --git a/CHANGELOG.md b/CHANGELOG.md index 14eb2b8b59..aef38e994f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,18 @@ file is the human-readable summary of what changed and why. ### Sandbox +- **Creating a sandbox is near-instant and no longer costs 8 GB** — a new + sandbox is meant to start as a copy-on-write clone of the shared disk image, + costing no space until the guest writes something. On macOS the clone never + happened: the copy fell back to writing the whole 8 GB image out byte for + byte, filling in the empty space the image leaves sparse. Creating or + resetting a sandbox spent that time writing gigabytes, then the guest booted + while the disk was still catching up and had to compete for it — which is why + a sandbox's first start could crawl and sometimes gave up with "sandbox + server did not become healthy" before finishing. The clone now happens for + real: creation is immediate, a sandbox costs roughly nothing on disk until it + is used, and first start takes about as long as every start after it. + ([#293](https://github.com/quickbeard/codev-code/pull/293)) - **Work sessions reach the model on WSL's mirrored networking** — on Windows, a sandbox's model traffic crosses from the WSL guest to a relay on the Windows host, and the guest found the host by assuming WSL's default NAT diff --git a/packages/codev-sandbox/src/vz/provider.ts b/packages/codev-sandbox/src/vz/provider.ts index 1d3f73f587..4a2288010b 100644 --- a/packages/codev-sandbox/src/vz/provider.ts +++ b/packages/codev-sandbox/src/vz/provider.ts @@ -91,9 +91,8 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider { await writeImageMarker(dirs.base, asset) } // The pristine unpacked disk stays untouched; the instance runs a - // copy-on-write clone (APFS clonefile via FICLONE, plain copy elsewhere), - // so reset is just re-cloning. - await copyFile(join(dirs.image, VZ_DISK_FILE), dirs.disk, constants.COPYFILE_FICLONE) + // copy-on-write clone, so reset is just re-cloning. + await cloneDisk(join(dirs.image, VZ_DISK_FILE), dirs.disk, platform) } // A custom image is used from where it lies; otherwise the published one @@ -309,6 +308,29 @@ export function createVzProvider(options: VzProviderOptions): SandboxProvider { } } +// A copy-on-write clone of the pristine disk, falling back to a byte copy. +// +// `copyFile`'s COPYFILE_FICLONE is a no-op on macOS: libuv opens the +// destination and calls fcopyfile(), and only clonefile(2) — which takes paths +// and requires the destination not to exist — can share extents. So every +// provision wrote the whole image out instead: ~9s and a full 8 GiB per +// sandbox for a disk that should cost nothing, and it expands the source's +// sparse holes into real zeroes. Worse, the VM boots straight after, so the +// guest competes with gigabytes of write-back for the disk it is reading — +// that contention, not the image itself, is what pushed first boot past the +// 60s start budget. `cp -c` is the syscall; it fails outright rather than +// silently copying when the clone cannot happen, which is exactly the signal +// the fallback needs. +async function cloneDisk(source: string, destination: string, platform: NodeJS.Platform) { + await rm(destination, { force: true }) + if (platform === "darwin") { + const cloned = await runCommand("cp", ["-c", source, destination]).catch(() => null) + if (cloned?.code === 0) return + await rm(destination, { force: true }) + } + await copyFile(source, destination, constants.COPYFILE_FICLONE) +} + async function defaultMacosVersion() { const result = await runCommand("sw_vers", ["-productVersion"]).catch(() => null) if (!result || result.code !== 0) return null