Boot NetBSD on QEMU's microvm machine type, and report where setup time goes - #162
Conversation
4c372cf to
d87032f
Compare
|
CI on d87032f: 82 of 83 jobs pass, including The one red job, Not caused by this branch. Building the QEMU command for a FreeBSD x86-64 guest on master and on this branch gives one difference, the host address the SSH port forward binds to: Machine type, CPU, devices, drives and firmware are identical, so nothing here changes what the guest boots on — and the guest is dead one second in, long before sshd or the port forward matter. It also looks like a property of the runner rather than of the version: Generated by Claude Code |
027ecdc to
82c1dff
Compare
|
Updating the numbers in my earlier comment, since the branch has changed since (microvm is now CI on 82c1dff: 84 of 85 jobs pass, including
Both FreeBSD 13.0 jobs passed on this run, having panicked at 1s uptime on the previous ones. Same code, different runners, which supports the reading in my earlier comment that it is a property of the host rather than of the version. The one red job is Intermittent, and reproduced on master's own code on today's runners, so not from this branch. Not re-running it again. Generated by Claude Code |
94d2c24 to
a458c8a
Compare
Reducing a VM's boot time starts with knowing where the time goes. Add a `Timings` helper that records each phase of `Action.run` and logs a breakdown, with the same numbers as JSON in the debug log for comparing runs mechanically. Concurrent phases are recorded nested inside the phase that runs them, so it is clear their durations overlap. Also log how long the VM took to become reachable and how many probes that needed -- the probe interval bounds how precisely the former can be read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9LHkNMNNfDf2Fa1ieGT8g
`hostfwd=tcp::<port>-:22` has an empty host address, which makes QEMU listen on every interface, so the guest's sshd is reachable by anything that can reach the runner. The guest gives its user passwordless sudo, and the NetBSD images are about to accept a login with no credential at all, which turns that from untidy into a way in. The action connects to localhost, so nothing needs the wider bind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9LHkNMNNfDf2Fa1ieGT8g
Every probe inherited the SSH configuration's ConnectTimeout of 10 seconds. That is right for a command that has to survive a busy guest, but wrong for a probe: user mode networking accepts the forwarded connection before the guest's sshd does, so a probe sent too early blocks for the whole 10 seconds instead of failing fast. That put the measured boot time of any fast guest mostly in this timeout. A NetBSD 11 guest reported 11.28 seconds to become ready, of which the first probe was 10.09 and the one after it 0.19. Probe with 2 seconds instead, matching the reboot wait in the custom shell. SIMH keeps the 10, since at roughly 1 MIPS even the banner exchange isn't necessarily prompt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9LHkNMNNfDf2Fa1ieGT8g
Some platforms can be run more than one way, and the choice changes what the guest looks like rather than only how fast it starts. That has to be something a job asks for, so it belongs in an input. A variant is a named, tested configuration rather than a set of independent knobs, which keeps the list short and everything on it something CI boots -- two flags can never combine into a shape nobody tried. `default` is what every platform has and what an unset input means, so no existing workflow changes. Asking a platform for a variant it doesn't have is an error listing the ones it does, checked before anything is downloaded. Falling back would hand back something other than what was asked for without saying so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9LHkNMNNfDf2Fa1ieGT8g
Two changes that only make sense together, because the second is why the first is a tar rather than a bare compressed image. The image is now a zstd compressed tar holding a raw `disk.img` and, where the platform has one, a `kernel`. qcow2's compression has to keep the image writable, so it loses to a solid stream, and the download is most of what is left of a job's setup: 11.0 x86-64 goes from 497 MiB to 268 MiB. The members are named generically, so another platform needs nothing here beyond its `imageFileExtension`. Which format an image is comes from its magic bytes, not from what the platform publishes today, so the qcow2 and bare zstd images earlier releases produced still boot: `image_url` can point at any of the three. A kernel cannot live inside the image, because the `microvm` machine type cannot boot from a disk and the kernel is handed to QEMU itself. Shipping it beside the disk keeps the two matching. Booting that way is `variant: microvm`, not the default. It changes the hardware the guest sees -- the root disk arrives as `ld0` (virtio-blk) instead of `sd0` (vioscsi), there is no PCI bus and no ACPI to inspect, and `uname -v` names a MICROVM kernel. None of that stops a guest booting, which is exactly why it would be dangerous applied silently: a test that reads hardware would fail for a reason its author has no way to attribute. An image or hypervisor archive that cannot boot it is an error naming the missing file, rather than a quiet firmware boot. The two paths are two classes rather than one class asking which it is: `Vm` boots through the firmware, `MicrovmVm` extends it and overrides only what the machine type changes. `Qemu.vmClassFor` is the hook that picks, since that decision needs the input and `vmClass` cannot see it. `-kernel` is not a way around the firmware, which is worth writing down because it reads like one: QEMU stages the kernel, but something in the guest still has to jump to it, and for PVH here that is qboot -- which is also what leaves behind the MP table a guest with no ACPI needs. Also stop generating an SSH key. The images accept a login with no credential, so there is nothing to deliver and no resources disk to deliver it on. An image given through `image_url` still gets both: it may have been built from a release that expects the key on the resources disk, and there is no way to tell which from the outside. CI boots the variant and checks the guest really came up that way, checks that asking for it on 10.1 -- which predates the MICROVM kernel configuration -- fails, and boots a NetBSD qcow2 from an earlier release through `image_url`. Ref: #151 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9LHkNMNNfDf2Fa1ieGT8g
Brings qboot, without which the microvm machine type has no firmware that can boot a kernel, so the fast path added alongside this stays switched off. Nothing else in the guest or the action changes: which path is taken is still decided by looking for the file. The major version is the removal of the Linaro UEFI firmware. Its only consumer was OpenBSD on ARM64, which moved to the shared edk2 `uefi.fd` in 1.4.0, so there is nothing here that still asks for it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9LHkNMNNfDf2Fa1ieGT8g
Without it the guest falls back to the i8254, and everything that asks
the time pays two port reads and a pair of exits. NetBSD scores an
unadvertised TSC below every alternative and refuses it; the machine
types with ACPI settle for the HPET, but this one has neither.
Copying 256 MiB into the guest over ssh, same image, same QEMU:
as shipped, timecounter i8254 15.3 MB/s
+invtsc, timecounter TSC 39.5 MB/s
firmware path, timecounter hpet0 56.0 MB/s
which is most of what made the faster boot a loss overall. The gap left
to the firmware path is virtio over MMIO having one interrupt line per
device where PCI has a vector per queue.
`interrupting on -1` on the console reads like a missing interrupt but
is not one: `vmstat -i` shows both devices interrupting through the
ioapic, so that line is only how a non-PCI attach prints its handle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M9LHkNMNNfDf2Fa1ieGT8g
v0.7.0 has qcow2 images, which this branch can no longer read: the download URL is built from `imageFileExtension`, so it asks for a .tar.zst that does not exist there and every NetBSD job 404s. v1.0.0 publishes a bundle for every version and architecture the CI matrix boots. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9LHkNMNNfDf2Fa1ieGT8g
a458c8a to
e631103
Compare
The consumer-side half of netbsd-builder#11: read the new image bundle, and add a
variant: microvmthat boots the kernel it carries on QEMU'smicrovmmachine type.Measured
From CI (
Test the microvm variant), NetBSD 11.0 x86-64: the action's wholeStart VMstep — download, unpack, boot, wait for sshd, synchronize files — takes 10 seconds, on a 268 MiB image. The guest's own kernel boot, from its console, is 221–347 ms.These numbers were measured separately, against master, and are not produced by the CI matrix:
variant: microvmThe
Timingsbreakdown of each setup phase, added here, is how they were measured and how the remaining time can be attributed. The table goes to the log; the same numbers go to the debug log as JSON, for comparing runs mechanically.Opt-in
microvmis a variant, not the new default, because it changes the hardware the guest sees: the root disk arrives asld0(virtio-blk) rather thansd0(vioscsi), there is no PCI bus and no ACPI to inspect, anduname -vnames a MICROVM kernel. None of that stops a guest booting, which is why applying it silently would be the dangerous option — a job that reads hardware would fail for a reason its author has no way to attribute.Unset means
default, which boots exactly as before, so no existing workflow changes. Asking formicrovmwhere it cannot be satisfied — an image bundle with no kernel, a hypervisor archive with no qboot, or NetBSD/VAX, which runs on SIMH — is an error naming what is missing, rather than a quiet fall back to the slower path.That strictness earned its keep immediately. The VM used to be constructed before the image bundle was unpacked, so the kernel was not yet on disk when the boot path was chosen — feature detection would have said "no kernel" on every fresh run and silently booted through the firmware, with CI green throughout. Disk preparation now happens before the VM is created, and two jobs cover it: one boots the variant and checks the guest really came up that way, the other asks for it on 10.1 — which predates the MICROVM kernel configuration — and requires the action to fail.
No breaking change for custom images
The image bundle itself is not opt-in: every NetBSD image this action downloads is now a
.tar.zstholding a rawdisk.imgand, where the platform has one, akernel. That is where the download savings come from, on both variants.An image supplied through
image_urlis unaffected, because nothing assumes the new format. Which of the three formats NetBSD has shipped a file is comes from its magic bytes, so a qcow2 or a bare.img.zstfrom an earlier netbsd-builder release still boots. And while the published images accept a login with no credential — so no SSH key is generated and no resources disk built for them — a custom image still gets both, since it may have been built from a release that expects the key there and there is no way to tell from the outside.Neither of those was covered before:
Test custom VM imageuses OpenBSD, so custom NetBSD images had no CI at all.Test a custom NetBSD image from before the bundleboots v0.7.0'snetbsd-11.0-x86-64.qcow2and closes that gap.Merging
The prerequisites are done:
netbsd:insrc/version.ts— pinned tov1.0.0.Ready to merge. Afterwards, netbsd-builder's
Test Imagestep needs itsuses:pinned back from this branch to@master.