/** * The rig's contract with the Docker host it runs on. * * On Linux, docker runs on the host kernel and there is no contract to state. * On macOS it runs inside a virtual machine, and two of that VM's settings * dominate how long a run takes. Both were measured on 2026-09-05, on an M1 Pro * with 32 GiB, while a suite was running: * * **Memory.** The VM held 24 GiB of the host's 32. It used 1.9 GiB and filled * the rest with page cache, which is what a Linux guest is supposed to do. But * the guest's idea of "cached in RAM" is the host's idea of "anonymous memory I * may compress or swap", so the host sat at 21.9 GiB of swap used with * `kernel_task` (the memory compressor) at 60% of a core, and every image-layer * read the guest thought was free became a host decompress or an SSD read. The * guest cannot give the memory back — there is no balloon driver — so the only * lever is not to hand it over in the first place. * * **Mount type.** The celilo checkout is bind-mounted into the management * container. Under colima's default `sshfs` every file operation crosses FUSE, * an SSH channel, a userspace TCP stack on the host, an ssh multiplexer and * `sftp-server`. `virtiofs` is a shared-memory transport with none of that * chain. * * The two costs had to be separated, because measuring them together * attributed nearly all of it to the wrong one. Same probe, same image, three * VM configurations: * * | VM | CLI on image disk | CLI over mount | * |-------------------------------------|-------------------|----------------| * | 24 GiB sshfs (host swapping 21.9 G) | 0.27s | 2.50s | * | 12 GiB sshfs | 0.16s | 0.56s | * | 12 GiB virtiofs | 0.14s | 0.31s | * * So the 2.3-second gap in the first row was about 1.9s of host swap and 0.4s * of transport. Memory is the big lever; the mount is real but secondary, and * shows up most on bulk reads — reading 200 source files took 0.24s on sshfs * and 0.073s on virtiofs, a factor of 3.3. * * Nothing here can fix either one — colima refuses to change a VM's mount type * after creation, and shrinking memory needs a restart. So this module's job is * to READ what the VM is actually running and say plainly when it disagrees * with the policy, which is what `cele2e doctor` and `cele2e host` do with it. * * It reads lima's own instance config rather than colima's saved profile, * because those two can disagree and only one of them is the running VM. They * disagreed while this was being written: colima accepts `--mount-type` on the * command line, prints `'volume mount type' cannot be updated after initial * setup, discarded`, and rewrites its saved profile back — so the profile is a * record of what was asked for, and lima's is a record of what happened. */ import { existsSync, readFileSync } from 'node:fs'; import { cpus, homedir, totalmem } from 'node:os'; import { join } from 'node:path'; import { parse as parseYaml } from 'yaml'; /** What the VM is actually running, read from lima's instance config. */ export interface HostVmFacts { /** colima profile name, e.g. `default`. */ profile: string; cpus: number; memoryGiB: number; /** Normalized: lima's `reverse-sshfs` is colima's `sshfs`. */ mountType: string; /** `vz` (Apple Virtualization) or `qemu`. virtiofs requires `vz`. */ vmType: string; } /** The host the VM is carved out of. */ export interface HostFacts { cpus: number; memoryGiB: number; } /** What the VM should be given. */ export interface HostVmBudget { cpus: number; memoryGiB: number; mountType: 'virtiofs' | 'sshfs'; } /** * The share of host RAM above which the host starts paying for the guest's * page cache in swap. Not a tuning knob so much as a line: at 24 of 32 GiB * (75%) the host swapped 21.9 GiB; the policy below lands at 37.5%, which * leaves the guest more than it uses and the host its own working set. */ export const MAX_HOST_MEMORY_SHARE = 0.5; const TARGET_HOST_MEMORY_SHARE = 0.375; /** Floor and ceiling on the VM's memory, in GiB. */ const MIN_VM_MEMORY_GIB = 4; const MAX_VM_MEMORY_GIB = 16; /** Cores left to the host, so the Mac stays usable while a suite runs. */ const HOST_RESERVED_CPUS = 2; /** * The VM settings this rig wants on a given host. * * Pure, and expressed as a fraction rather than a constant, so it says * something true on a 16 GiB laptop and a 64 GiB desktop rather than encoding * one machine's answer. */ export function recommendedBudget(host: HostFacts, vmType: string): HostVmBudget { const memoryGiB = Math.min( MAX_VM_MEMORY_GIB, Math.max(MIN_VM_MEMORY_GIB, Math.floor(host.memoryGiB * TARGET_HOST_MEMORY_SHARE)), ); return { cpus: Math.max(2, host.cpus - HOST_RESERVED_CPUS), memoryGiB, // virtiofs is an Apple Virtualization feature; a qemu VM cannot have it. mountType: vmType === 'vz' ? 'virtiofs' : 'sshfs', }; } export function readHostFacts(): HostFacts { return { cpus: cpus().length, memoryGiB: Math.round(totalmem() / 1024 ** 3) }; } /** colima's lima instance directory: `colima` for the default profile, `colima-
` otherwise. */
export function limaInstanceDir(profile: string): string {
const instance = profile === 'default' ? 'colima' : `colima-${profile}`;
return join(homedir(), '.colima', '_lima', instance);
}
export function activeProfile(): string {
return process.env.COLIMA_PROFILE || 'default';
}
/** `12288MiB` / `12GiB` / `12884901888` → GiB. */
export function parseLimaMemory(raw: unknown): number {
if (typeof raw === 'number') return Math.round(raw / 1024 ** 3);
if (typeof raw !== 'string') return 0;
const match = raw.trim().match(/^([\d.]+)\s*([KMGT]i?B)?$/i);
if (!match) return 0;
const value = Number.parseFloat(match[1]);
const unit = (match[2] ?? 'B').toUpperCase();
const scale: Record