From 30af3d6f86c004226ad56da1fb9433103e25c607 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Fri, 23 Oct 2026 10:42:48 -0300 Subject: [PATCH 1/5] openvmm: let platform snapshots activate restore-time vCPUs Pin microsoft/openvmm#4735 at 2b681564dd63483f2f9c9f9bba3b98a4c00e9680, two commits on 8a0d945ed, the previous pin, which is the head of feature/microvm/dev since microsoft/openvmm#4734 merged it. The first commit lets a platform snapshot opt into restore-time vCPU activation. Restore-time memory expansion already worked on every tier, but a platform capture booted with maxcpus=B failed to publish, because the platform tier's command-line allowlist did not accept maxcpus=, and one captured without it refused --restore-processors at every restore. OpenVMM now accepts one maxcpus= token on a platform command line, the canonical form of the recorded boot-online count, as it accepts nr_cpus=, so each clone of one platform capture can bring its own vCPU prefix online at restore. The second stops the control-console broker from timing a host's authentication while host input is gated. A host that connected as soon as it launched a gated restore, and authenticated right away, was detached whenever the guest's repair outlasted --microvm-control-auth-timeout-ms, although the broker had not read its authentication yet. The timeout is now suspended while input is gated and restarts when the input is released. Part of #796 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- openvmm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openvmm b/openvmm index 8a0d945e..2b681564 160000 --- a/openvmm +++ b/openvmm @@ -1 +1 @@ -Subproject commit 8a0d945edca13b301a5152f225702057337b2a56 +Subproject commit 2b681564dd63483f2f9c9f9bba3b98a4c00e9680 From 24970cded8609c1d44d8fa907c9c6a790a4e4cb8 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Fri, 23 Oct 2026 10:42:48 -0300 Subject: [PATCH 2/5] doc: map the restore-time activation fixes in the OpenVMM roadmap Record the two commits that the previous commit pins as positions 339 and 340 of the microVM chain, and move the NVX pin to 2b681564d. Position 339 lets platform snapshots opt into restore-time vCPU activation. It fixes position 120, "openvmm: bring a restore-time processor prefix online", which added the activation without extending the platform allowlist that position 116 had introduced, so it folds into 120 in PR20. Position 340 times host authentication only while the control-console broker reads host input. It fixes position 136, "virtio_console: attach the broker to serial I/O with receive credits", so it folds into 136 in PR23. Extend the scope, the chain, the fold instructions, the table of the commits after the mapped tip, the PR summary, and the commit mapping, and count the fixups in the PR20 and PR23 nodes of the graph. The pin is now 183 commits past the mapped tip. Position 338, 8a0d945ed, is no longer only a proposal: microsoft/openvmm#4734 merged it, so it is the head of feature/microvm/dev, one commit on 79a501b63, which remains the head of feature/microvm/release/v2.3. Say so in the scope and in position 338's entry of the commit mapping, and name microsoft/openvmm#4735 in the scope as the proposal of positions 339 and 340 for feature/microvm/dev. Part of #796 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/openvmm-upstream-roadmap.md | 48 +++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/doc/openvmm-upstream-roadmap.md b/doc/openvmm-upstream-roadmap.md index 090ef252..8f6e5154 100644 --- a/doc/openvmm-upstream-roadmap.md +++ b/doc/openvmm-upstream-roadmap.md @@ -8,13 +8,14 @@ [Upstream status](#upstream-status). - Mapped fork tip: `a0556d7d85ce1fff89f5b45a98f15c5c79cbb6fb`, position 157. The graph, PR summary, and commit mapping below cover positions 001-157 and the fixups at positions - 334-338. -- NVX's pinned OpenVMM revision: `8a0d945edca13b301a5152f225702057337b2a56`, position 338, - which microsoft/openvmm#4734 proposes for `feature/microvm/dev`. It is one commit on - `79a501b63`, the head of the `feature/microvm/dev` and `feature/microvm/release/v2.3` - branches of microsoft/openvmm, and 181 commits past the mapped tip on the same first-parent - chain. Positions 158-333 are not yet assigned to PRs; see - [Commits after the mapped tip](#commits-after-the-mapped-tip). + 334-340. +- NVX's pinned OpenVMM revision: `2b681564dd63483f2f9c9f9bba3b98a4c00e9680`, position 340. + Positions 339 and 340, which microsoft/openvmm#4735 proposes for `feature/microvm/dev`, are + two commits on position 338, `8a0d945ed`, the head of `feature/microvm/dev` since + microsoft/openvmm#4734 merged it. Position 338 is one commit on `79a501b63`, the head of the + `feature/microvm/release/v2.3` branch of microsoft/openvmm, and the pin is 183 commits past + the mapped tip on the same first-parent chain. Positions 158-333 are not yet assigned to PRs; + see [Commits after the mapped tip](#commits-after-the-mapped-tip). - Upstream main: `22c0485be597f87a76771e1db03eaa823a685ffc` (2026-10-22), the fork base. ## Upstream status @@ -202,10 +203,10 @@ flowchart TB P17["PR17
Capture + authoritative restore
P - 11 commits"] P18["PR18
Host attachments
P - 11 commits + split tip"] P19["PR19
Block roles + tiers
N/P - 3 commits"] - P20["PR20
Restore activation
P - 8 commits + split tip"] + P20["PR20
Restore activation
P - 9 commits + split tip"] P21["PR21
Network egress policy
P/N - 6 commits"] P22["PR22
Filesystem deny policy
P - 2 commits"] - P23["PR23
Authenticated control console
N - 9 commits + split tip"] + P23["PR23
Authenticated control console
N - 10 commits + split tip"] P24["PR24
Managed workload/reporting
N - 5 commits"] P25["PR25
Management plane
P - 3 commits + split tip"] P26["PR26
Profiling benchmark
U - 1 commit"] @@ -302,7 +303,8 @@ The physical branch is one first-parent chain: -> unassigned [158-333] -> PR10 fixup [334] -> PR05/PR20/unassigned split [335] -> PR14 fixup [336] -> PR09 fixup [337] - -> PR02 fixup [338] -> 8a0d945ed (NVX pin) + -> PR02 fixup [338] -> PR20 fixup [339] -> PR23 fixup [340] + -> 2b681564d (NVX pin) ``` For upstream review, PR01-PR08 and PR14 can be rebased independently onto current upstream main, @@ -318,14 +320,16 @@ the commits that [Rebase conflicts and fixups](#rebase-conflicts-and-fixups) nam into PR10, position 336 into PR14, position 337 into PR09, and position 335 into PR05, the PR20 fixup at position 151, and the unassigned positions 163 and 197. Fold position 338 into PR02's position 010, which microsoft/openvmm#4558 carries. PR27 can be prepared once PR22 lands; its -generic confinement commit can be submitted independently if preferred. +generic confinement commit can be submitted independently if preferred. Fold position 339 into +PR20's position 120 and position 340 into PR23's position 136. ## Commits after the mapped tip -The NVX pin extends the chain past position 157 without merges. Of the 181 later commits, +The NVX pin extends the chain past position 157 without merges. Of the 183 later commits, positions 158-333 are not yet classified or assigned to PRs, positions 334-337 are the fixups -of the `feature/microvm/release/v2.3` rebase, and position 338 is a PR02 fixup. This table -groups them by topic in chain order; ranges are inclusive, as in the [PR summary](#pr-summary). +of the `feature/microvm/release/v2.3` rebase, position 338 is a PR02 fixup, and positions 339 +and 340 are PR20 and PR23 fixups. This table groups them by topic in chain order; ranges are +inclusive, as in the [PR summary](#pr-summary). | Positions | Commit range | Count | Topic | | ---: | --- | ---: | --- | @@ -361,6 +365,8 @@ groups them by topic in chain order; ranges are inclusive, as in the [PR summary | 333 | `fcb4ea1c6` | 1 | Petri and the VMM tests fall back to the host CPU profile where `auto`'s is unsupported | | 334-337 | `9bc90f55f..79a501b63` | 4 | Fixups of the `feature/microvm/release/v2.3` rebase; see [Rebase conflicts and fixups](#rebase-conflicts-and-fixups) | | 338 | `8a0d945ed` | 1 | Windows mapped-file flushes mark every page of a writable view modified and write the range in one ascending pass, so NTFS no longer zero-fills ranges that are then written again (microsoft/openvmm#4734); fold into 010 | +| 339 | `2f51e8bab` | 1 | Platform snapshots opt into restore-time vCPU activation with one canonical `maxcpus=` token, so each clone of a platform capture can bring its own VP prefix online; fold into 120 | +| 340 | `2b681564d` | 1 | The control-console authentication timeout counts only while the broker reads host input, so a gated restore no longer detaches a host that authenticated at launch; fold into 136 | Some of these commits change mapped work rather than add to it. Positions 246-250 remove the legacy microVM clock paths and change files that PR10 and PR17 also change. Position 252 makes @@ -393,10 +399,10 @@ as whole physical commits here but should be folded into their target commits be | PR17 | P | microVM snapshot capture and authoritative restore | 093-103 | `693d64ca8..d0cb54c9d` | 11 | PR16 | | PR18 | P | microVM host attachments and device persistence | 104-113; 153 (split); 154 (fixup) | `9611571b9..b7816551d`; `9638a8f42` (split); `e1586485a` | 11 + split | PR17 | | PR19 | N/P | Sandbox block roles and snapshot tiers | 114-116 | `cec1a5510..59cb42a01` | 3 | PR18 | -| PR20 | P | Restore-time processor and memory activation | 117-123; 151-152; 335 (split) | `9f2d6eeee..764d668d1`; `73a324390..9be096e1b`; `d2e83bdfd` (split) | 8 + split | PR19 | +| PR20 | P | Restore-time processor and memory activation | 117-123; 151-152; 335 (split); 339 (fixup) | `9f2d6eeee..764d668d1`; `73a324390..9be096e1b`; `d2e83bdfd` (split); `2f51e8bab` | 9 + split | PR19 | | PR21 | P/N | Network egress policy | 124-129 | `44ec6b08d..b6e8dc870` | 6 | PR20; core can start after PR07 + PR08 + PR13 | | PR22 | P | Filesystem deny policy | 130-131 | `07eb316dc..7894a3b50` | 2 | PR21 | -| PR23 | N | Authenticated control console | 132-140; 153 (split) | `2d649d42c..ed9d48548`; `9638a8f42` (split) | 9 + split | PR22; protocol/broker can start after PR03 + PR04 + PR13 | +| PR23 | N | Authenticated control console | 132-140; 153 (split); 340 (fixup) | `2d649d42c..ed9d48548`; `9638a8f42` (split); `2b681564d` | 10 + split | PR22; protocol/broker can start after PR03 + PR04 + PR13 | | PR24 | N | Managed workload and outcome reporting | 141-145 | `ddaa1810b..ad0b68265` | 5 | PR23 | | PR25 | P | Management plane | 146-147; 152 (split); 155 (fixup) | `941f8f720..a1820db81`; `9be096e1b` (split); `e9f23af5d` | 3 + split | PR24 | | PR26 | U | Profiling benchmark | 148 | `d90539d5a` | 1 | PR25 in the current stack | @@ -407,7 +413,7 @@ as whole physical commits here but should be folded into their target commits be Positions 152 and 153 intentionally appear under two PRs because their commits must be split when forming submission branches. Positions 154 and 155 are fixups that should be folded into positions -104 and 146, respectively. Tip positions 151-155 and the fixups at positions 334-338 are listed +104 and 146, respectively. Tip positions 151-155 and the fixups at positions 334-340 are listed under their destination PRs rather than in numerical order, and position 335 appears under both PRs that it changes. The counts distinguish whole commits from selected split hunks. Struck-through entries landed upstream before the fork base. The `microvm/v2.2` and @@ -438,7 +444,7 @@ Partial upstream overlap is called out without striking the remaining fork work, - 008 `8d72321f5` - mesh_protobuf: clone type-erased messages and expose encoded length (**partly landed upstream**: microsoft/openvmm#4556, merged as `96647c331`, derives `Clone`; this commit now adds only `ProtobufAny::value_len` and a clone check) - 009 `15a895dd8` - sparse_mmap: add private copy-on-write file views (**open upstream**: microsoft/openvmm#4557) - 010 `a95f8ef33` - sparse_mmap: flush file-backed ranges with bounded concurrency (**open upstream**: microsoft/openvmm#4558) -- 338 `8a0d945ed` - sparse_mmap: flush writable file views whole, in one ascending pass (**fold into 010**; proposed for `feature/microvm/dev` in microsoft/openvmm#4734) +- 338 `8a0d945ed` - sparse_mmap: flush writable file views whole, in one ascending pass (**fold into 010**; merged into `feature/microvm/dev` by microsoft/openvmm#4734) @@ -679,7 +685,7 @@ that upstream lacks.
-PR20 - Restore-time processor and memory activation (P) - 8 commits + split tip +PR20 - Restore-time processor and memory activation (P) - 9 commits + split tip - 117 `9f2d6eeee` - chipset: expose a stable generation ID and restore entropy on portb - 118 `310b3e8f5` - openvmm: gate external input until post-restore repair completes @@ -691,6 +697,7 @@ that upstream lacks. - 151 `73a324390` - virt_mshv: synchronize restored TSCs only on created VPs - 152 `9be096e1b` - openvmm_core: instantiate only the requested VP prefix on MSHV restore (**split with PR25**: worker and backend changes belong in PR20; the gRPC reference hunk belongs in PR25) - 335 `d2e83bdfd` - virt_mshv: adapt the microVM partition to partition scrub (**split fixup with PR05**: the hunk for position 151 belongs in PR20) +- 339 `2f51e8bab` - microVM: let platform snapshots opt into restore-time vCPU activation (**fold into 120**)
@@ -715,7 +722,7 @@ that upstream lacks.
-PR23 - Authenticated control console (N) - 9 commits + split tip +PR23 - Authenticated control console (N) - 10 commits + split tip - 132 `2d649d42c` - serial_core: expose local peer identity and active-peer eviction - 133 `cba534b9d` - serial_socket: report Unix and named-pipe peer identities @@ -727,6 +734,7 @@ that upstream lacks. - 139 `6c1043d08` - openvmm_helpers: record the control console in the microVM machine contract - 140 `ed9d48548` - openvmm_entry: add authenticated microVM control-console endpoints - 153 `9638a8f42` - Restore listener endpoint rebinding (**split with PR18**: authenticated control-listener portions belong in PR23; boot-listener and shared attachment-contract portions belong in PR18) +- 340 `2b681564d` - virtio_console: time host authentication only while host input is read (**fold into 136**)
From b5899557c67466941e908778ae89c0c710db8a62 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Fri, 23 Oct 2026 10:42:49 -0300 Subject: [PATCH 3/5] doc: let platform snapshots restore with --restore-processors The pinned OpenVMM accepts restore-time vCPU activation on platform snapshots. Say in the run guide's restore-readiness section that a platform snapshot opts in as other snapshots do, by capturing with a canonical maxcpus= boot-online prefix, so each clone of one platform capture can bring its own prefix online. Part of #796 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/run.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/run.md b/doc/run.md index 1d879d89..e308416b 100644 --- a/doc/run.md +++ b/doc/run.md @@ -534,7 +534,9 @@ complete while the restored vCPU remains stopped. With the source must have captured a canonical `maxcpus` boot-online prefix, and the guest onlines exactly the requested prefix before readiness and host input release. Targets are limited to 1, 2, 4, or 8 and cannot be below the captured -boot-online count or above capacity. Legacy snapshots reject the option. The +boot-online count or above capacity. Platform snapshots opt in the same way, so +each clone of one platform capture can bring its own prefix online. Legacy +snapshots reject the option. The peer must accept and read while startup is in progress; Windows flush completion waits for the named-pipe peer to consume the frame. Failure to write the complete event aborts and tears down the restore. From a6971a8badd397cf21e8a978e396d4539e398109 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Fri, 23 Oct 2026 10:42:50 -0300 Subject: [PATCH 4/5] scripts: test restore-time vCPU activation of a platform snapshot No test covered restore-time vCPU activation on a platform snapshot, which OpenVMM refused until the pinned fix. Add the platform-restore-processors microVM scenario to the default suite. The scenario captures a platform snapshot of capacity 4 whose only caller token is maxcpus=1, with a distro layer and a scratch disk, so the guest's nvx-snapshot takes the platform tier from the host. It restores that snapshot at each requested --restore-processors count up to the capacity, 1, 2, and 4 by default, and once without a target, each with a fresh scratch disk. Each restore must run the platform repair and pass the checks of restore-processors: the online CPU prefix, the warp probe over every pair of online CPUs, the time ABI's restore line, and the VP-binding profile, which on MSHV must bind only the target's prefix. OpenVMM must refuse the counts 0 and 3, which no microVM supports, and 8, above the capacity, before the guest runs, and no restore may change the snapshot. The scenario needs sandbox blocks, so it runs only with the Alpine sandbox-control guest, like snapshot-tiers. It took 10 to 17 s on bare-metal KVM, MSHV, and WHP hosts, so the shards are planned again with an estimate of 16 s, which CI's duration reports replace once CI has run it. Unit tests cover the capture and restore commands, the refused counts, the platform repair check, the MSHV prefix check, and the snapshot's integrity, and the runner tests cover its processor counts and its duration record. Part of #796 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/ci.md | 4 + doc/usage.md | 6 +- scripts/nvx_tools/microvm_shards.json | 30 ++-- scripts/nvx_tools/microvm_tests.py | 169 ++++++++++++++++++++++ scripts/test_microvm_tests.py | 201 +++++++++++++++++++++++++- 5 files changed, 392 insertions(+), 18 deletions(-) diff --git a/doc/ci.md b/doc/ci.md index e2484c7d..3f66a5b4 100644 --- a/doc/ci.md +++ b/doc/ci.md @@ -111,6 +111,10 @@ it once, and the first snapshot a second time to prove that a restore leaves it reusable. `restore-processors` captures one boot-online CPU with capacity 8 and probes the CPUs that each restore target activated. After the 1/2/4/8-CPU restores, it restores the same snapshot once without `--restore-processors`. +`platform-restore-processors` does the same for a platform snapshot of +capacity 4, with the requested counts up to 4: its capture boots with +`maxcpus=1` and sandbox blocks, each restore must also report the platform +repair, and OpenVMM must refuse the counts 0, 3, and 8 before the guest runs. After the warp probe, every `smp-snapshot` and `restore-processors` restore runs `/sbin/nvx-time status`, which waits for the restore's deferred checks; it must exit 0 with a passing `NVX-TIME-ABI ... phase=restore` line that diff --git a/doc/usage.md b/doc/usage.md index 94f0ed98..1e906166 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -814,7 +814,8 @@ and `sandbox-proxy-only` drive the public `sandbox` lifecycle commands (see Ubuntu layer and scratch template, which packages omit. Alpine remains the default. Ubuntu and Azure Linux cannot act as sandbox control, so they reject the Alpine-control-only `sandbox-blocks` and `scratch-snapshot` scenarios and -the sandbox-control-dependent `snapshot-tiers`, `managed-exec-config`, +the sandbox-control-dependent `snapshot-tiers`, `platform-restore-processors`, +`managed-exec-config`, `sandbox-lifecycle`, and `sandbox-proxy-only` scenarios. Ubuntu also rejects the Alpine-prompt-specific `console-snapshot` scenario. `--debug-kernel` boots the CI debug kernel, `build/vmlinux-debug`, whose soft-lockup and hung-task @@ -881,7 +882,8 @@ characters, and otherwise `null`. A `scenario` record also has the `scenario`, the `guest`, `backend`, and `kernel`, and its `processors`: the `--processors` counts that the execution covers, such as `[4]` for `smp` on 4 vCPUs, the largest count for `time-abi-conformance`, all of them for -`smp-snapshot` and `restore-processors`, or `null` for a scenario that doesn't +`smp-snapshot` and `restore-processors`, those up to 4 for +`platform-restore-processors`, or `null` for a scenario that doesn't take them. A failed run ends with the failed scenario's record and the `teardown`; a run that is killed keeps every record that it completed and has no `teardown`. diff --git a/scripts/nvx_tools/microvm_shards.json b/scripts/nvx_tools/microvm_shards.json index 6b166f43..f3391ef0 100644 --- a/scripts/nvx_tools/microvm_shards.json +++ b/scripts/nvx_tools/microvm_shards.json @@ -46,6 +46,7 @@ "lifecycle": 1.7, "managed-lifecycle": 4.2, "network-snapshot": 17.0, + "platform-restore-processors": 16.0, "private-network-ingress": 31.3, "restore-downtime": 69.2, "restore-memory": 4.1, @@ -63,31 +64,31 @@ }, "shards": [ { - "seconds": 228.8, + "seconds": 237.2, "scenarios": [ "console-snapshot", "cpu-profile-fallback", + "directional-network-policy", "endpoint-policy-snapshot", "filesystem-comma-paths", "filesystem-snapshot", "network-snapshot", + "platform-restore-processors", "restore-downtime", "restore-memory", "restore-processors", "scratch-snapshot", "smp-snapshot", - "snapshot-core", "snapshot-tiers", "time-abi-conformance" ] }, { - "seconds": 228.9, + "seconds": 236.5, "scenarios": [ "command-line-limit", "console-exit", "denied-filesystem-paths", - "directional-network-policy", "filesystem-owner", "filesystem-policy", "filesystem-shares", @@ -103,6 +104,7 @@ "private-network-ingress", "sandbox-blocks", "smp", + "snapshot-core", "structured-outcome", "virtio-net", "workload-identity" @@ -134,6 +136,7 @@ "lifecycle": 1.6, "managed-lifecycle": 4.3, "network-snapshot": 16.8, + "platform-restore-processors": 16.0, "private-network-ingress": 31.6, "restore-downtime": 71.0, "restore-memory": 4.0, @@ -151,14 +154,15 @@ }, "shards": [ { - "seconds": 208.0, + "seconds": 216.1, "scenarios": [ "console-snapshot", "cpu-profile-fallback", "directional-network-policy", - "endpoint-policy-snapshot", + "filesystem-shares", "filesystem-snapshot", "network-snapshot", + "platform-restore-processors", "restore-downtime", "restore-memory", "restore-processors", @@ -170,15 +174,15 @@ ] }, { - "seconds": 208.0, + "seconds": 215.9, "scenarios": [ "command-line-limit", "console-exit", "denied-filesystem-paths", + "endpoint-policy-snapshot", "filesystem-comma-paths", "filesystem-owner", "filesystem-policy", - "filesystem-shares", "guest-boot", "guest-identity", "host-loopback-policy", @@ -222,6 +226,7 @@ "lifecycle": 2.1, "managed-lifecycle": 17.8, "network-snapshot": 17.6, + "platform-restore-processors": 16.0, "private-network-ingress": 17.4, "restore-downtime": 84.7, "restore-memory": 12.5, @@ -239,14 +244,15 @@ }, "shards": [ { - "seconds": 300.4, + "seconds": 308.8, "scenarios": [ "console-snapshot", "cpu-profile-fallback", - "endpoint-policy-snapshot", + "filesystem-policy", "filesystem-snapshot", "host-loopback-policy", "network-snapshot", + "platform-restore-processors", "restore-downtime", "restore-memory", "restore-processors", @@ -258,15 +264,15 @@ ] }, { - "seconds": 301.1, + "seconds": 308.7, "scenarios": [ "command-line-limit", "console-exit", "denied-filesystem-paths", "directional-network-policy", + "endpoint-policy-snapshot", "filesystem-comma-paths", "filesystem-owner", - "filesystem-policy", "filesystem-shares", "guest-boot", "guest-identity", diff --git a/scripts/nvx_tools/microvm_tests.py b/scripts/nvx_tools/microvm_tests.py index 66be0b58..7647e318 100644 --- a/scripts/nvx_tools/microvm_tests.py +++ b/scripts/nvx_tools/microvm_tests.py @@ -118,6 +118,7 @@ "managed-lifecycle", "managed-exec-config", "network-snapshot", + "platform-restore-processors", "private-network-ingress", "restore-downtime", "restore-memory", @@ -139,6 +140,7 @@ SANDBOX_CONTROL_SCENARIOS = frozenset( ( "managed-exec-config", + "platform-restore-processors", "sandbox-blocks", "sandbox-lifecycle", "sandbox-proxy-only", @@ -297,6 +299,21 @@ GUEST_BOOT_COMPLETION_MARKER = b"NVX-GUEST-BOOT-CHECK-OK" GUEST_IDENTITY_COMPLETION_MARKER = b"NVX-GUEST-IDENTITY-OK" RESTORE_PROCESSORS_FAILURE_MARKER = b"NVX-RESTORE-PROCESSORS-FAIL" +# A platform snapshot opts into restore-time vCPU activation as an untiered one +# does, by capturing its capacity booted with maxcpus=. A capacity of 4 leaves +# a supported count, 8, above it. +PLATFORM_RESTORE_PROCESSORS_CAPACITY = 4 +PLATFORM_RESTORE_PROCESSORS_BOOT_ONLINE = 1 +# The restore-online counts that OpenVMM refuses before the guest runs, and +# why: counts that a microVM does not support, and a supported count above the +# capacity. The snapshot's restore preparation checks them before the +# command-line validation, which would refuse the unsupported counts too. +PLATFORM_RESTORE_PROCESSORS_REJECTED = ( + (0, b"restore-online VP count 0 is not supported"), + (3, b"restore-online VP count 3 is not supported"), + (8, b"restore-online VP count 8 exceeds VP capacity 4"), +) +PLATFORM_POST_RESTORE_MARKER = b"NVX-POST-RESTORE-OK: tier=platform" # Records the time ABI rates and restore clock report of each restore. These # records are written before the restored VPs run, so they never split a guest # console line; targets that log while the guest runs, such as virt_kvm's @@ -5047,6 +5064,137 @@ def run_restore_processors( ) +def run_platform_restore_processors( + executable: Path, + kernel: Path, + initrd: Path, + backend: str, + processor_counts: list[int], + *, + memory_mib: int, + timeout: float, + output_dir: Path, +) -> None: + """Bring a different vCPU prefix online in each clone of one platform + snapshot. + + The platform capture boots its capacity with ``maxcpus=1``, its only + caller token. Each restore of a requested count up to the capacity, and + one without a target, runs the platform repair and the checks of + ``restore-processors``. OpenVMM refuses unsupported and over-capacity + counts before the guest runs. + """ + capacity = PLATFORM_RESTORE_PROCESSORS_CAPACITY + boot_online = PLATFORM_RESTORE_PROCESSORS_BOOT_ONLINE + script = _read_script("restore-processors.sh") + post_restore_checks() + environment = _restore_environment() + environment["OPENVMM_LOG"] = TIME_ABI_RESTORE_LOG_FILTER + environment[SNAPSHOT_PROFILE_ENV] = "1" + with tempfile.TemporaryDirectory( + prefix="nvx-platform-restore-processors-" + ) as temporary: + root = Path(temporary) + snapshot_path = root / "snapshot" + layer = root / "distro.erofs" + capture_scratch = root / "capture-scratch.raw" + _write_pattern(layer, 1024 * 1024, 0x3C) + _write_pattern(capture_scratch, 1024 * 1024, 0xA5) + boot_command = workload_boot_command( + executable, + backend, + kernel, + initrd, + memory_mib, + f"maxcpus={boot_online}", + processors=capacity, + ) + capture_snapshot( + [ + *boot_command, + "--snapshot-destination", + str(snapshot_path), + "--snapshot-tier", + "platform", + "--microvm-sandbox-block", + _block_arg("distro", layer, read_only=True), + "--microvm-sandbox-block", + _block_arg("scratch", capture_scratch, read_only=False), + ], + snapshot_path, + timeout=timeout, + processors=boot_online, + post_restore_script=script, + log_path=output_dir / "platform-restore-processors-capture.log", + ) + fingerprint = _scratch_snapshot_fingerprint(snapshot_path) + + def restore_command(target: int | None, name: str) -> list[str]: + # Each clone gets its own fresh scratch of the captured geometry. + scratch = root / f"restore-scratch-{name}.raw" + _write_pattern(scratch, 1024 * 1024, 0x5A) + command = snapshot_restore_command( + executable, + backend, + snapshot_path, + processors=capacity, + restore_processors=target, + ) + command.extend( + ( + "--microvm-sandbox-block", + _block_arg("distro", layer, read_only=True), + "--microvm-sandbox-block", + _block_arg("scratch", scratch, read_only=False), + ) + ) + return command + + # An untargeted restore keeps the captured boot-online prefix. + targets: list[int | None] = [ + *(count for count in dict.fromkeys(processor_counts) if count <= capacity), + None, + ] + for target in targets: + name = "untargeted" if target is None else str(target) + online = boot_online if target is None else target + context = f"platform {_restore_label(target)}" + command = restore_command(target, name) + output = _measure_restore( + command, + context=context, + environment=environment, + timeout=timeout, + log_path=output_dir / f"platform-restore-processors-{name}.log", + failure_marker=RESTORE_PROCESSORS_FAILURE_MARKER, + ) + if not contains_output_line(output, PLATFORM_POST_RESTORE_MARKER): + raise RuntimeError(f"{context} did not run the platform repair") + marker = f"NVX-RESTORE-PROCESSORS-OK count={online}".encode() + if not contains_output_line(output, marker): + raise RuntimeError(f"{context} did not report {marker.decode()!r}") + _check_restore_warp(output, processors=online, context=context) + _check_restore_status(output, command, processors=online, context=context) + _check_restore_vp_bindings( + output, + backend, + target=target, + capacity=capacity, + ) + if _scratch_snapshot_fingerprint(snapshot_path) != fingerprint: + raise RuntimeError(f"{context} modified snapshot artifacts") + + for target, reason in PLATFORM_RESTORE_PROCESSORS_REJECTED: + _expect_process_failure( + restore_command(target, f"rejected-{target}"), + output_dir / f"platform-restore-processors-rejected-{target}.log", + timeout, + reason, + forbidden_markers=(RESTORE_MARKER,), + ) + if _scratch_snapshot_fingerprint(snapshot_path) != fingerprint: + raise RuntimeError("a refused platform restore modified snapshot artifacts") + + def run_restore_downtime( executable: Path, kernel: Path, @@ -8105,6 +8253,27 @@ def _run_scenarios( timeout=args.timeout, output_dir=output_dir, ) + if "platform-restore-processors" in scenarios: + report.start( + "platform-restore-processors", + "Running microVM platform-snapshot restore-processor correctness on " + f"OpenVMM/{args.backend}", + processors=[ + count + for count in args.processors + if count <= PLATFORM_RESTORE_PROCESSORS_CAPACITY + ], + ) + run_platform_restore_processors( + executable, + kernel, + initrd, + args.backend, + args.processors, + memory_mib=args.memory_mib, + timeout=args.timeout, + output_dir=output_dir, + ) if "restore-downtime" in scenarios: report.start( "restore-downtime", diff --git a/scripts/test_microvm_tests.py b/scripts/test_microvm_tests.py index 01ca1231..bb33cd5c 100644 --- a/scripts/test_microvm_tests.py +++ b/scripts/test_microvm_tests.py @@ -235,17 +235,20 @@ def _restore_processors_measure( *, mshv_prefix: bool = True, warp_offset_ns: int = 40, + capacity: int = 8, + prefix: bytes = b"", ) -> MagicMock: def measure(command: list[str], **kwargs: object) -> None: target = _restore_target(command) online = 1 if target is None else target - vp_count = 8 + vp_count = capacity if mshv_prefix and backend == "mshv" and target is not None: vp_count = target log_path = cast(Path, kwargs["log_path"]) log_path.parent.mkdir(parents=True, exist_ok=True) log_path.write_bytes( - f"NVX-RESTORE-PROCESSORS-OK count={online}\r\n".encode() + prefix + + f"NVX-RESTORE-PROCESSORS-OK count={online}\r\n".encode() + _warp_probe_output(online, offset_ns=warp_offset_ns) # The snapshot's guest booted with one online CPU. + _restore_status_output(backend, online, boot_cpus=1) @@ -9429,6 +9432,187 @@ def test_restore_processors_report_guest_failures_with_the_target(self): ) self.assertIs(raised.exception.__cause__, failure) + def _run_platform_restore_processors( + self, + backend: str, + processor_counts: list[int], + output_dir: Path, + measure_once: MagicMock, + *, + fingerprints: list[tuple[str, str, str, str | None]] | None = None, + ) -> tuple[MagicMock, MagicMock, MagicMock]: + fingerprint = ("manifest", "state", "memory", None) + with ( + patch.object( + microvm_tests, + "workload_boot_command", + return_value=["openvmm", "boot"], + ) as workload_boot_command, + patch.object(microvm_tests, "capture_snapshot") as capture_snapshot, + patch.object(microvm_tests, "measure_once", measure_once), + patch.object( + microvm_tests, + "_scratch_snapshot_fingerprint", + side_effect=fingerprints or itertools.repeat(fingerprint), + ), + patch.object(microvm_tests, "_expect_process_failure") as expect_failure, + ): + microvm_tests.run_platform_restore_processors( + Path("openvmm"), + Path("vmlinux"), + Path("initrd"), + backend, + processor_counts, + memory_mib=128, + timeout=60, + output_dir=output_dir, + ) + return workload_boot_command, capture_snapshot, expect_failure + + def test_platform_restore_processors_activate_each_target_of_one_capture(self): + def blocks(command: list[str]) -> list[str]: + return [ + command[index + 1] + for index, argument in enumerate(command) + if argument == "--microvm-sandbox-block" + ] + + with tempfile.TemporaryDirectory() as temporary: + output_dir = Path(temporary) / "logs" + measure_once = _restore_processors_measure( + "mshv", + capacity=4, + prefix=microvm_tests.PLATFORM_POST_RESTORE_MARKER + b"\r\n", + ) + workload_boot_command, capture_snapshot, expect_failure = ( + self._run_platform_restore_processors( + "mshv", [1, 2, 4, 8], output_dir, measure_once + ) + ) + logs = sorted(path.name for path in output_dir.iterdir()) + + self.assertIn( + "platform-restore-processors", microvm_tests.MICROVM_TEST_SCENARIOS + ) + self.assertIn( + "platform-restore-processors", microvm_tests.SANDBOX_CONTROL_SCENARIOS + ) + self.assertEqual(workload_boot_command.call_args.kwargs["processors"], 4) + # A platform command line carries no tenant configuration, so the + # boot-online count is the capture's only caller token. + self.assertEqual(workload_boot_command.call_args.args[5], "maxcpus=1") + capture = capture_snapshot.call_args.args[0] + self.assertEqual(capture[capture.index("--snapshot-tier") + 1], "platform") + capture_blocks = blocks(capture) + self.assertEqual(len(capture_blocks), 2) + self.assertRegex(capture_blocks[0], r"^distro:file:.+,ro$") + self.assertRegex(capture_blocks[1], r"^scratch:file:.+(? Path: "--scenario", "restore-processors", "--scenario", + "platform-restore-processors", + "--scenario", "time-abi-conformance", "--processors", "2", @@ -10452,10 +10638,13 @@ def require(path: Path, _description: str) -> Path: patch.object(microvm_tests, "require_file", side_effect=require), patch.object(microvm_tests, "run_smp_snapshot") as smp_snapshot, patch.object(microvm_tests, "run_restore_processors") as processors, + patch.object( + microvm_tests, "run_platform_restore_processors" + ) as platform_processors, patch.object(microvm_tests, "run_time_abi_conformance") as conformance, ): self.assertEqual(microvm_tests.run(args), 0) - for run in (smp_snapshot, processors): + for run in (smp_snapshot, processors, platform_processors): run.assert_called_once() self.assertEqual(run.call_args.args[4], [2, 4]) self.assertEqual(run.call_args.kwargs["output_dir"], output_dir) @@ -10598,7 +10787,11 @@ def require(path: Path, _description: str) -> Path: for scenario in scenarios: if scenario in ("console-exit", "smp", "smp-lapic"): expected.extend((scenario, [processors]) for processors in (2, 1)) - elif scenario in ("smp-snapshot", "restore-processors"): + elif scenario in ( + "smp-snapshot", + "restore-processors", + "platform-restore-processors", + ): expected.append((scenario, [2, 1])) elif scenario == "time-abi-conformance": expected.append((scenario, [2])) From 42bfb8e13d63b4e04b15bb67b98f0044b3dd9259 Mon Sep 17 00:00:00 2001 From: Pedro Henrique Penna Date: Fri, 23 Oct 2026 10:42:50 -0300 Subject: [PATCH 5/5] scripts: benchmark restore-time vCPU activation of a platform snapshot Add the platform-snapshot-restore-vcpu workload suite, which restores one platform snapshot of capacity 8 at the online targets 1, 2, 4, and 8. Like snapshot-restore-vcpu, it reports each target's restore latency and OpenVMM's peak RSS, and its lifecycle phases with --snapshot-profile. A platform command line carries no tenant tokens, so the capture boots with maxcpus=1 as its only caller token, a distro layer, and a scratch disk, and the guest captures from its shell instead of with the pre-banner capture. Each restore reattaches the blocks and reaches its marker after the guest's platform repair, which onlines the target's prefix, refreshes the clone's identity, and acknowledges the restore gate. The restored guest then waits for the host to end it: a sandbox guest's virtio console can deliver the marker after a prequeued exit has started OpenVMM's teardown, too late to sample its peak RSS. The suite is explicit, so it changes no performance baseline. On bare-metal hosts, its medians are close to snapshot-restore-vcpu's at every target: for 1, 2, 4, and 8 vCPUs, 82, 136, 154, and 232 ms on Linux/MSHV, 68, 152, 200, and 231 ms on Linux/KVM, and 178, 385, 486, and 530 ms on Windows/WHP. Part of #796 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- doc/benchmarks.md | 19 ++++ doc/usage.md | 2 +- scripts/nvx_tools/benchmark.py | 134 +++++++++++++++++++++++- scripts/test_nvx_tools.py | 185 +++++++++++++++++++++++++++++++++ 4 files changed, 338 insertions(+), 2 deletions(-) diff --git a/doc/benchmarks.md b/doc/benchmarks.md index e5da38df..50e60720 100644 --- a/doc/benchmarks.md +++ b/doc/benchmarks.md @@ -139,6 +139,24 @@ guest resume behavior: python3 scripts/nvx.py benchmark --suite snapshot-restore-vcpu --backend mshv --processors 8 --memory-mib 128 --warmups 1 --runs 5 --snapshot-profile --skip-build --output-dir data/runs/restore-vcpu-mshv-capacity-8 ``` +Measure the same activation from one platform snapshot, the clone tier that +a fleet restores with a different size each time: + +```console +python3 scripts/nvx.py benchmark --suite platform-snapshot-restore-vcpu --backend kvm --processors 8 --memory-mib 512 --warmups 1 --runs 5 --skip-build --output-dir data/runs/platform-restore-vcpu-kvm +``` + +A platform command line carries no tenant configuration, so the suite cannot +use the pre-banner capture: the capture boots with `maxcpus=1` as its only +command-line option, a distro layer, and a scratch disk, and the guest captures +from its shell with `--snapshot-tier platform`. Each restore reattaches the +blocks and reaches its marker after the guest finishes the platform repair, +which brings the target prefix online, refreshes the clone's identity, and +acknowledges the restore gate. Its latency is therefore comparable between +targets and backends, not with `snapshot-restore-vcpu`. `--snapshot-profile` +adds the lifecycle phases of each target, and the results stay out of the +performance CSVs. + Measure restore-time memory activation from one immutable 512 MiB snapshot with 2 GiB of ABI-reserved capacity: @@ -260,6 +278,7 @@ python3 scripts/nvx.py performance collect --platform linux-kvm-baremetal --comm | Shell snapshot | `benchmark --suite shell-snapshot` | Compares cold boot with lifecycle-aligned snapshot restore at 128, 256, and 512 MiB. | | Shell snapshot restore | `benchmark --suite shell-snapshot-restore` | Captures an unmeasured lifecycle-aligned snapshot and measures only restore latency for the selected memory sizes. | | Restore-time vCPU activation | `benchmark --suite snapshot-restore-vcpu --processors 8` | Restores one boot-online-1, capacity-8 snapshot at online targets 1/2/4/8 and reports latency plus peak RSS. | +| Platform restore-time vCPU activation | `benchmark --suite platform-snapshot-restore-vcpu --processors 8` | Restores one boot-online-1, capacity-8 platform snapshot at online targets 1/2/4/8, through the platform repair, and reports latency plus peak RSS. | | Network snapshot | `benchmark --suite network-snapshot` | Compares a network-ready cold boot with snapshot restore and verifies gateway connectivity. | | Snapshot lifecycle profile | `benchmark --suite snapshot-profile` | Retains raw capture and restore phase records and summarizes 128/256/512/1024 MiB warm/cold restores. | | Virtio device restore profile | `benchmark --suite device-restore-profile` | Verifies active and driver-unbound deferred restore for console, network, and virtio-fs; emits standalone diagnostic JSON and raw logs. | diff --git a/doc/usage.md b/doc/usage.md index 1e906166..b87bda0a 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -1435,7 +1435,7 @@ python3 scripts/nvx.py benchmark [OPTIONS] | Option | Default | Description | | --- | --- | --- | -| `--suite {boot,snapshot,restore,e2e,phase2,snapshot-profile,all,cold-start,device-io,device-restore-profile,network-snapshot,performance,shell-snapshot,shell-snapshot-restore,snapshot-restore-memory,snapshot-restore-vcpu,virtfs}` | `boot` | Select an acceptance, diagnostic, or workload suite. | +| `--suite {boot,snapshot,restore,e2e,phase2,snapshot-profile,all,cold-start,device-io,device-restore-profile,network-snapshot,performance,platform-snapshot-restore-vcpu,shell-snapshot,shell-snapshot-restore,snapshot-restore-memory,snapshot-restore-vcpu,virtfs}` | `boot` | Select an acceptance, diagnostic, or workload suite. | | `--backend {whp,kvm,mshv,both}` | `both` on Windows; `kvm` elsewhere | Select the hypervisor backend. | | `--platform NAME` | inferred OS/backend | Record the host-typed performance series. | | `--openvmm-dir PATH` | `openvmm/` | Select the OpenVMM repository. | diff --git a/scripts/nvx_tools/benchmark.py b/scripts/nvx_tools/benchmark.py index 1d801cea..ec760276 100644 --- a/scripts/nvx_tools/benchmark.py +++ b/scripts/nvx_tools/benchmark.py @@ -81,6 +81,7 @@ "shell-snapshot-restore", "snapshot-restore-memory", "snapshot-restore-vcpu", + "platform-snapshot-restore-vcpu", "network-snapshot", "device-restore-profile", "performance", @@ -125,6 +126,8 @@ SHELL_SNAPSHOT_MEMORY_MIB = (128, 256, 512) SNAPSHOT_PROFILE_MEMORY_MIB = (*SHELL_SNAPSHOT_MEMORY_MIB, 1024) RESTORE_VCPU_TARGETS = (1, 2, 4, 8) +# The size of the distro layer and scratch disk of a platform snapshot. +PLATFORM_SNAPSHOT_BLOCK_BYTES = 1024 * 1024 RESTORE_MEMORY_BASE_MIB = 512 RESTORE_MEMORY_TARGETS_MIB = (512, 1024, 2048) RESTORE_MEMORY_MARKER_PATTERN = re.compile( @@ -4273,6 +4276,121 @@ def benchmark_snapshot_restore_vcpu_workload( print() +def benchmark_platform_snapshot_restore_vcpu_workload( + args: argparse.Namespace, + executable: Path, + kernel: Path, + initrd: Path, + backend: str, + *, + command_prefix: Sequence[str] = (), + windows_cpus: set[int] | None = None, +) -> None: + """Restore one platform snapshot at each online vCPU target. + + The capture boots the capacity with ``maxcpus=1``, the only caller token + that a platform command line may carry, and sandbox blocks. Each restore + reattaches the blocks and runs until the guest finishes the platform + repair, which brings the target's vCPU prefix online. + """ + if args.processors != 8: + raise ValueError("platform-snapshot-restore-vcpu requires --processors 8") + snapshot_profile = bool(getattr(args, "snapshot_profile", False)) + + print( + "platform snapshot restore vCPU activation, " + f"median of {args.runs} runs, capacity 8, boot-online 1" + ) + print(f'marker : "{RESTORE_MARKER.decode()}"') + print(f"kernel : {kernel}") + print(f"initrd : {initrd}") + print() + with tempfile.TemporaryDirectory( + prefix="openvmm-platform-vcpu-restore-" + ) as temporary: + root = Path(temporary) + snapshot_path = root / "snapshot" + layer = root / "distro.erofs" + scratch = root / "scratch.raw" + layer.write_bytes(b"\x3c" * PLATFORM_SNAPSHOT_BLOCK_BYTES) + scratch.write_bytes(b"\xa5" * PLATFORM_SNAPSHOT_BLOCK_BYTES) + # The guest never mounts the scratch of a platform capture, so every + # restore can reattach the same one. + sandbox_blocks = ( + "--microvm-sandbox-block", + f"distro:file:{layer},ro", + "--microvm-sandbox-block", + f"scratch:file:{scratch}", + ) + capture_command = workload_boot_command( + executable, + backend, + kernel, + initrd, + args.memory_mib, + "maxcpus=1", + processors=args.processors, + command_prefix=command_prefix, + ) + # A sandbox guest's console is a virtio console, which can deliver the + # restore marker after a prequeued exit has started OpenVMM's teardown, + # too late for its peak RSS. The restored guest therefore waits at its + # shell, and each sample ends it after the marker with --teardown-mode. + capture_snapshot( + [ + *capture_command, + "--snapshot-destination", + str(snapshot_path), + "--snapshot-tier", + "platform", + *sandbox_blocks, + ], + snapshot_path, + timeout=args.timeout, + windows_cpus=windows_cpus, + processors=1, + teardown_mode="host-terminate", + ) + + for target in RESTORE_VCPU_TARGETS: + restored = benchmark( + [ + *command_prefix, + *snapshot_restore_command( + executable, + backend, + snapshot_path, + processors=args.processors, + restore_processors=target, + ), + *sandbox_blocks, + ], + warmups=args.warmups, + runs=args.runs, + timeout=args.timeout, + marker=RESTORE_MARKER, + marker_must_be_line=True, + windows_cpus=windows_cpus, + teardown_mode=args.teardown_mode, + snapshot_profile=snapshot_profile, + ) + print(f"== restore-online {target} vCPU ==") + print( + _format_shell_snapshot_line("snapshot restore", restored["samples_ms"]) + ) + print( + f" OpenVMM peak RSS : {format_rss_summary(restored['peak_rss_samples_bytes'])}" + ) + restore_profile = restored.get("profile") + if restore_profile is not None: + print_lifecycle_profile_summary( + f"platform-snapshot-restore-vcpu/{backend}/" + f"capacity-{args.processors}/online-{target}", + restore_profile, + ) + print() + + def benchmark_network_snapshot_workload( args: argparse.Namespace, executable: Path, @@ -4506,7 +4624,7 @@ def write_benchmark_metadata( "processors": processors, "restore_processor_targets": ( list(RESTORE_VCPU_TARGETS) - if args.suite == "snapshot-restore-vcpu" + if args.suite in {"snapshot-restore-vcpu", "platform-snapshot-restore-vcpu"} else None ), "restore_memory_base_mib": ( @@ -4654,6 +4772,8 @@ def run_workload_benchmarks( (output_dir / "acceptance.json").unlink(missing_ok=True) if args.suite == "snapshot-restore-vcpu": (output_dir / "snapshot-restore-vcpu.log").unlink(missing_ok=True) + if args.suite == "platform-snapshot-restore-vcpu": + (output_dir / "platform-snapshot-restore-vcpu.log").unlink(missing_ok=True) if args.suite == "snapshot-restore-memory": (output_dir / "snapshot-restore-memory.log").unlink(missing_ok=True) metadata_path = write_benchmark_metadata( @@ -4764,6 +4884,18 @@ def run_workload_benchmarks( windows_cpus=windows_cpus, ), ), + "platform-snapshot-restore-vcpu": ( + "platform-snapshot-restore-vcpu.log", + lambda: benchmark_platform_snapshot_restore_vcpu_workload( + args, + executable, + kernel, + initrd, + backend, + command_prefix=command_prefix, + windows_cpus=windows_cpus, + ), + ), "network-snapshot": ( "network.log", lambda: benchmark_network_snapshot_workload( diff --git a/scripts/test_nvx_tools.py b/scripts/test_nvx_tools.py index bfbcf125..d623e6d4 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -952,6 +952,23 @@ def test_benchmark_exposes_restore_vcpu_matrix(self): self.assertEqual(args.memory_mib, 512) self.assertIs(args.handler, nvx.command_benchmark) + def test_benchmark_exposes_platform_restore_vcpu_matrix(self): + args = nvx.parse_args( + [ + "benchmark", + "--suite", + "platform-snapshot-restore-vcpu", + "--processors", + "8", + "--output-dir", + "results", + ] + ) + + self.assertEqual(args.suite, "platform-snapshot-restore-vcpu") + self.assertIn(args.suite, benchmark.WORKLOAD_SUITES) + self.assertIs(args.handler, nvx.command_benchmark) + def test_benchmark_exposes_snapshot_profile_matrix(self): args = nvx.parse_args( [ @@ -23403,6 +23420,174 @@ def test_restore_vcpu_suite_reuses_one_capacity_snapshot_for_matrix(self): "kvm", ) + def test_platform_restore_vcpu_suite_restores_one_platform_capture(self): + args = argparse.Namespace( + processors=8, + memory_mib=512, + warmups=1, + runs=5, + timeout=40.0, + teardown_mode="guest-exit", + snapshot_profile=True, + ) + result = cast( + benchmark.BenchmarkResult, + { + "samples_ms": [10.0, 12.0], + "peak_rss_samples_bytes": [1024, 2048], + "profile": { + "schema_version": 1, + "raw_samples": [], + "phases": { + "restore.guest_repair_gate": { + "exclusive": False, + "source": "openvmm", + "samples_ms": [2.0], + "p50_ms": 2.0, + "p95_ms": 2.0, + "min_ms": 2.0, + "max_ms": 2.0, + } + }, + }, + }, + ) + + def blocks(command: list[str]) -> list[str]: + return [ + command[index + 1] + for index, argument in enumerate(command) + if argument == "--microvm-sandbox-block" + ] + + prefix = ("taskset", "-c", "2") + with ( + patch.object(benchmark, "capture_snapshot") as capture, + patch.object(benchmark, "benchmark", return_value=result) as run, + patch("sys.stdout", new_callable=io.StringIO) as output, + ): + benchmark.benchmark_platform_snapshot_restore_vcpu_workload( + args, + Path("openvmm"), + Path("vmlinux"), + Path("initrd"), + "mshv", + command_prefix=prefix, + ) + + capture.assert_called_once() + self.assertEqual(capture.call_args.kwargs["timeout"], args.timeout) + self.assertEqual(capture.call_args.kwargs["processors"], 1) + # The restored guest waits for the host after the marker instead of + # running a prequeued exit, so its peak RSS can be sampled. + self.assertNotEqual(capture.call_args.kwargs["teardown_mode"], "guest-exit") + capture_command = capture.call_args.args[0] + self.assertEqual(tuple(capture_command[:3]), prefix) + self.assertEqual( + capture_command[capture_command.index("--processors") + 1], "8" + ) + # The platform command line takes no tenant tokens, such as quiet or + # the pre-banner capture's shellsnap. + self.assertEqual( + capture_command[capture_command.index("--cmdline") + 1], "maxcpus=1" + ) + self.assertEqual( + capture_command[capture_command.index("--snapshot-tier") + 1], "platform" + ) + capture_blocks = blocks(capture_command) + self.assertEqual(len(capture_blocks), 2) + self.assertRegex(capture_blocks[0], r"^distro:file:.+,ro$") + self.assertRegex(capture_blocks[1], r"^scratch:file:.+(?