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:
-
Pure-data fields (the bulk of the struct): primitives,
String,Vec,Option<_>, plusMonitorReport/BulkDrainResult/ProgVerifierStats/StimulusEvent/KvmStatsTotals/VirtioBlkCountersSnapshot/VirtioNetCountersSnapshot. Every clone produces an independent value — mutations to one do not affect the other. Thevirtio_blk_counters/virtio_net_countersfields are materialized*CountersSnapshottypes (atomic loads done at construction time insidesuper::KtstrVm::collect_results), so clones cannot alias live device state. -
Arc-shared handles (
snapshot_bridge,stats_client): these wrapArc<Mutex<…>>/Arc<AtomicUsize>and clone via shallow refcount bump. TwoVmResultclones SHARE the underlying store — callingsnapshot_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 localVecbefore cloning theVmResult. -
The capture-series cache (
periodic_series_cache): aOnceLock<SampleSeries>that memoizes the one destructive drain of the category-2snapshot_bridge(seeSelf::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_bucketswas 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, callSelf::captures_series(or any accessor that routes through it) on the original BEFORE cloning.
- Populated (any of
Fields§
§success: boolOverall success flag: true when the test reported a pass AND
the VM exited cleanly without crash, timeout, or watchdog.
vcpus: u32Guest vCPU count for this run (topology llcscoresthreads),
carried from KtstrVm to the sidecar (vcpus, cpu_budget) stamp.
cpu_budget: u32Effective 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: boolTrue 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: i32Guest exit code as surfaced through the SHM ring
(MSG_TYPE_EXIT) or COM2 sentinel.
duration: DurationWall-clock duration of the VM run.
timed_out: boolTrue when the host hit its watchdog before the guest exited.
output: StringCaptured guest stdout (and any non-dmesg serial console content).
stderr: StringCaptured 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 ofVIRTIO_BLK_T_INrequests that returnedS_OKto the guest. Bumped together withbytes_readperVirtioBlkCounters::record_read.writes_completed— count ofVIRTIO_BLK_T_OUTrequests that returnedS_OK. Bumped together withbytes_written.flushes_completed— count ofVIRTIO_BLK_T_FLUSHrequests that returnedS_OK(realfdatasyncfor read-write disks, no-op forread_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 seeS_IOERRfor a stall (the request is deferred until the bucket refills). This counter is separate fromio_errorsso 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 reportsS_IOERR: spec violations, backendpread/pwriteerrors, malformed chains,add_usedfailures. Stalls do not reportS_IOERR; seethrottled_count.currently_throttled_gauge— live 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 fromthrottled_countwhich answers “how many stall events happened over time.”invalid_avail_idx_count— cumulative count ofError::InvalidAvailRingIndexevents observed bydrain_bracket_impl(avail.idx more thanqueue.sizeahead ofnext_avail— a virtio-v1.2 §2.7.13.3 avail.idx-distance violation by the guest). Per-event counter; thequeue_poisonedflag 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 TXadd_usedsucceeded. Over-size-dropped and malformed chains are still marked used (so the guest doesn’t hang) but do NOT advancetx_packets; a chain whoseadd_usedfails advancestx_add_used_failuresinstead. Sotx_packetsadvances 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_packetsandtx_packetsgate INDEPENDENTLY per chain:rx_packetsbumps when the loopback delivers — recorded BEFORE the TXadd_used— whiletx_packetsbumps only when that later TXadd_usedsucceeds. So the identity `rx_packets == tx_packets - tx_dropped_no_rx_buffer- tx_dropped_rx_poisoned
holds ONLY when both the RX-side failure counters ANDtx_add_used_failuresare zero: RX-side failures (rx_add_used_failures,rx_chain_invalid,rx_write_failed) makerx_packetsfall SHORT oftx_packets - drops, while atx_add_used_failureson a chain whose RX already delivered makesrx_packets` EXCEED it. Asymmetric counts surface queue-state breakage on either side.
- tx_dropped_rx_poisoned
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 bytx_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’smax_mtupermits.rx_write_failed— RX chain whose shape was valid but whose guest-memorywrite_slice(header or frame) hit an unmapped GPA. Distinct fromrx_chain_invalidso 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_failures—add_usedfailures, indicating the queue’s used-ring address itself is unmapped or otherwise inaccessible. Distinct from the*_chain_invalid/rx_write_failedcounters 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 ofError::InvalidAvailRingIndexevents observed byprocess_tx_loopback(avail.idx more thanqueue.sizeahead ofnext_avail— virtio-v1.2 §2.7.13.3 violation by the guest). Per-event counter; the per-queuequeue_poisonedflag 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-vqVIRTIO_NET_CTRL_MQ/VQ_PAIRS_SETcommands (the device updatedcurr_queue_pairsand wroteVIRTIO_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 avirtqueue_pairsoutside[1, queue_pairs]. Per-event hostile/buggy-guest counter; the control-vq analog oftx_chain_invalid.ctrl_add_used_failures— cumulative count of control-vq status-write oradd_usedfailures (the status byte’s GPA or the used-ring address is unmapped). Queue-state breakage distinct fromctrl_chain_invalid; the control-vq analog oftx_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: SnapshotBridgeSnapshot 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: u32Number 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: u32Periodic 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: u32Configured 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: u64Runtime 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: u64The 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
impl VmResult
Sourcepub fn kaslr_enabled(&self) -> bool
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.).
Sourcepub fn captures_series(&self) -> &SampleSeries
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.
Sourcepub fn periodic_series(&self) -> SampleSeries
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.
Sourcepub fn stimulus_timeline(&self) -> Vec<StimulusEvent>
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.
Sourcepub fn step_throughput(&self, phase: Phase) -> Option<f64>
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.
Sourcepub fn throughput_ratio(&self, a: Phase, b: Phase) -> Option<f64>
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.
Sourcepub fn guest_assert_result(&self) -> Result<AssertResult>
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.
Sourcepub fn phase_buckets(&self) -> Vec<PhaseBucket>
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:
- 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) throughcrate::assert::build_phase_buckets_with_stimulususingSelf::stimulus_timelinefor the step windows (window + metric folds;per_cgroupempty by construction); and - the guest per-cgroup carriers from
Self::guest_assert_result(stats.phases[].per_cgroup), folded in bycrate::assert::fold_guest_per_cgroup_into_host_bucketskeyed bystep_index(the host window + metrics win; each carrier contributes only itsper_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).
Sourcepub fn phase_cgroup(
&self,
phase: Phase,
cgroup: &str,
) -> Option<PhaseCgroupStats>
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.
Sourcepub fn phase_metric(
&self,
phase: Phase,
metric: impl Into<MetricId>,
) -> Option<f64>
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:
crate::assert::PhaseBucket::metrics(viacrate::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, …), anditeration_rate.- 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 registeredCounters with no per-sample source (crate::stats::MetricDef::read_samplereturnsNone), so they never reachmetrics; without this fallbackphase_metric(phase, "total_migrations")returned a silentNoneeven 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");Sourcepub fn run_metric(&self, metric: impl Into<MetricId>) -> Option<f64>
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 typedGauntletRowfields (not ext-only); read them per-phase viaSelf::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");Sourcepub fn better_across_phases<'v>(
&self,
verdict: &'v mut Verdict,
baseline: Phase,
candidate: Phase,
metric: impl Into<MetricId>,
) -> BetterThanPhase<'v>
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.
Sourcepub fn phase_cgroup_metric(
&self,
phase: Phase,
cgroup: &str,
metric: impl Into<MetricId>,
) -> Option<f64>
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).
Sourcepub fn better_across_phases_cgroup<'v>(
&self,
verdict: &'v mut Verdict,
baseline: Phase,
candidate: Phase,
cgroup: &str,
metric: impl Into<MetricId>,
) -> BetterThanPhase<'v>
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.
Sourcepub fn wprof_pb_path(&self) -> Result<PathBuf>
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.
Sourcepub fn repro_wprof_pb_path(&self) -> Result<PathBuf>
pub fn repro_wprof_pb_path(&self) -> Result<PathBuf>
Per-test sidecar path for the .repro.wprof.pb artifact.
Sourcepub fn failure_dump_path(&self) -> Result<PathBuf>
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.
Sourcepub fn guest_kmsg(&self) -> String
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.
Sourcepub fn scheduler_log(&self) -> String
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.
Sourcepub fn watchdog_observation(&self) -> Option<WatchdogObservation>
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).
Sourcepub fn assert_wprof_pb_landed(&self) -> Result<()>
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§
Auto Trait Implementations§
impl !Freeze for VmResult
impl !RefUnwindSafe for VmResult
impl Send for VmResult
impl Sync for VmResult
impl Unpin for VmResult
impl !UnwindSafe for VmResult
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self>
fn instrument(self, span: Span) -> Instrumented<Self>
§fn in_current_span(self) -> Instrumented<Self>
fn in_current_span(self) -> Instrumented<Self>
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self>
fn into_either(self, into_left: bool) -> Either<Self, Self>
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
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