VmResult

Struct VmResult 

Source
pub struct VmResult {
Show 26 fields pub success: bool, pub vcpus: u32, pub cpu_budget: u32, pub resolve_source: Option<String>, pub expect_auto_repro_satisfied: bool, pub exit_code: i32, pub duration: Duration, pub timed_out: bool, pub output: String, pub stderr: String, pub monitor: Option<MonitorReport>, pub guest_messages: Option<BulkDrainResult>, pub verifier_stats: Vec<ProgVerifierStats>, pub kvm_stats: Option<KvmStatsTotals>, pub crash_message: Option<String>, pub cleanup_duration: Option<Duration>, pub virtio_blk_counters: Option<VirtioBlkCountersSnapshot>, pub virtio_net_counters: Option<VirtioNetCountersSnapshot>, pub snapshot_bridge: SnapshotBridge, pub stats_client: Option<SchedStatsClient>, pub periodic_fired: u32, pub periodic_real: u32, pub periodic_target: u32, pub kern_kaslr_offset: u64, pub entry_name: Option<&'static str>, pub variant_hash: u64, /* private fields */
}
Expand description

Result of a VM execution.

Clone is supported, but two field categories have different Clone semantics that callers must understand:

  1. Pure-data fields (the bulk of the struct): primitives, String, Vec, Option<_>, plus MonitorReport / BulkDrainResult / ProgVerifierStats / StimulusEvent / KvmStatsTotals / VirtioBlkCountersSnapshot / VirtioNetCountersSnapshot. Every clone produces an independent value — mutations to one do not affect the other. The virtio_blk_counters / virtio_net_counters fields are materialized *CountersSnapshot types (atomic loads done at construction time inside super::KtstrVm::collect_results), so clones cannot alias live device state.

  2. Arc-shared handles (snapshot_bridge, stats_client): these wrap Arc<Mutex<…>> / Arc<AtomicUsize> and clone via shallow refcount bump. Two VmResult clones SHARE the underlying store — calling snapshot_bridge.drain() on one clone empties the data visible to the other. See each field’s own doc for the precise drain / iteration contract. If you need an independent snapshot view, drain into a local Vec before cloning the VmResult.

  3. The capture-series cache (periodic_series_cache): a OnceLock<SampleSeries> that memoizes the one destructive drain of the category-2 snapshot_bridge (see Self::captures_series). Its clone behavior depends on whether it is populated at clone time:

    • Populated (any of Self::captures_series / Self::periodic_series / Self::phase_buckets was already called): the clone carries an INDEPENDENT copy of the cached series — category-1 semantics. Both clones return the same captures without touching the (now-drained) shared bridge.
    • Empty: the clone shares the category-2 bridge, so the FIRST captures_series() call on EITHER clone performs the single drain and the other clone — if it later drains the same shared bridge — sees nothing. To give each clone its own buckets, call Self::captures_series (or any accessor that routes through it) on the original BEFORE cloning.

Fields§

§success: bool

Overall success flag: true when the test reported a pass AND the VM exited cleanly without crash, timeout, or watchdog.

§vcpus: u32

Guest vCPU count for this run (topology llcscoresthreads), carried from KtstrVm to the sidecar (vcpus, cpu_budget) stamp.

§cpu_budget: u32

Effective host-CPU budget the vCPU threads ran on; stamped to the sidecar. Normally KtstrVm::effective_cpu_budget (the build-time plan’s CPU count), but the default-overcommit path (host too small for a 1:1 pin) overrides it in run() with the actual masked host-CPU count, since the build-time value assumes 1:1 pinning. Below vcpus means host overcommit, which confounds the guest-scheduler timing metrics.

§resolve_source: Option<String>

How the userspace scheduler binary was resolved for this run — the snake_case crate::test_support::ResolveSource::as_str tag ("auto_built", "target_debug", "path", …). Stamped by the host eval layer (run_ktstr_test_inner_impl) AFTER the run, alongside entry_name / variant_hash, then carried to the sidecar resolve_source stamp the same way vcpus / cpu_budget are. None for VmResults built outside the host eval path (the freeze coordinator, test fixtures) — those resolve no scheduler binary.

§expect_auto_repro_satisfied: bool

True when the #[ktstr_test(expect_auto_repro)] attribute set expect_auto_repro = true on the entry AND the auto-repro path fired with a valid repro artifact during the run — the signal that the verdict-flip from fail-with-artifact → PASS is satisfied.

The eval-layer derives this field AFTER evaluate_vm_result returns (preserving the original success + error chain for diagnostic visibility); the eval layer then wraps any failure Err with the crate::test_support::eval::ExpectAutoReproSatisfied marker, and the dispatch arm (crate::test_support::dispatch::result_to_exit_code) downcasts the marker and routes the verdict to EXIT_PASS without mutating the original success or stripping the error chain. Pattern mirrors the expect_err matcher inversion.

Default false. When expect_auto_repro = false (the macro-attribute default) the eval layer skips the artifact probe entirely and leaves the field at false, so the dispatch arm is never matched and the original verdict stands.

§exit_code: i32

Guest exit code as surfaced through the SHM ring (MSG_TYPE_EXIT) or COM2 sentinel.

§duration: Duration

Wall-clock duration of the VM run.

§timed_out: bool

True when the host hit its watchdog before the guest exited.

§output: String

Captured guest stdout (and any non-dmesg serial console content).

§stderr: String

Captured guest stderr (separated from output when the guest reported them distinctly).

§monitor: Option<MonitorReport>

