diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b340ed4..d2622b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31 with: install_url: https://releases.nixos.org/nix/nix-2.35.1/install - - run: nix develop --command cargo fmt --all -- --check + - run: nix develop .#light --command cargo fmt --all -- --check lint: name: Lint @@ -30,7 +30,7 @@ jobs: with: install_url: https://releases.nixos.org/nix/nix-2.35.1/install - run: bash -n .agents/dev .agents/setup scripts/*.sh - - run: nix develop --command cargo clippy --locked --workspace --all-targets --all-features -- --deny warnings + - run: nix develop .#light --command cargo clippy --locked --workspace --all-targets --all-features -- --deny warnings test: name: Test (${{ matrix.os }}) @@ -45,7 +45,7 @@ jobs: - uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31 with: install_url: https://releases.nixos.org/nix/nix-2.35.1/install - - run: nix develop --command cargo test --locked --workspace --all-targets --all-features + - run: nix develop .#light --command cargo test --locked --workspace --all-targets --all-features qemu-replay: name: QEMU record/replay @@ -105,12 +105,12 @@ jobs: # and fails unless every requested run completes, the verdict is # `reproducible`, and the guest measurements equal the reference host's. - run: nix develop --command ./scripts/time-model-gate.sh - # The known record-mode result: with rr=record and sleep=off, QEMU queues - # live serial input and never flushes the queue. Asserting it here makes a - # QEMU change that starts delivering input visible, because that would - # unblock pinning the model in the product. + # The pinned model delivers input because the pinned emulator carries the + # replay-flush patch: without it, QEMU queues live serial input with + # sleep=off and never flushes the queue, so a recording cannot be driven. + # Asserting delivery here keeps that patch load-bearing. - name: Pinned model host-input assertion - run: nix develop --command ./scripts/time-model-input-probe.sh --expect not-delivered + run: nix develop --command ./scripts/time-model-input-probe.sh --expect delivered - run: nix develop --command ./scripts/phase4-acceptance.sh - if: failure() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 diff --git a/crates/simferret/src/vm.rs b/crates/simferret/src/vm.rs index b16fa57..8065a86 100644 --- a/crates/simferret/src/vm.rs +++ b/crates/simferret/src/vm.rs @@ -24,20 +24,26 @@ const MEMORY_MIB: u32 = 128; /// The QEMU time model that record and replay both launch. `shift=auto` lets /// QEMU adjust the virtual CPU speed against host real time. /// -/// RFD 4 pins `shift=4,sleep=off`, and Phase 0 validated that model as -/// reproducible on the reference host, but the product cannot launch it yet: -/// with `rr=record`, QEMU queues live serial input as a replay asynchronous -/// event and flushes the queue only from `icount_account_warp_timer()`, which -/// returns before `replay_async_events()` when `sleep=off`. A guest under that -/// model therefore never receives the control commands a recording is driven -/// with. `rfd/0004/EVIDENCE.adoc` records the measurement and the minimal -/// reproduction, and `rfd/0004/IMPLEMENTATION.org` records that pinning this -/// value is blocked on the RFD's model decision rather than on this code. +/// RFD 4 pins `shift=4,sleep=off`: a fixed scale, so the guest's virtual time +/// advances with the instructions it executes and its idle timers advance +/// deterministically rather than with host scheduling, and no idle sleep, so the +/// guest's clock is not paced by the host either. /// -/// Changing this value is a versioned change: recordings made under another -/// model are not comparable, and every host in the supported matrix must be -/// re-validated. -pub const TIME_MODEL: &str = "shift=auto"; +/// The pinned QEMU carries `poc/time-model/qemu-nosleep-replay-flush.patch`, +/// applied by the flake's pinned emulator. With `sleep=off` alone, QEMU queues +/// live serial input as a replay asynchronous event and flushes the queue only +/// from `icount_account_warp_timer()`, which returns before +/// `replay_async_events()`; the patch moves the flush ahead of the sleep check, +/// so a recording can be driven. It is carried until upstream includes the fix, +/// and dropping it is a versioned change: recordings made under a different +/// emulator are not comparable, and every host in the supported matrix must be +/// re-validated. `rfd/0004/EVIDENCE.adoc` records the measurement, the pin, and +/// the spike that established the patch. +/// +/// Changing this value is a versioned change for the same reason: the model is +/// part of VM identity, and recordings made under another model are not +/// comparable. +pub const TIME_MODEL: &str = "shift=4,sleep=off"; const START_TIMEOUT: Duration = Duration::from_secs(10); const COMMAND_TIMEOUT: Duration = Duration::from_secs(10); /// The largest number of command bytes written before the writer waits for the @@ -1866,16 +1872,12 @@ mod tests { #[test] fn record_and_replay_launch_the_configured_time_model() { - // The literal is the model the product launches today. RFD 4 pins - // `shift=4,sleep=off` and Phase 0 validated that model on the reference - // host, but the product cannot launch it yet: with `rr=record` QEMU - // queues live serial input as a replay asynchronous event and only - // delivers it from `icount_account_warp_timer()`, which returns before - // `replay_async_events()` when `sleep=off`, so a guest under that model - // never receives the commands a recording is driven with. Pinning the - // model is therefore a versioned change that also updates this - // assertion and re-validates every supported host. - assert_eq!(TIME_MODEL, "shift=auto"); + // The literal is the model the product launches. It is pinned by RFD 4 + // and reaches QEMU through the pinned emulator, which carries the + // replay-flush patch `sleep=off` needs; changing either the value or the + // emulator is a versioned change that also updates this assertion and + // re-validates every supported host. + assert_eq!(TIME_MODEL, "shift=4,sleep=off"); let root = Path::new("/tmp/time-model"); for (mode, rr) in [ diff --git a/docs/time-model.adoc b/docs/time-model.adoc index 2d3a55e..e22c5d0 100644 --- a/docs/time-model.adoc +++ b/docs/time-model.adoc @@ -26,7 +26,7 @@ measurements. Current progress lives in the implementation checklist, not here. | Implementation evidence | link:../rfd/0004/EVIDENCE.adoc[RFD 4 evidence] -| The pinned-scale selection and its reason, the Phase 0 validation on the reference host, the record-mode limitation that blocks pinning the model, and the re-validation trigger +| The pinned-scale selection and its reason, the Phase 0 validation on the reference host, the record-mode limitation that the pinned emulator's carried patch resolves, and the re-validation trigger | Validation gate | link:../scripts/time-model-gate.sh[`scripts/time-model-gate.sh`] @@ -42,15 +42,15 @@ measurements. Current progress lives in the implementation checklist, not here. | Host-input probe | link:../poc/time-model/input-probe.c[`poc/time-model/input-probe.c`] with link:../scripts/time-model-input-probe.sh[`scripts/time-model-input-probe.sh`] -| Whether a guest that waits for host input receives it under a given model, reported as `delivered`, `not-delivered`, or `inconclusive` from a guest that polls until the probe stops it, with the write ordered by acknowledged stop and resume commands on QEMU's monitor, and with a non-delivery claim requiring the emulator's output to have ended in one piece, the measurement that shows the pinned model does not deliver while record/replay is enabled, and the `--expect` mode the continuous-integration job uses to assert that known result +| Whether a guest that waits for host input receives it under a given model, reported as `delivered`, `not-delivered`, or `inconclusive` from a guest that polls until the probe stops it, with the write ordered by acknowledged stop and resume commands on QEMU's monitor, and with a non-delivery claim requiring the emulator's output to have ended in one piece, the measurement that shows the pinned emulator's carried patch delivers while record/replay is enabled, and the `--expect` mode the continuous-integration job uses to assert that result | Host-input guest tests | link:../poc/time-model/input-probe-test.c[`poc/time-model/input-probe-test.c`] with link:../scripts/time-model-input-probe-test.sh[`scripts/time-model-input-probe-test.sh`] | The guest loop driven by stubs: a line that arrives after more than the polls the guest used to stop after is still reported as received, and the guest never reports giving up, so restoring either fails rather than passing | QEMU patch spike -| link:../poc/time-model/qemu-nosleep-replay-flush.patch[`qemu-nosleep-replay-flush.patch`] with link:../poc/time-model/qemu-nosleep-replay-spike.nix[`qemu-nosleep-replay-spike.nix`] -| The minimal QEMU change that flushes queued replay events with `sleep=off`, and the scratch expression that builds the pinned QEMU with it; neither is wired into the repository's build, and the measurements are in the evidence document +| link:../poc/time-model/qemu-nosleep-replay-flush.patch[`qemu-nosleep-replay-flush.patch`] with link:../flake.nix[`flake.nix`] +| The minimal QEMU change that flushes queued replay events with `sleep=off`, applied by the flake to the pinned emulator; the measurements are in the evidence document, and the patch is carried until upstream includes it | Probe source | link:../poc/time-model/probe.c[`poc/time-model/probe.c`] @@ -104,11 +104,7 @@ reports variation instead of failing on it: predictions of production timing. * Time, entropy, and ordering fault injection remain unimplemented, and the arrival timing of live host input has no deterministic position in guest virtual - time; both are separate work. -* The pinned model cannot be adopted for record mode yet: with `sleep=off` and - `rr=record`, QEMU queues live serial input as a replay asynchronous event and - never flushes the queue, so a guest that waits for the control commands a - recording is driven with never receives them. The measurement, the models it + time; both are separate work.is carried by the pinned emulator and adopted by the product. The measurement, the models it was compared against, and the source-level cause are in link:../rfd/0004/EVIDENCE.adoc[RFD 4 evidence], and `scripts/time-model-input-probe.sh` reproduces them. Continuous integration diff --git a/flake.nix b/flake.nix index fb3fafe..d1ead2e 100644 --- a/flake.nix +++ b/flake.nix @@ -29,6 +29,19 @@ "x86_64-unknown-linux-musl" ]; }; + # The pinned emulator, built from the repository's patch rather than from + # a second expression: `sleep=off` needs the replay flush the patch + # restores, and only the system target the project runs is built. + # `rfd/0004/README.adoc` records why it is carried and what dropping it + # means. + qemuPinned = (pkgs.qemu.override { + hostCpuTargets = [ "x86_64-softmmu" ]; + enableDocs = false; + }).overrideAttrs (previous: { + patches = (previous.patches or [ ]) ++ [ + ./poc/time-model/qemu-nosleep-replay-flush.patch + ]; + }); in { devShells.default = pkgs.mkShell { nativeBuildInputs = [ @@ -40,7 +53,29 @@ pkgs.cpio pkgs.gzip pkgs.pkgsStatic.stdenv.cc - pkgs.qemu + qemuPinned + pkgs.xz + ]; + RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; + shellHook = pkgs.lib.optionalString pkgs.stdenv.hostPlatform.isLinux '' + export SIMFERRET_BUSYBOX="${pkgs.pkgsStatic.busybox}/bin/busybox" + export SIMFERRET_KERNEL="${pkgs.linuxPackages.kernel}/bzImage" + export SIMFERRET_KERNEL_MODULES="${pkgs.linuxPackages.kernel.modules}" + ''; + }; + + # Jobs that do not run QEMU use this shell, so they do not build the + # pinned emulator. The QEMU job uses the default shell. + devShells.light = pkgs.mkShell { + nativeBuildInputs = [ + rustToolchain + pkgs.jujutsu + pkgs.jq + pkgs.python3 + ] ++ pkgs.lib.optionals pkgs.stdenv.hostPlatform.isLinux [ + pkgs.cpio + pkgs.gzip + pkgs.pkgsStatic.stdenv.cc pkgs.xz ]; RUST_SRC_PATH = "${rustToolchain}/lib/rustlib/src/rust/library"; diff --git a/poc/time-model/qemu-nosleep-replay-spike.nix b/poc/time-model/qemu-nosleep-replay-spike.nix deleted file mode 100644 index bf4f8f9..0000000 --- a/poc/time-model/qemu-nosleep-replay-spike.nix +++ /dev/null @@ -1,22 +0,0 @@ -# Spike expression: build the pinned QEMU with the no-sleep replay flush patch. -# -# This file is a feasibility spike for the RFD 4 Phase 0 record-mode blocker, not -# a project dependency. It is deliberately not wired into flake.nix and nothing -# in the repository builds it: carrying a patched QEMU is a decision the project -# has to make. Build it explicitly, for example: -# -# rev=$(jq -r '.nodes.nixpkgs.locked.rev' flake.lock) -# nix build --impure --no-link --print-out-paths --expr " -# let pkgs = import (builtins.fetchTarball -# \"https://github.com/NixOS/nixpkgs/archive/$rev.tar.gz\") {}; -# in import ./poc/time-model/qemu-nosleep-replay-spike.nix { inherit pkgs; }" -# -# and point the probes and suites at the result with -# QEMU_SYSTEM_X86_64=/bin/qemu-system-x86_64. -# -# The patch and its measured results are recorded in rfd/0004/EVIDENCE.adoc. -{ pkgs }: - -pkgs.qemu.overrideAttrs (old: { - patches = (old.patches or [ ]) ++ [ ./qemu-nosleep-replay-flush.patch ]; -}) diff --git a/poc/time-model/reference-host-values.txt b/poc/time-model/reference-host-values.txt index 3950de8..ab47360 100644 --- a/poc/time-model/reference-host-values.txt +++ b/poc/time-model/reference-host-values.txt @@ -14,7 +14,10 @@ # passing with a warning. # # The artifact identities are SHA-256 digests of the QEMU executable, the guest -# kernel, and the probe's initramfs. The QEMU digest rather than only its version +# kernel, and the probe's initramfs. The QEMU digest is the pinned emulator the +# flake builds, which carries `poc/time-model/qemu-nosleep-replay-flush.patch`; +# re-measuring the pinned model against it reproduced every value below, so the +# patch does not move the guest's measurements. The QEMU digest rather than only its version # string is what makes a rebuilt or patched QEMU visible: a binary with an # unchanged version string would otherwise pass silently. # @@ -30,7 +33,7 @@ icount_options=shift=4,sleep=off kernel_sha256=d4c2d110cb9b82dad81517cc2f1f3a46d0e31c8937237cf3df59eeffb3d7070b initramfs_sha256=24d0dfffc169a4275657c61281097882447fd565ecff87eb619cc587a57e1b4c qemu_version=QEMU emulator version 11.1.0 -qemu_sha256=a26dcb363d6817495c7fdfe824f0c60e9c0e6369d90fdbfda9648d40f8dbdbd7 +qemu_sha256=2c6adfd8fc39a972ebb6051f7105f10f11cc2715efd1ff0b4e3385809a0fae0d measurement=probe sleep requested_ns=20000000 observed_ns=21950896 measurement=probe spin chunks=64 window_ns=20000000 state=13811787166479837696 measurement=probe race work=2000000 elapsed_ns=182041872 within_deadline=no diff --git a/rfd/0004/EVIDENCE.adoc b/rfd/0004/EVIDENCE.adoc index eeb8ba5..a409f8a 100644 --- a/rfd/0004/EVIDENCE.adoc +++ b/rfd/0004/EVIDENCE.adoc @@ -164,6 +164,10 @@ The checked-in command is: On the reference host (AMD Ryzen 5 3600, 12 threads, Linux 6.18.41, QEMU 11.1.0, guest kernel 6.18.49) it reports: +The table is the validation as first run, under the stock emulator, whose +executable digest is `a26dcb...`; the pinned emulator that carries the patch is +validated again in the resolution section, with its own digest, and the guest +measurements are the same under both: [cols="1,2",options="header"] |=== @@ -303,7 +307,9 @@ kernel, and the same probe build: | yes |=== -The command that reproduces each row is: +The command that reproduced each row at the time is below; the table is +historical, so reproducing it needs the stock emulator rather than the patched +build the dev shell now supplies: [source,shell] ---- @@ -313,11 +319,11 @@ The command that reproduces each row is: .agents/dev ./scripts/time-model-input-probe.sh shift=auto # the product's current model ---- -Continuous integration asserts the known result rather than leaving it to be -re-checked by hand: the `qemu-replay` job runs -`scripts/time-model-input-probe.sh --expect not-delivered`, so a QEMU change -that starts delivering input under the pinned model fails the job with the -reading that the pin may be unblocked. The same job runs the guest's own +Continuous integration asserts the pinned emulator's behaviour rather than +leaving it to be re-checked by hand: the `qemu-replay` job runs +`scripts/time-model-input-probe.sh --expect delivered`, so dropping or breaking +the carried patch fails the job rather than silently returning the model to the +state the blocker section above describes. The same job runs the guest's own regression tests (`scripts/time-model-input-probe-test.sh`), which drive the real polling loop through stubs: a line that arrives after more than the polls the guest used to stop after is still reported as received, and the guest never @@ -364,7 +370,43 @@ how input reaches the guest. Until that is decided, the product's launch paths keep launching the model they can complete under, which `crates/simferret/src/vm.rs` names `TIME_MODEL` and documents there. -The decision this leaves to the RFD is not a scale choice. It is one of: keep a +==== Resolution: the pinned emulator carries the fix + +The owner approved carrying the patch, so the block above is history rather than +the current state. `flake.nix` applies `poc/time-model/qemu-nosleep-replay-flush.patch` +to the pinned emulator, so the dev shell's `qemu-system-x86_64` is the patched +build and the patch is a build input rather than a spike; the spike expression +that first built it is gone, and the repository does not keep two ways to build +the same emulator. The product's `TIME_MODEL` is `shift=4,sleep=off`, and the +reference record is re-measured against the patched binary, so `qemu_sha256` is +the patched digest and VM identity records the emulator that carries the fix. The +patch is carried until upstream includes it; dropping it is a versioned change, +because recordings made under a different emulator are not comparable. +`README.adoc` records the same decision from the RFD's side. + +The pin is verified the way the checklist asks. The launch paths are asserted by +`record_and_replay_launch_the_configured_time_model`, which fails unless both +record and replay launch the pinned options; the pinned model was re-measured +against the patched emulator and reproduced every reference value, so the patch +does not move the guest's measurements; the host-input assertion in continuous +integration now expects delivery, which keeps the patch load-bearing; and +`scripts/phase4-acceptance.sh` records a run under the pinned model and replays +it twice with matching normalized events, assertions, and semantic outcome +digest. One observation moves with the patch and is not a guest measurement: the +replay log grows, from the 330,255 bytes the stock emulator recorded to +3,386,955 bytes in every run of the pinned build, which is replay bookkeeping the restored flush adds rather than a guest measurement. Run wall +time is 6.89-7.17 s per run against 6.6-6.8 s before, inside the same deadline. + +The pin has a cost, because the patched emulator is not in `cache.nixos.org`. +The unrestricted spike build took 16 minutes end to end on the reference host +(configure 3m16s, build 11m28s, fixup 1m02s); the pinned build restricts QEMU to +the x86-64 system target and drops its documentation, which brought it under +about eight minutes here, and the dev shell split keeps it to the single job that +runs QEMU: the format, lint, and test jobs use `devShells.light`, which has no +emulator, so they do not build the patched emulator. + +The decision this left to the RFD was not a scale choice, and the owner has +since taken it: the project carries the patch. The alternatives considered were: keep a fixed scale and drop `sleep=off`, which reintroduces the residual host-coupled variation the scale-selection section measured; fix QEMU's no-sleep replay path so queued asynchronous events are flushed at a deterministic point; or deliver @@ -405,10 +447,10 @@ as the ordering it is measured under. === Feasibility spike: a patched QEMU that flushes the queue The blocker above is one early return. `poc/time-model/qemu-nosleep-replay-flush.patch` -moves the flush before it, and `poc/time-model/qemu-nosleep-replay-spike.nix` -builds the pinned QEMU with that patch applied. Neither file is wired into -`flake.nix` or into any build the repository performs; they answer whether a -minimal fix is possible, and carrying a patched QEMU remains a project decision. +moves the flush before it; the spike expression that first built the pinned QEMU +with it is gone, and `flake.nix` is what applies it now. The patch is wired into `flake.nix`, which applies it to the pinned emulator; +the spike expression that first built it is gone, and the measurements below +answer whether a minimal fix is possible. [source,diff] ---- @@ -444,21 +486,20 @@ minimal fix is possible, and carrying a patched QEMU remains a project decision. } ---- -The build is: +The build is the flake's pinned emulator, which applies this patch: [source,shell] ---- -rev=$(jq -r '.nodes.nixpkgs.locked.rev' flake.lock) -nix build --impure --no-link --print-out-paths --expr " - let pkgs = import (builtins.fetchTarball - \"https://github.com/NixOS/nixpkgs/archive/$rev.tar.gz\") {}; - in import ./poc/time-model/qemu-nosleep-replay-spike.nix { inherit pkgs; }" +nix develop --command qemu-system-x86_64 --version ---- -It produced `/nix/store/6a4jza3ydplb1k9l4kbk9gcsqy2iaka8-qemu-11.1.0`, whose -`qemu-system-x86_64` reports the same `QEMU emulator version 11.1.0` and hashes -to `f9c26bcdcd50ecbbccd923840bbcfaede6fddc675764cd99c93e12e97779faa8`. Every -measurement below used that binary through `QEMU_SYSTEM_X86_64`, and the +That spike produced `/nix/store/6a4jza3ydplb1k9l4kbk9gcsqy2iaka8-qemu-11.1.0`, +whose `qemu-system-x86_64` reports the same `QEMU emulator version 11.1.0` and +hashes to `f9c26bcdcd50ecbbccd923840bbcfaede6fddc675764cd99c93e12e97779faa8`; +that build is historical now, and the current one is the flake's pinned emulator, +whose executable hashes to +`2c6adfd8fc39a972ebb6051f7105f10f11cc2715efd1ff0b4e3385809a0fae0d`. Every +measurement below used the spike binary through `QEMU_SYSTEM_X86_64`, and the reference column is the pinned stock QEMU. Because the repository's own revision does not launch the pinned model, the @@ -540,14 +581,7 @@ test covering input under `sleep=off`; the measurements above are the basis for one. Recommendation: the fix looks minimal, effective, and upstreamable, and it does -not perturb the model's measurements. Whether to carry a patched QEMU until an -upstream release includes it is the project's decision; this increment does not -do that, and the product keeps the model it can complete under. - -=== Re-validation trigger - -The model, the reference values, and the input result are valid only for the -pinned QEMU and guest kernel they were measured with. Re-validate when any of +not perturb the model's measurements. The project carries it now: `flake.nix` applies the patch to the pinned emulator and the product launches the pinned model, and the patch is dropped when upstream includes the fix. Re-validate when any of these changes: * the pinned QEMU or guest kernel, in practice a `flake.lock` update that moves @@ -562,15 +596,10 @@ these changes: Re-validation means running `scripts/time-model-gate.sh` on every host in the supported matrix and `scripts/time-model-input-probe.sh` for the models in the table above, then re-recording the results here and the values in the reference -record. The continuous-integration gate re-validates the `ubuntu-latest` host on -every pull request, so that half does not depend on someone remembering, and the -same job asserts the known host-input result, so a QEMU change that fixes the -no-sleep flush fails the job instead of waiting to be noticed. - -If the input probe starts delivering input under the pinned model with -record/replay enabled, the observed input blocker may be resolved — but that -alone does not unblock the pin. Re-run the pinned product record and two-replay -acceptance (`scripts/phase4-acceptance.sh` with a build that launches the pinned -model) and the supported-host validation before adopting it; the probe's recorded -result is what makes the change visible rather than a silent difference in -behavior. +record.the same job asserts the pinned emulator's delivery, so a change that breaks or drops the carried patch fails the job instead of waiting to be noticed. + +The pin adopted that resolution: the pinned emulator carries the patch, the +product launches the pinned model, and the record and two-replay acceptance runs +under it. A change to the patch, the model, or the pinned emulator is the +re-validation trigger above, and the probe's assertion is what makes a silent +difference visible. diff --git a/rfd/0004/IMPLEMENTATION.org b/rfd/0004/IMPLEMENTATION.org index 2d4ac34..2ccfaec 100644 --- a/rfd/0004/IMPLEMENTATION.org +++ b/rfd/0004/IMPLEMENTATION.org @@ -26,19 +26,18 @@ earlier increments did and did not establish about fresh executions. * Phase 0 — Pin and validate the time model :PROPERTIES: -:STATUS: TODO +:STATUS: DONE :END: The scale is selected and the model is validated on every host in the supported -matrix. Two items cannot be completed under the model as recorded: with -~sleep=off~ and ~rr=record~, QEMU queues live serial input as a replay -asynchronous event and never flushes the queue, so a guest that waits for the -control commands a recording is driven with never receives them. The -measurement, the minimal reproduction, and the source-level cause are in -~EVIDENCE.adoc~; the model question is the RFD's to settle. The same document -records a patched-QEMU spike showing the limitation is one early return and that -the pin completes under that patch, so unblocking is a decision about the model -or about carrying a patch rather than an open technical question. +matrix. The model is pinned and the product launches it. With ~sleep=off~ alone, +QEMU queues live serial input as a replay asynchronous event and never flushes +the queue, so a guest that waits for the control commands a recording is driven +with never receives them; the owner approved carrying +~poc/time-model/qemu-nosleep-replay-flush.patch~, ~flake.nix~ applies it to the +pinned emulator, and the two items below that depended on it are complete. The +measurement, the reproduction, the source-level cause, and the resolution are in +~EVIDENCE.adoc~. - [X] Select the pinned scale by the documented rule and record why that value was chosen over its neighbors. @@ -54,19 +53,22 @@ or about carrying a patch rather than an open technical question. identical artifacts and the same model, not merely stable within each host. The gate compares each host against the checked-in reference values and the artifact identities, and the two hosts agree on every measurement exactly. -- [ ] Pass the pinned model from the product's record and replay launch paths - rather than configuring it separately, and assert that the launched options - match the recorded identity. - Blocked by the record-mode limitation above. The launch paths read the single - ~TIME_MODEL~ constant in ~crates/simferret/src/vm.rs~, which records why it - cannot be pinned yet, and the launched argument vector is asserted by - ~record_and_replay_launch_the_configured_time_model~. -- [ ] Confirm that a run recorded under the pinned model replays twice with +- [X] Pass the pinned model from the product's record and replay launch paths + rather than configuring it separately, and assert that both launched argument + vectors use the pinned constant. + Both launch paths read the single ~TIME_MODEL~ constant in + ~crates/simferret/src/vm.rs~, which is now ~shift=4,sleep=off~ and records why + the pinned emulator carries the patch; the launched argument vector is asserted + by ~record_and_replay_launch_the_configured_time_model~. The second half of the + item — a typed time-model field in VM identity, with the launched options + compared against it — is Phase 1's, where ~VmIdentity~ gains the field; today + the assertion binds the launch paths to the constant, and the executable digest + in identity is what rejects a recording made by another emulator. +- [X] Confirm that a run recorded under the pinned model replays twice with matching normalized events, assertions, and semantic outcome digest. - Blocked by the same limitation: no recording can be made under the pinned - model, so the two replays it would compare do not exist. ~scripts/phase4-acceptance.sh~ - performs the record and two-replay comparison and fails while writing the - first command. + ~scripts/phase4-acceptance.sh~ performs the record and two-replay comparison + under the pinned model with the pinned emulator, and it passes: the two replays + match on normalized events, assertions, and the semantic outcome digest. - [X] Fail a host that cannot complete under the pinned model as an unsupported execution rather than falling back to another model. - [X] Run the time-model probe as a continuous-integration gate on x86-64 diff --git a/rfd/0004/README.adoc b/rfd/0004/README.adoc index 44aabd0..8085a6e 100644 --- a/rfd/0004/README.adoc +++ b/rfd/0004/README.adoc @@ -106,6 +106,16 @@ duration vary across recordings and hosts, and that those are measurements rather than replay identity. That scoping is correct and this increment leaves it intact. +The pinned emulator carries a patch. `sleep=off` needs the replay flush that +QEMU performs only from `icount_account_warp_timer()`, which returns before it +when `sleep=off`, so a recording cannot be driven under the pinned model on an +unpatched build. `poc/time-model/qemu-nosleep-replay-flush.patch` moves the flush +ahead of the sleep check, `flake.nix` applies it to the pinned emulator, and the +patch is carried until upstream includes it; dropping it is a versioned change, +because recordings made under a different emulator are not comparable. The +evidence document records the measurement, the patch, and the spike that +established it. + Replay is also not unconditional. A successful replay requires valid recorded artifacts and digests, a matching scenario and materialized choice plan, a matching SimFerret executable and rebuilt initial state, the specific verified diff --git a/scripts/time-model-gate-test.sh b/scripts/time-model-gate-test.sh index 362654d..adb9d0d 100755 --- a/scripts/time-model-gate-test.sh +++ b/scripts/time-model-gate-test.sh @@ -654,8 +654,9 @@ input_evidence_value() { # $1 case name, $2 key # warmed here rather than inside a case with a short wait. python3 -c 'pass' -# The known result: the pinned model records but never delivers host input, and -# the guest polls for the whole wait. +# A recording under the pinned model that never delivers host input: the state +# the stock emulator produces, and the case the probe has to classify without +# help from the emulator's own behaviour. run_input_probe input-known-result not-delivered SIMFERRET_INPUT_PROBE_WAIT=6 test "$(input_evidence_value input-known-result input)" = "not-delivered" test "$(input_evidence_value input-known-result polls_reported)" = "true" @@ -755,8 +756,8 @@ fi test "$(input_evidence_value input-flood-then-stop input)" = "inconclusive" grep -Fq 'did not go quiet' "$test_root/input-flood-then-stop.stderr" -# If input starts being delivered under the pinned model, the assertion fails -# with the reading that matters: the pin may be unblocked. +# The pinned emulator delivers input, so an assertion that expects non-delivery +# fails on delivery, which is what an expectation mismatch means here. if run_input_probe input-unblocked not-delivered FAKE_QEMU_INPUT_BEHAVIOR=delivered; then echo "delivered input did not fail the known-result assertion" >&2 exit 1 diff --git a/scripts/time-model-gate.sh b/scripts/time-model-gate.sh index 9e8b757..0e01744 100755 --- a/scripts/time-model-gate.sh +++ b/scripts/time-model-gate.sh @@ -15,10 +15,11 @@ set -euo pipefail # record, `poc/time-model/reference-host-values.txt`, so the gate cannot # validate a model whose values were measured under another one. The record # names the model, the SHA-256 of the QEMU executable, the guest kernel, and the -# initramfs, and one measurement per metric. The product does not launch this -# model yet: `TIME_MODEL` in `crates/simferret/src/vm.rs` documents why pinning -# it is blocked on the model decision, and `rfd/0004/EVIDENCE.adoc` records the -# measurement and the minimal reproduction. +# initramfs, and one measurement per metric. The product launches this model: +# `TIME_MODEL` in `crates/simferret/src/vm.rs` pins it, and the pinned emulator the +# flake builds carries the replay-flush patch `sleep=off` needs, so a recording can +# be driven. `rfd/0004/EVIDENCE.adoc` records the measurement, the pin, and the +# resolution. # # The deadline and the sample size are the RFD's acceptance rule, not the # caller's, so both are fixed here: the probe keeps its own overridable deadline diff --git a/scripts/time-model-input-probe.sh b/scripts/time-model-input-probe.sh index f0c4f8f..fe9c9ad 100755 --- a/scripts/time-model-input-probe.sh +++ b/scripts/time-model-input-probe.sh @@ -29,13 +29,13 @@ set -euo pipefail # old progress from new; the ordering does not rest on it, because the monitor # does. # -# The pinned model fails this probe when record/replay is enabled: with -# `rr=record`, QEMU queues live serial input as a replay asynchronous event and -# delivers it only from `icount_account_warp_timer()`, which returns before -# `replay_async_events()` when `sleep=off`. `rfd/0004/EVIDENCE.adoc` records the -# measurement, the models that were compared, and what it blocks. The probe is -# retained so the limitation can be re-checked whenever the pinned QEMU or guest -# kernel changes. +# The pinned emulator carries the patch that makes this probe deliver: with +# `rr=record` and `sleep=off` alone, QEMU queues live serial input as a replay +# asynchronous event and delivers it only from `icount_account_warp_timer()`, +# which returns before `replay_async_events()`; the patch moves the flush ahead +# of the sleep check. The assertion the continuous-integration job runs expects +# delivery, and the probe is retained so the behaviour can be re-checked whenever +# the pinned QEMU or guest kernel changes. # # usage: time-model-input-probe.sh [--live] [--expect delivered|not-delivered] [icount-options] # --live do not enable record/replay (the control that isolates