Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,23 @@ jobs:
> /tmp/verdict.md

cat /tmp/verdict.md

# What reached the machine since the release before, read from git: the commits by the
# part they changed, the pins that moved, the kernel options and the patches. Against
# the newest published release that is not this one, as the verdict is.
previous=$(gh release list -R "$GITHUB_REPOSITORY" --limit 30 --json tagName -q '.[].tagName' |
grep -vx "$VERSION" | head -1 || true)
if [ -n "$previous" ]; then
go run ./hack/releasenotes -from "$previous" -to HEAD > /tmp/changes.md
else
echo "The first release: nothing before it to say what changed against." > /tmp/changes.md
fi
cat /tmp/changes.md

{
echo 'notes<<RELEASE_NOTES_EOF'
cat /tmp/changes.md
echo
cat /tmp/verdict.md
echo
echo 'The machine as it stood on this date.'
Expand Down
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ tasks:
deps: [tools]
vars:
OUT: '{{.OUT | default (printf "%s/report.json" .OUTPUT_ABS)}}'
REPS: '{{.REPS | default "3"}}'
REPS: '{{.REPS | default "20"}}'
cmds:
# TMPDIR on disk, because the overlays and memory files go there: gigabytes a tmpfs
# /tmp holds in RAM.
Expand Down
3 changes: 1 addition & 2 deletions boot/bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ type variant struct {
profile bool // boot with `--profile`: initcall profiling, console silent
setup string // shell run with the overlay mounted, $MNT its root
flags []string // spin-machine boot flags after the rest: the feature matrix's axes
timeout time.Duration // how long a boot may take before it is a hang; 70 s when zero
}

// Masks are written into the overlay and never passed as `systemd.mask=`. That parameter is
Expand Down Expand Up @@ -540,7 +539,7 @@ func bootOnce(t *testing.T, out string, v variant) boot.Run {
// The cap is for a machine that hangs. The normal path is Watch returning at the login
// prompt, and then this kills a machine that is working perfectly — which is the point:
// nothing after the phase being measured is being measured.
timer := time.AfterFunc(cmp.Or(v.timeout, 70*time.Second), kill)
timer := time.AfterFunc(70*time.Second, kill)
defer func() { timer.Stop(); kill(); _ = cmd.Wait() }()

run, err := boot.Watch(stdout, t0, time.Now, boot.Usable)
Expand Down
17 changes: 5 additions & 12 deletions boot/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import (
"runtime"
"strings"
"testing"
"time"

"github.com/spin-stack/spin-machine/boot"
)
Expand All @@ -34,9 +33,9 @@ type choice struct {
// is the same on every host and every run.
const diskFile = "/report/disk.qcow2"

// KVM only. The TCG build is for CI runners without /dev/kvm, where a boot is ~4 s of emulation:
// timing it says nothing about a host, and its rows were most of a report's minutes.
var axes = []axis{
// The KVM build a host runs, and the TCG build that has no /dev/kvm to ask.
{"accel", []choice{{"kvm", nil}, {"tcg", []string{"--accel", "tcg"}}}},
// RAM fixed at its boot size, or with a ceiling reached through virtio-mem.
{"memory", []choice{
{"anonymous", nil},
Expand Down Expand Up @@ -72,10 +71,10 @@ func combinations() [][]choice {
// SPIN_REPORT=<file> run at all, and where the JSON goes
// SPIN_REPORT_FLAGS="..." spin-machine boot flags added to every boot, recorded
// SPIN_REPORT_ONLY=<regexp> only the rows whose id matches, for iterating on one question
// REPS=<n> boots per row (default 3)
// REPS=<n> boots per row (default 20)
//
// Needs /dev/kvm and a built release tree (SPIN_MACHINE_OUTPUT, or _output). The vsock rows
// need /dev/vhost-vsock, the TCG rows the release's TCG build; without them those rows are
// need /dev/vhost-vsock; without it those rows are
// listed as skipped rather than reported as failing. With sudo and /dev/nbd0 the getty is
// replaced by an echo, as in TestBootCost, and every variant of the image runs; without them
// the usable column carries agetty's second and the variants that edit the image are skipped.
Expand All @@ -85,7 +84,7 @@ func TestReport(t *testing.T) {
t.Skip("set SPIN_REPORT=<file>: this boots every combination of the machine's features")
}
out := releaseDir(t)
reps := envInt(t, "REPS", 3)
reps := envInt(t, "REPS", 20)
flags := strings.Fields(os.Getenv("SPIN_REPORT_FLAGS"))
only, err := regexp.Compile(os.Getenv("SPIN_REPORT_ONLY"))
if err != nil {
Expand Down Expand Up @@ -119,9 +118,6 @@ func TestReport(t *testing.T) {
} else {
_ = f.Close()
}
if _, err := os.Stat(filepath.Join(out, "bin/qemu-system-x86_64-tcg")); err != nil {
missing["accel=tcg"] = "no TCG build in the release"
}

// One boot nobody measures. The first after the release was written or the host started
// reads the base image from disk and not from the page cache, and would be the slowest
Expand Down Expand Up @@ -161,9 +157,6 @@ func TestReport(t *testing.T) {

v := variant{label: row.ID, cpus: "2", memory: "2048", files: login,
flags: rowFlags}
if row.Features["accel"] == "tcg" {
v.timeout = 5 * time.Minute
}
row.Boot = measure(t, out, v, reps)
r.Specs = append(r.Specs, row)
}
Expand Down
17 changes: 11 additions & 6 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,17 @@ combination of the machine's features, booted from the published tarball to a lo
no API here to promise compatibility about, and the one thing a version could promise —
that checkpoints still resume — is decided by the fingerprint of the artefacts, not by a
number anybody chose. Pushing a `v*` tag releases that version; running the workflow by
hand with no input generates the next sequence for today, tags the commit, and puts the
three checksums in the release notes.
hand with no input generates the next sequence for today, tags the commit, and writes the
notes: what changed since the release before (`hack/releasenotes`: the commits that reached
the machine by the part they changed, the pins that moved, the kernel options turned on, off
or changed, and the patches added, changed or removed), whether checkpoints carry over
(`hack/fingerprint-diff`), and the checksums.

## The feature matrix

A release also says what it costs. After it publishes, the `report` job boots the tarball it
just published, on a self-hosted runner labelled `kvm`, through every combination of the
machine's features (accelerator; memory fixed or with a virtio-mem ceiling; vsock; a hotplug
machine's features under KVM (memory fixed or with a virtio-mem ceiling; vsock; a hotplug
controller) and every boot variant of the image. It writes `report.json` beside the tarball: per row, the command
line, the shape and fingerprint, whether QEMU ran it, and p50/p95 of each boot phase. It then
appends `spin-machine compare` against the newest earlier release that has a report to the
Expand All @@ -53,9 +56,11 @@ task report OUT=exp.json FLAGS='--append mitigations=off' # or --kernel /path/
_output/bin/spin-machine compare --old base.json --new exp.json
```

`ONLY=<regexp>` runs the rows whose id matches, and `REPS=` sets the boots per row (default 3,
after one unmeasured boot that warms the page cache). Times compare only between reports taken
on the same kind of host; `compare` says so when they were not.
`ONLY=<regexp>` runs the rows whose id matches, and `REPS=` sets the boots per row (default 20,
after one unmeasured boot that warms the page cache). Twenty, because at three the comparison
of v20261001.01 with v20260930.02 flagged every KVM row 5-11% slower, and twenty boots of each,
alternated on one host, put them within 5 ms of each other (2026-10-01). Times compare only
between reports taken on the same kind of host; `compare` says so when they were not.

`report.yml` is the job, and it runs by hand too: `gh workflow run report.yml -f
version=<release>` measures any published release and keeps `report.json` and the comparison as
Expand Down
13 changes: 8 additions & 5 deletions hack/release
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,11 @@ echo "Firmware:"
for f in bios.bin bios-256k.bin pvh.bin kvmvapic.bin efi-virtio.rom qboot.bin; do
require "${OUTPUT_DIR}/qemu/${f}" "${SHARE}/qemu/${f}" "firmware ${f}"
done
require qemu/qboot/write-pointer.patch "${SHARE}/qemu/qboot-write-pointer.patch" "qboot's patch"
require qemu/qboot/pam.patch "${SHARE}/qemu/qboot-pam.patch" "qboot's patch"
# Every patch in the directory, not a list of them: mtrr.patch was added to the build and not to
# a list here, and v20261001.01 shipped a qboot.bin without the change it was built with.
for p in qemu/qboot/*.patch; do
require "$p" "${SHARE}/qemu/qboot-$(basename "$p")" "qboot's patch $(basename "$p")"
done
require qemu/qboot/COPYING "${SHARE}/qemu/qboot-COPYING" "qboot's licence"
# The patches QEMU is built with, for the same reason: a changed GPL source is distributed with
# its changes. Read from the tree, as qboot's are: the binaries were built from this commit.
Expand Down Expand Up @@ -156,7 +159,7 @@ QEMU ${QEMU_VERSION}
binaries: usr/share/spin-stack/bin/qemu-system-x86_64
usr/share/spin-stack/bin/qemu-system-x86_64-tcg
usr/share/spin-stack/bin/qemu-img
firmware: usr/share/spin-stack/qemu/*, except patches/, qboot.bin and the three qboot files below
firmware: usr/share/spin-stack/qemu/*, except patches/, qboot.bin and the qboot files below
source: https://download.qemu.org/qemu-${QEMU_VERSION}.tar.xz
sha256: ${QEMU_SHA256}
built by: qemu/Dockerfile, with usr/share/spin-stack/qemu/patches applied to that source
Expand All @@ -167,8 +170,8 @@ qboot ${QBOOT_COMMIT}
source: https://github.com/bonzini/qboot, commit ${QBOOT_COMMIT}; the sha256 of what
\`git archive\` writes for it is ${QBOOT_ARCHIVE_SHA256}
licence: GPL-2.0, usr/share/spin-stack/qemu/qboot-COPYING
built by: qemu/Dockerfile's qboot stage, with usr/share/spin-stack/qemu/qboot-write-pointer.patch
and then qboot-pam.patch applied to that source, and the compiler flags in that file
built by: qemu/Dockerfile's qboot stage, with usr/share/spin-stack/qemu/qboot-*.patch applied
to that source in the order that file applies them, and its compiler flags

e2fsprogs ${E2FSPROGS_VERSION}
binaries: usr/share/spin-stack/bin/mkfs.ext4, debugfs, e2fsck, dumpe2fs
Expand Down
Loading