Host-side monitor report: sampled per-CPU state, stuck verdicts, and SCX event deltas. None when the monitor did not run (host-only tests, early VM failure).

§guest_messages: Option<BulkDrainResult>

TLV messages drained from the guest after VM exit. Merges mid-flight bytes the freeze coordinator pulled off virtio-console port 1 during the run with the final port-1 port1_tx_buf flush.

§verifier_stats: Vec<ProgVerifierStats>

BPF verifier stats collected from host-side memory reads.

§kvm_stats: Option<KvmStatsTotals>

KVM per-vCPU cumulative stats (requires Linux >= 5.14).

§crash_message: Option<String>

Crash message extracted from COM2 output via crate::test_support::extract_panic_message. The guest panic hook in rust_init/init.rs writes PANIC: <info>\n<bt>\n to /dev/ttyS1 synchronously inside KVM_RUN, so the host captures the full backtrace in output even when the guest is wedged. None when no PANIC:-prefixed line was seen.

§cleanup_duration: Option<Duration>

Wall-clock time from BSP exit to the moment super::KtstrVm::collect_results finishes assembling VmResult. Records the host-side cost of every teardown step that runs after the guest has stopped advancing: watchdog join, AP joins, monitor join, BPF-writer join, SHM drain, exit/crash-message extraction, and BPF verifier-stat read. Always Some(_) for VMs whose super::KtstrVm::run_vm returns normally — including the host-watchdog timeout path, because run_bsp_loop exits cleanly with timed_out = true and collect_results still executes, populating the field. None only when run_vm does not complete (a BSP panic propagated through ?, or any pre-BSP setup error that returns an Err before VmRunState is constructed) and on the test_fixture / skip-sidecar paths that never boot a VM. Persisted via SidecarResult so stats tooling can flag cleanup regressions across runs.

§virtio_blk_counters: Option<VirtioBlkCountersSnapshot>

Host-side virtio-blk device counters, snapshotted after the guest has exited. Some(_) when the builder attached a disk via super::KtstrVmBuilder::disk; None when no disk was configured and super::KtstrVm::init_virtio_blk returned None. The device increments its internal AtomicU64 counters from drain_bracket_impl (production cfg: on the dedicated ktstr-vblk worker thread; cfg(test): inline on the test thread); by the time collect_results constructs the VmResult every vCPU and the worker have joined and no further mutation can occur. The snapshot is taken at that point — readers see plain u64 fields holding the final cumulative totals; no atomic load is needed on the consumer side.

The counter struct exposes nine AtomicU64 fields, each bumped from drain_bracket_impl (in src/vmm/virtio_blk/drain.rs) via the VirtioBlkCounters::record_* helpers (defined in src/vmm/virtio_blk/counters.rs). Per-request cumulative counters, per-event cumulative counters, and per-request live gauges are kept distinct per the counter-taxonomy doc on VirtioBlkCounters:

  • reads_completed — count of VIRTIO_BLK_T_IN requests that returned S_OK to the guest. Bumped together with bytes_read per VirtioBlkCounters::record_read.
  • writes_completed — count of VIRTIO_BLK_T_OUT requests that returned S_OK. Bumped together with bytes_written.
  • flushes_completed — count of VIRTIO_BLK_T_FLUSH requests that returned S_OK (real fdatasync for read-write disks, no-op for read_only).
  • bytes_read — total bytes returned to the guest for completed reads.
  • bytes_written — total bytes accepted from the guest for completed writes.
  • throttled_count — cumulative token-bucket stall events for the device’s lifetime. The chain is rolled back and the worker arms a retry timerfd; the guest does not see S_IOERR for a stall (the request is deferred until the bucket refills). This counter is separate from io_errors so operators can distinguish “throttle bucket drained, request deferred” from “real IO problem”. Per-event (NOT per-request): a single chain that stalls twice produces two bumps.
  • io_errors — every path that reports S_IOERR: spec violations, backend pread/pwrite errors, malformed chains, add_used failures. Stalls do not report S_IOERR; see throttled_count.
  • currently_throttled_gaugelive gauge: how many requests are RIGHT NOW waiting for throttle tokens. Increments when a chain transitions into stalled, decrements on retry success or reset. Bounded at 0 or 1 on this single-queue device. NOT cumulative — answers “what’s stuck now,” distinct from throttled_count which answers “how many stall events happened over time.”
  • invalid_avail_idx_count — cumulative count of Error::InvalidAvailRingIndex events observed by drain_bracket_impl (avail.idx more than queue.size ahead of next_avail — a virtio-v1.2 §2.7.13.3 avail.idx-distance violation by the guest). Per-event counter; the queue_poisoned flag short-circuits subsequent kicks so one guest fault produces exactly one bump regardless of how many notifications follow before reset.

Counters are cumulative for the device’s lifetime. A guest driver re-bind (writing STATUS=0 to VIRTIO_MMIO_STATUS triggers VirtioBlk::reset) does NOT zero them — the device’s internal AtomicU64 storage persists across reset cycles, and the post-exit snapshot captures the final cumulative totals spanning the entire device lifetime, not just a post-reset fragment.

Reading example:

let r: VmResult = builder.run()?;
let c = r.virtio_blk_counters.expect("disk attached");
assert!(c.reads_completed > 0);

#[allow(dead_code)]: the field is part of the public API surface and read by user test code outside lib.rs, but the lib build doesn’t see any in-tree readers because no lib code path calls .virtio_blk_counters on a VmResult. The in-tree readers live in unit tests.

§virtio_net_counters: Option<VirtioNetCountersSnapshot>

Host-side virtio-net device counters, snapshotted after the guest has exited — the cross-NIC AGGREGATE (field-wise saturating sum via VirtioNetCountersSnapshot::aggregate) over every attached NIC. Some(_) when the builder attached one or more networks via super::KtstrVmBuilder::network; None when no network was configured (the aggregate over an empty NIC set). Each NIC’s device increments its own AtomicU64 counters on the vCPU thread inside process_tx_loopback; by the time collect_results constructs the VmResult every vCPU has joined and no further mutation can occur. The per-NIC snapshots are summed at that point — readers see plain u64 fields holding the final cumulative totals across all NICs; no atomic load is needed on the consumer side. Per-NIC IRQ-delivery observability comes from the per-CPU / per-IRQ metrics axis, not these device-internal loopback counters.

The counter struct exposes sixteen AtomicU64 fields, each bumped across the TX-drain path rooted at process_tx_loopback (several are bumped inside the pop_and_capture_tx / try_loopback_to_rx helpers it calls; the three ctrl_* counters are bumped on the control-vq path, not the TX-drain path):

  • tx_packets — count of TX chains whose L2 frame was captured (frame_len = Some) AND whose TX add_used succeeded. Over-size-dropped and malformed chains are still marked used (so the guest doesn’t hang) but do NOT advance tx_packets; a chain whose add_used fails advances tx_add_used_failures instead. So tx_packets advances per successfully-captured-and-published chain, not per parsed chain.
  • tx_bytes — bytes of L2 frame data captured from successfully parsed TX chains (excludes the 12-byte virtio header).
  • rx_packets / rx_bytes — count + bytes of RX chains successfully written and marked used. rx_packets and tx_packets gate INDEPENDENTLY per chain: rx_packets bumps when the loopback delivers — recorded BEFORE the TX add_used — while tx_packets bumps only when that later TX add_used succeeds. So the identity `rx_packets == tx_packets - tx_dropped_no_rx_buffer
    • tx_dropped_rx_poisonedholds ONLY when both the RX-side failure counters ANDtx_add_used_failures are zero: RX-side failures (rx_add_used_failures, rx_chain_invalid, rx_write_failed) make rx_packetsfall SHORT oftx_packets - drops, while a tx_add_used_failureson a chain whose RX already delivered makesrx_packets` EXCEED it. Asymmetric counts surface queue-state breakage on either side.
  • tx_dropped_no_rx_buffer — successfully-captured TX frames the device could not deliver because the RX queue was empty (transient back-pressure event).
  • tx_dropped_rx_poisoned — successfully-captured TX frames dropped because the RX queue was poisoned by a prior guest avail-ring violation (wedged until a virtio reset), as opposed to the transient empty-queue back-pressure counted by tx_dropped_no_rx_buffer.
  • tx_chain_invalid / rx_chain_invalid — chains rejected for malformed shape (short header, wrong direction, attacker-controlled descriptor address overflow).
  • tx_oversize_dropped — TX chains dropped (not truncated) because the captured post-header frame data exceeded the maximum L2 frame size the guest’s max_mtu permits.
  • rx_write_failed — RX chain whose shape was valid but whose guest-memory write_slice (header or frame) hit an unmapped GPA. Distinct from rx_chain_invalid so an operator can tell “guest violated the RX descriptor- direction rule” from “guest posted a buffer at an unmapped GPA”; the two are mutually exclusive per chain.
  • tx_add_used_failures / rx_add_used_failuresadd_used failures, indicating the queue’s used-ring address itself is unmapped or otherwise inaccessible. Distinct from the *_chain_invalid / rx_write_failed counters so an operator can tell “guest sent malformed frame” / “guest’s posted buffer GPA was unmapped” from “queue itself is broken”.
  • invalid_avail_idx_count — cumulative count of Error::InvalidAvailRingIndex events observed by process_tx_loopback (avail.idx more than queue.size ahead of next_avail — virtio-v1.2 §2.7.13.3 violation by the guest). Per-event counter; the per-queue queue_poisoned flag short-circuits subsequent kicks so one guest fault produces exactly one bump regardless of how many notifications follow before reset.
  • ctrl_mq_set — cumulative count of successful control-vq VIRTIO_NET_CTRL_MQ / VQ_PAIRS_SET commands (the device updated curr_queue_pairs and wrote VIRTIO_NET_OK). Per-event counter.
  • ctrl_chain_invalid — cumulative count of control-vq chains the device could not satisfy: malformed shape (no device-writable status descriptor, too few readable command bytes), an unknown (class, cmd), or a virtqueue_pairs outside [1, queue_pairs]. Per-event hostile/buggy-guest counter; the control-vq analog of tx_chain_invalid.
  • ctrl_add_used_failures — cumulative count of control-vq status-write or add_used failures (the status byte’s GPA or the used-ring address is unmapped). Queue-state breakage distinct from ctrl_chain_invalid; the control-vq analog of tx_add_used_failures.

Counters are cumulative for the device’s lifetime — a guest driver re-bind (writing STATUS=0) does NOT zero them.

§snapshot_bridge: SnapshotBridge

Snapshot bridge populated by the freeze coordinator over the run’s lifetime. Every Op::CaptureSnapshot and Op::WatchSnapshot fire stores a FailureDumpReport keyed by its tag.

#[ktstr_test] test bodies whose scenario fires snapshot ops in the guest assert on the captured reports through a post_vm = NAME attribute. The named callback runs on the HOST after vm.run() returns (see crate::test_support::KtstrTestEntry::post_vm) and receives &VmResult; it calls crate::scenario::snapshot::SnapshotBridge::drain on this field to take ownership of the stored reports and walks them — typically through crate::scenario::snapshot::Snapshot::new for typed access to map values, per-CPU entries, and scalar variables. Out-of-tree consumers can drain the bridge the same way: VmResult is in ktstr::prelude.

Always present after a successful run_vm; None-equivalent (empty) when the VM crashed before any snapshot fired.

Drained exactly once, via Self::captures_series: the bridge yields each capture once, so the host-side consumers share a single drain. The first call to Self::captures_series — whether from a post_vm callback (which runs FIRST, via Self::periodic_series / Self::phase_buckets) or from the framework’s evaluate_vm_result (which runs AFTER post_vm to build crate::assert::ScenarioStats::phases) — drains this bridge and memoizes the resulting crate::scenario::sample::SampleSeries on the periodic_series_cache field; every later call reads the cache. That is why a per-phase post_vm and the framework’s result.stats.phases build no longer starve each other (a post_vm that drained the bridge first used to leave stats.phases empty). Integration tests under tests/ that bypass the series accessors and call result.snapshot_bridge.drain*() directly (e.g. tests/stats_bridge_e2e.rs, tests/temporal_assertions_e2e.rs) are unaffected: the cache is only populated by Self::captures_series, which those tests never call, so the raw destructive drain still returns the full capture set.

§stats_client: Option<SchedStatsClient>

Live scheduler-stats client. Some(_) when the run wired the virtio-console port-2 stats bridge (the in-tree path always does so, but tests that construct a VmResult manually via Self::test_fixture leave this None). Test code that asserts on scheduler-reported metrics calls super::SchedStatsClient::stats / super::SchedStatsClient::stats_meta on this handle WHILE the guest is alive — calling after VM exit will time out because the relay thread has already exited. Cloneable; multiple test threads may share the same client.

§periodic_fired: u32

Number of periodic snapshot boundaries the freeze coordinator actually fired during this run. Includes both successful captures and rendezvous-timeout placeholders. Tests can assert result.periodic_fired >= some_lower_bound to guard periodic-capture coverage; mismatches against Self::periodic_target flag missing samples (early VM exit, kill-flag stop, abandoned-after-timeouts).

§periodic_real: u32

Periodic captures that landed REAL BPF state — the placeholder-excluded subset of Self::periodic_fired (periodic_fired counts rendezvous-timeout placeholders as fired). Snapshotted from crate::scenario::snapshot::SnapshotBridge::periodic_real_count at result-collection time so it is stable regardless of any later test-side drain of the bridge. periodic_real < periodic_fired means the gap is placeholder-only fills (the boundary fired but the dump was degraded); the failure-output periodic-samples section surfaces this so a “100% fired” run whose captures were all placeholders does not read as full coverage.

§periodic_target: u32

Configured num_snapshots count for the entry that drove this run (mirrors the KtstrTestEntry::num_snapshots field the entry was registered with). 0 when periodic capture was disabled. Pairs with Self::periodic_fired so a test can compute coverage without re-reading the entry table.

§kern_kaslr_offset: u64

Runtime virt-KASLR offset (kernel-image slide). Captured from the freeze coordinator’s kern_virt_kaslr Arc snapshot at run-end via load(Acquire).saturating_sub(1). 0 means either (a) KASLR was off — test ran with #[ktstr_test(kaslr = false)] or Scheduler::kargs(&["nokaslr"]), OR (b) the derivation chain (MSR_LSTAR readback in vmm::x86_64::msr_kaslr + KERN_ADDRS _text path in crate::vmm::freeze_coord::dispatch) never published a non-zero value (early-boot crash, kallsyms masked by kptr_restrict, FRED-enabled kernel). E2E test consumers distinguish (a) from (b) by reading the test entry’s kaslr attribute alongside this field — see Self::kaslr_enabled for the binary-question companion.

§entry_name: Option<&'static str>

Name of the #[ktstr_test] fn whose execution produced this result. Stamped from crate::test_support::entry::KtstrTestEntry::name (a &'static str the macro emits at compile time) in test_support::eval::run_ktstr_test_inner_impl immediately after super::KtstrVm::run returns and BEFORE the post_vm callback dispatch runs.

Some(_) for every result that flowed through the real run_ktstr_test_inner_impl path. None for the freeze_coord::collect_results direct-synthesis path (entry-agnostic boundary; entry is not in scope there) and for #[cfg(test)]-only Self::test_fixture callers. The path-derivation methods wprof_pb_path and repro_wprof_pb_path (require the wprof feature) bail with a loud diagnostic on None so any VmResult reaching the derivation path without going through the eval-layer stamping site surfaces the misuse rather than producing a garbage-named path.

Test authors writing post_vm callbacks should derive per-test sidecar paths via the helper methods rather than hardcoding a wprof_pb_path("<literal>") string against the fn name — a future rename of the test fn drifts the hardcoded literal silently, where the method-form derives from this field automatically.

§variant_hash: u64

The run’s variant hash (see variant_hash_from_parts), stamped alongside Self::entry_name after vm.run() returns. The post-VM failure_dump_path / wprof_pb_path derivations embed it as the -{16-hex} filename suffix so a gauntlet test’s per-preset dumps don’t clobber and each matches its sidecar’s variant hash. 0 on a synthesized/fixture result (which has entry_name = None and thus bails before reading this).

Implementations§

Source§

impl VmResult

Source

pub fn kaslr_enabled(&self) -> bool

Whether the guest kernel booted with KASLR enabled (= a non-zero virt-KASLR offset published into the freeze coordinator’s kern_virt_kaslr Arc). Returns true when Self::kern_kaslr_offset is non-zero. The inverse case (returns false) covers two scenarios: (a) the test explicitly opted out via #[ktstr_test(kaslr = false)] or Scheduler::kargs(&["nokaslr"]), OR (b) the derivation chain failed to publish a non-zero value (early-boot crash, kallsyms masked, kernel built without CONFIG_RANDOMIZE_BASE). E2E test consumers distinguish (a) from (b) by reading the test entry’s kaslr attribute alongside this method.

Companion to Self::kern_kaslr_offset — use this when the caller cares about the binary “did KASLR happen?” question and use the raw field for exact-offset assertions (alignment, entropy-range, etc.).

Source

pub fn captures_series(&self) -> &SampleSeries

The full capture series for this run — every snapshot the freeze coordinator stored on Self::snapshot_bridge (periodic boundaries AND on-demand Op::CaptureSnapshot / watchpoint-fire captures), in the order the bridge surfaced.

Performs the bridge’s single destructive drain on the first call and memoizes the resulting crate::scenario::sample::SampleSeries on the periodic_series_cache field; every later call — and Self::periodic_series / Self::phase_buckets and the framework’s evaluate_vm_result — returns the cached series without re-draining. This is what lets a post_vm callback and the framework’s crate::assert::ScenarioStats::phases build share one drain instead of starving each other (the bridge yields each capture exactly once).

Takes &self: the cache uses interior mutability (std::sync::OnceLock) so this composes with the #[ktstr_test(post_vm = ...)] callback signature (fn(&VmResult) -> Result<()>).

A consumer that calls snapshot_bridge.drain*() directly (e.g. integration tests under tests/) bypasses this cache. If such a raw drain runs BEFORE the first captures_series() call the cache memoizes an empty series, so prefer this accessor over a raw drain on any path that also reaches evaluate_vm_result.

Source

pub fn periodic_series(&self) -> SampleSeries

The periodic-capture-only view of this run’s series: the "periodic_"-tagged subset of Self::captures_series — the projection the temporal-assertion / per-phase patterns expect (on-demand Op::CaptureSnapshot and watchpoint-fire captures are filtered out as off-cadence outliers, see crate::scenario::sample::SampleSeries::periodic_only).

Reads the shared Self::captures_series cache (the single bridge drain) and returns an owned, periodic-only clone. Idempotent: calling it twice — or alongside Self::phase_buckets / evaluate_vm_result — no longer empties the bridge for the other consumers (the pre-cache behavior, which silently starved whichever drained second).

Takes &self so it composes with the #[ktstr_test(post_vm = ...)] callback signature.

Source

pub fn stimulus_timeline(&self) -> Vec<StimulusEvent>

The complete per-phase stimulus timeline for post_vm callbacks doing per-phase metric assertions: one crate::timeline::StimulusEvent per guest Stimulus frame (the step-start boundaries, via crate::timeline::StimulusEvent::from_wire) PLUS the synthesized terminal scenario-end boundary (from the ScenarioEnd frame’s final cumulative count, via crate::timeline::StimulusEvent::terminal).

Fold THIS through crate::assert::build_phase_buckets_with_stimulus — it is the SAME timeline the framework’s own evaluate_vm_result builds, so the LAST step gets an iteration_rate (the terminal supplies its right boundary). A hand-rolled map over only the guest Stimulus frames would omit the terminal and silently drop the final step’s rate.

Non-destructive: reads the already-drained guest_messages TLV log (unlike the bridge-cache accessors Self::captures_series / Self::periodic_series / Self::phase_buckets, which perform the single destructive snapshot-bridge drain), so it may be called alongside the bridge drain. CRC-bad / malformed frames are skipped.

Source

pub fn step_throughput(&self, phase: Phase) -> Option<f64>

Worker-iteration throughput (iterations/sec) for one scenario Phase, from the stimulus timeline’s StepStart[k] -> StepEnd[k] step-local window (the per-event rate via crate::timeline::StimulusEvent::rate_to).

None when the phase has no StepStart (crate::assert::Phase::BASELINE, or a step the run never reached), no right boundary (no StepEnd, no later step, and no scenario-end terminal), or the rate is unmeasurable (zero-length window / counter went backward). A step whose workers made zero forward progress over a positive hold returns Some(0.0) (measured zero), not None.

Collapse-immune: the stimulus timeline carries per-step boundaries independent of the periodic-capture pipeline, so this works even for --cell-parent-cgroup schedulers where the capture-derived PhaseBucket path can collapse.

Source

pub fn throughput_ratio(&self, a: Phase, b: Phase) -> Option<f64>

Ratio step_throughput(a) / step_throughput(b) — e.g. scheduler-vs-EEVDF throughput when phase b runs on the detached kernel default (crate::scenario::ops::Op::detach_scheduler). Walks the stimulus timeline once. None when either phase has no measurable throughput; a Some(0.0) denominator yields inf so a collapsed/stalled phase b surfaces rather than vanishing to None.

Source

pub fn guest_assert_result(&self) -> Result<AssertResult>

The guest-side crate::assert::AssertResult decoded from this run’s MSG_TYPE_TEST_RESULT frame — the verdict the in-VM scenario body produced, carrying the per-phase per-cgroup raw telemetry carriers in stats.phases[].per_cgroup (crate::assert::PhaseCgroupStats: total_migrations, run_delays_ns, cpus_used, …) and the per-cgroup reductions in stats.cgroups.

Non-destructive: reads the already-drained guest_messages TLV log (the last crc-ok MSG_TYPE_TEST_RESULT entry), so it composes with the snapshot-bridge accessors and may be called repeatedly. Shares the exact decode the eval layer uses (crate::test_support::parse_assert_result_from_drain).

Err when there is no guest verdict to decode — a host-only run, a crash before the body emitted its result, or a drain with no MSG_TYPE_TEST_RESULT frame.

This is the GUEST view: the run-level distribution / ext_metrics re-pools (worst_* wake-latency / run-delay aggregates, pooled iterations_per_cpu_sec) are applied HOST-side in evaluate_vm_result AFTER the body returns and are NOT on this value — only stats.phases[].per_cgroup and the per-cgroup stats.cgroups reductions are guest-authoritative. For the per-phase per-cgroup view aligned to the host capture windows use Self::phase_buckets (which folds these carriers in); for one cgroup in one phase use Self::phase_cgroup.

Source

pub fn phase_buckets(&self) -> Vec<PhaseBucket>

The framework-computed per-phase metric buckets for this run — the SAME crate::assert::PhaseBucket vec the framework folds onto crate::assert::ScenarioStats::phases in evaluate_vm_result, INCLUDING the per-phase per-cgroup carriers in crate::assert::PhaseBucket::per_cgroup.

This is the answer to “my post_vm callback wants the per-phase metrics the framework already built.” It folds the same two sources evaluate_vm_result does:

  1. the host-rebuilt buckets from Self::periodic_series (the periodic-only projection of the shared single drain — on-demand / watchpoint captures are off-cadence outliers excluded from per-phase folds) through crate::assert::build_phase_buckets_with_stimulus using Self::stimulus_timeline for the step windows (window + metric folds; per_cgroup empty by construction); and
  2. the guest per-cgroup carriers from Self::guest_assert_result (stats.phases[].per_cgroup), folded in by crate::assert::fold_guest_per_cgroup_into_host_buckets keyed by step_index (the host window + metrics win; each carrier contributes only its per_cgroup, an unmatched carrier surfacing as a (0,0)-window orphan bucket).

Production builds stats.phases from these same two sources (same periodic-only series, same stimulus timeline, same guest carriers) and the eval layer’s later populate_run_* passes write only stats.ext_metrics / stats.cgroups, never phases[].per_cgroup, so this returns content IDENTICAL to result.stats.phases — the no-carrier case pinned by phase_buckets_equals_stats_phases_and_post_vm_read_does_not_starve and the with-per-cgroup-carrier case by phase_buckets_equals_stats_phases_with_guest_per_cgroup_carriers.

Both sources are non-destructive on the snapshot bridge: Self::periodic_series reads the memoized single drain (Self::captures_series) and Self::guest_assert_result reads the already-drained guest_messages. A run with no guest verdict (Self::guest_assert_result Err — host-only / early crash) folds no carriers and returns the host-rebuilt buckets alone (the prior behavior).

Source

pub fn phase_cgroup( &self, phase: Phase, cgroup: &str, ) -> Option<PhaseCgroupStats>

One phase’s per-cgroup telemetry for cgroup — the per-phase analog of reading result.stats.cgroups for a single phase. Reads Self::phase_buckets (which folds the guest per-cgroup carriers against the host capture windows) and returns the crate::assert::PhaseCgroupStats keyed by cgroup in phase.

None when phase has no bucket (no capture landed in it AND no stimulus StepStart synthesized one) or the phase carries no carrier for cgroup (the cgroup ran no workers in that phase, or the run had no step-local cgroups). Owned (cloned from the folded bucket) so it composes with the &VmResult post_vm callback signature.

Source

pub fn phase_metric( &self, phase: Phase, metric: impl Into<MetricId>, ) -> Option<f64>

One framework-computed per-phase metric for phase — the metric-name analog of Self::step_throughput / Self::throughput_ratio. Resolves metric (any impl Into<MetricId> — a typed BuiltinMetric, typo-proof, or a dynamic scheduler-runtime string) from the folded Self::phase_buckets bucket for phase, checking two stores:

  1. crate::assert::PhaseBucket::metrics (via crate::assert::PhaseBucket::get) — the host-folded per-sample / monitor / stimulus metrics: per-phase CPU time (system_time_ns, user_time_ns), scheduling quality (avg_imbalance_ratio, avg_dsq_depth, …), and iteration_rate.
  2. failing that, the cross-cgroup phase sum of a per-cgroup Counter (crate::assert::PhaseBucket::cgroup_counter_total) for the keys whose value lives ONLY in the per-cgroup carriers — "total_migrations", "total_iterations", and "total_cpu_time_ns". These are registered Counters with no per-sample source (crate::stats::MetricDef::read_sample returns None), so they never reach metrics; without this fallback phase_metric(phase, "total_migrations") returned a silent None even though the value was present per-cgroup.

The wake-latency / run-delay distributions (MetricKind::Distribution) have no per-phase metrics value and no single per-cgroup Counter (they re-pool run-level from the per-cgroup raw sample vectors), so they are not readable here — read them per-cgroup via Self::phase_cgroup.

Reads the SAME buckets Self::phase_buckets folds onto result.stats.phases, so a post_vm callback can compare any metric across two phases (e.g. scheduler-vs-detached-EEVDF total_migrations or system_time_ns) without re-deriving the buckets — the general form of the scheduler-vs-EEVDF throughput compare.

None when phase has no bucket — no capture landed in it AND no stimulus StepStart synthesized one (e.g. Phase::BASELINE when the settle window fired no captures, or a phase past the last step) — or the bucket carries no reading for metric in either store. Sentinel-free, distinct from a real Some(0.0): a per-cgroup counter returns Some(0.0) when carriers exist but counted zero, None only when the phase has no carrier at all. A started-but-uncaptured step (a StepStart with zero captures) DOES produce a synthesized bucket, so phase_metric returns its stimulus-derived iteration_rate rather than None.

// Typed (typo-proof) is the primary form; a dynamic scheduler-runtime
// string is the escape hatch through the SAME call.
let p99 = result.phase_metric(Phase::step(0), BuiltinMetric::WakeupP99LatencyUs);
let custom = result.phase_metric(Phase::step(0), "scx_layered_layer0_util");
Source

pub fn run_metric(&self, metric: impl Into<MetricId>) -> Option<f64>

One run-level extensible (“ext”) metric by name — the whole-run analog of Self::phase_metric, for a post_vm callback asserting a run-level aggregate (e.g. avg_irq_util, max_cpu_hardirqs, worst_p99_wake_latency_us, iterations_per_cpu_sec). SELF-COMPUTES the run-level ext_metrics map exactly as the framework’s evaluate_vm_result does — a VmResult carries no stored run-level stats (post_vm runs BEFORE the host populates them) — by replaying the shared crate::assert::populate_run_ext_all sequence over Self::periodic_series, the pre-derive phase fold (phase_buckets_pre_derive), and the guest per-cgroup stats.cgroups. The result is byte-identical to the run-level ext_metrics the sidecar records for this run.

Resolves the ext-sourced family that crate::assert::ScenarioStats::run_metric (the post-merge host accessor) resolves — the read_sample-wired registry metrics, the phase-only ext metrics (avg_imbalance_ratio, iteration_rate, system_time_ns, user_time_ns, the IRQ counters/rates, the per-CPU spatial maxes max_cpu_hardirqs / max_cpu_softirq_net_rx and their concentrations), the pooled iterations_per_cpu_sec, and the run-level Distribution / WorstLowest / WakeLatencyTailRatio / WorstCrossNodeRatio re-pools — and for those keys the two accessors return identical values (this one self-computes pre-merge, the other reads the stored post-merge map).

ADDITIONALLY resolves the 5 ext-only run-level MONITOR metrics from Self::monitor’s summary: avg_nr_running, avg_irq_util, max_avg_irq_util, psi_irq_full_avg10, total_irq_pressure_us (folded via MonitorSummary::fold_run_level_ext, shared with the sidecar row). These DIVERGE from crate::assert::ScenarioStats::run_metric, which has no MonitorReport to fold and returns None for them — the one place the two accessors differ.

RESOLVED here None-aware (via the delegated ScenarioStats::run_metric typed dispatch): the cross-cgroup metrics worst_spread, worst_migration_ratio, worst_gap_ms, total_migrations, total_iterations, worst_page_locality, worst_cross_node_migration_ratio. The dispatch re-derives each from the carriers (the per-cgroup stats.cgroups + the per_cgroup-folded stats.phases this method builds) — None when no carrier measured it, Some(0.0) for a measured zero. The 5 non-NUMA metrics are 0.0-sentinel typed struct fields (re-derived because the field cannot carry the measured-vs-unmeasured distinction); the two NUMA roll-ups (worst_page_locality, worst_cross_node_migration_ratio) have no struct field and re-pool purely from the per-phase NUMA carriers.

NOT resolved here:

  • the typed-backed monitor run-level metrics (max_imbalance_ratio, max_dsq_depth, stuck_count, total_fallback, total_keep_last) — these have typed GauntletRow fields (not ext-only); read them per-phase via Self::phase_metric.

Sentinel-free, matching Self::phase_metric: None means the metric is absent from this run (no populator produced it, or a name not in the map); Some(0.0) is a real measured zero. Check crate::stats::MetricId::def on a dynamic key to distinguish an unregistered key from genuinely-absent data (built-in ids always resolve).

A host-only run (no guest verdict — Self::guest_assert_result Err) resolves the SampleSeries + phase families but no per-cgroup pooled / distribution keys (no per-cgroup data exists), the same absence crate::assert::ScenarioStats::run_metric documents.

Non-destructive: reads the memoized snapshot-bridge drain (Self::periodic_series / Self::phase_buckets) and the already-drained guest_messages, so it composes with Self::phase_metric in one post_vm callback.

let irq = result.run_metric(BuiltinMetric::AvgIrqUtil);
let custom = result.run_metric("scx_layered_layer0_util");
Source

pub fn better_across_phases<'v>( &self, verdict: &'v mut Verdict, baseline: Phase, candidate: Phase, metric: impl Into<MetricId>, ) -> BetterThanPhase<'v>

Polarity-aware “is the candidate phase better than the baseline phase on metric?” comparator — the per-phase A/B primitive for “assert scheduler X beats EEVDF across phases” (e.g. an scx Step vs the EEVDF Step after Op::DetachScheduler). Resolves both per-phase values via Self::phase_metric and the metric’s polarity via crate::stats::metric_def, then returns a crate::assert::temporal::BetterThanPhase builder whose terminal (better_than / by_at_least) records the outcome into verdict. “Better” is oriented from the registry polarity, so the SAME call works for a LowerBetter latency (BuiltinMetric::WakeupP99LatencyUs) and a HigherBetter throughput (BuiltinMetric::SchbenchLoopCount) with no caller-specified direction.

A post_vm callback collapses the verdict to its anyhow::Result via crate::assert::Verdict::into_anyhow_or_log, which bails on a Fail OR an Inconclusive — so a missing metric (a phase with no schbench carrier), an undirected metric, or a zero-baseline fractional-margin comparison does NOT silently pass.

Source

pub fn phase_cgroup_metric( &self, phase: Phase, cgroup: &str, metric: impl Into<MetricId>, ) -> Option<f64>

One per-phase, PER-CGROUP derived metric — the per-cgroup analog of Self::phase_metric, answering “metric M of cgroup C in phase P” as readily as the phase aggregate (the N-cgroups-to-N-queryable-sets goal). Resolves metric (any impl Into<MetricId> — a typed BuiltinMetric or a dynamic scheduler-runtime string) from this cgroup’s per-phase carrier via crate::assert::PhaseCgroupStats::get (its derived metrics map), falling back to crate::assert::PhaseCgroupStats::cgroup_counter for the per-cgroup Counters total_migrations/total_iterations/total_cpu_time_ns (carrier fields, not derived) — symmetric with Self::phase_metric’s cgroup_counter_total fallback. None when phase has no bucket, the bucket has no carrier for cgroup, or the carrier carried no finite value for the metric — sentinel-free, distinct from a real Some(0.0).

Source

pub fn better_across_phases_cgroup<'v>( &self, verdict: &'v mut Verdict, baseline: Phase, candidate: Phase, cgroup: &str, metric: impl Into<MetricId>, ) -> BetterThanPhase<'v>

Per-cgroup analog of Self::better_across_phases: “is metric of cgroup better in the candidate phase than the baseline phase?”, oriented from the registry polarity. Reuses the SAME crate::assert::temporal::BetterThanPhase comparator/verdict machinery; only value resolution is re-scoped from the pooled aggregate to the named cgroup, so a missing carrier / undirected metric / zero-baseline margin collapses to Inconclusive (not a silent pass), exactly as the pooled form.

Source

pub fn wprof_pb_path(&self) -> Result<PathBuf>

Per-test sidecar path for the .wprof.pb artifact: {sidecar_dir()}/{entry_name}-{variant_hash:016x}.wprof.pb.

Source

pub fn repro_wprof_pb_path(&self) -> Result<PathBuf>

Per-test sidecar path for the .repro.wprof.pb artifact.

Source

pub fn failure_dump_path(&self) -> Result<PathBuf>

Per-test failure-dump sidecar path. Derives {sidecar_dir()}/{entry_name}-{variant_hash:016x}.failure-dump.json from the macro-stamped Self::entry_name.

§Sibling to

crate::scenario::Ctx::failure_dump_path

The pre-VM body context carries its own copy of the macro-stamped entry name (stamped at Ctx construction by the dispatch path) and computes the same path string. A test body invocation ctx.failure_dump_path() and a post-VM result.failure_dump_path() resolve to identical paths because both stamp from the same entry.name: &'static str source — proc-macro emission at the #[ktstr_test] site. This pair gives post_vm callbacks a symmetric path-derivation surface to the pre-VM body, so a future post_vm hook that wants to inspect or clean up the failure dump uses the same method shape the body uses to look at it.

§Errors

Returns Err when Self::entry_name is None.

Source

pub fn guest_kmsg(&self) -> String

Concatenated guest /dev/kmsg content forwarded via crate::send_kmsg, or empty when no frames arrived (the scenario did not forward, or the VM exited before the forward completed). Lets a post_vm callback read the guest kernel log even at the default loglevel=0, where kernel printks never reach the COM1 console (and thus not stderr). Encapsulates the bulk-port ShmEntry + MsgType::Dmesg filter inside the crate.

Source

pub fn scheduler_log(&self) -> String

The scheduler’s captured log — its stderr, including any libbpf / BPF-verifier output — extracted from the bulk-port MSG_TYPE_SCHED_LOG frames in Self::guest_messages. Empty when no scheduler log was captured (no scheduler was spawned, or the framed markers never arrived).

Mirrors the extraction crate::verifier::collect_verifier_output performs: concatenate the SchedLog chunks, slice the span from the first SCHED_OUTPUT_START to the last SCHED_OUTPUT_END (spanning any intervening markers when the guest staged more than one log), and fall back to output when the bulk-port drain carried no SchedLog frames (a kernel without the bulk port). Lets a post_vm callback assert on what the scheduler logged — e.g. a libbpf verifier-reject — without reaching into the crate-internal wire types.

Source

pub fn watchdog_observation(&self) -> Option<WatchdogObservation>

The host watchdog-override readback (expected_jiffies = host-written, observed_jiffies = read back from guest memory). None when the scheduler never attached (no readback recorded). A post_vm callback asserts the two are equal to prove the override landed; the readback is taken eagerly by the monitor (in-DRAM, microseconds), so it is immune to the watchdog-kworker starvation that inflates the kernel-measured stall duration. Encapsulates the MonitorReport access (WatchdogObservation is re-exported at the crate root for the return type).

Source

pub fn assert_wprof_pb_landed(&self) -> Result<()>

Assert the primary-VM .wprof.pb landed and is shape-valid. Returns Ok(()) immediately when self.success is false.

Trait Implementations§

Source§

impl Clone for VmResult

Source§

fn clone(&self) -> VmResult

Returns a duplicate of the value. Read more
1.0.0 · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for VmResult

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> Pointable for T

§

const ALIGN: usize

The alignment of pointer.
§

type Init = T

The type for initializers.
§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

impl<T> MaybeSend for T
where T: Send,

§

impl<T> MaybeSend for T
where T: Send,