1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! Diagnostic snapshot capture and traversal.
//!
//! Test scenarios use [`Op::CaptureSnapshot`](crate::scenario::ops::Op::CaptureSnapshot)
//! to request a host-side diagnostic capture mid-run. The capture
//! result — a `crate::monitor::dump::FailureDumpReport` — is keyed by the `name` argument
//! and stored on the scenario's [`SnapshotBridge`], where downstream
//! test code reaches it via [`Snapshot`] for typed traversal of
//! BTF-rendered map values, per-CPU entries, and scalar variables.
//!
//! # Lifecycle
//!
//! 1. **Wire-up.** Before [`execute_steps`](crate::scenario::ops::execute_steps)
//! runs, host orchestration installs a [`SnapshotBridge`] in the
//! current thread via [`SnapshotBridge::set_thread_local`]. The
//! bridge owns the storage map and a callable that performs the
//! capture.
//!
//! 2. **Capture.** When the executor reaches `Op::CaptureSnapshot { name }`,
//! it invokes [`SnapshotBridge::capture`] with the name. The
//! closure performs the freeze rendezvous (request/reply with
//! the freeze coordinator), builds a `crate::monitor::dump::FailureDumpReport`, and
//! returns it; the bridge stores it under the name.
//!
//! 3. **Inspection.** After the scenario completes, the test author
//! pulls captured reports out via [`SnapshotBridge::drain`] and
//! constructs [`Snapshot`] views to assert against rendered
//! values:
//! `snapshot.var("nr_cpus_onln").as_u64()? > 0`,
//! `snapshot.map("scx_per_task")?.find(|e| e.get("tid").as_i64()? == pid)?`.
//!
//! # On-demand vs error-trigger captures
//!
//! `Op::CaptureSnapshot` requests are orthogonal to the error-class freeze
//! path. The freeze coordinator's existing state machine for
//! `SCX_EXIT_ERROR` triggers (Idle → TookEarly → Done) governs the
//! *unsolicited* capture pipeline; on-demand captures funnel
//! through a separate request/reply channel and never touch the
//! error-trigger state. The coordinator services on-demand requests
//! even after Done so post-failure scenarios can still snapshot
//! state for context. The serialisation rule: at most one capture in
//! flight at a time — the on-demand path waits for the previous
//! capture's vCPUs to fully return to `parked == false` before
//! issuing the next freeze request, mirroring the rendezvous
//! invariants the error-trigger path already obeys.
//!
//! # Guest → host wire: ioeventfd doorbell (locked)
//!
//! The guest-driven capture trigger uses an in-kernel ioeventfd
//! doorbell, NOT a synchronous MMIO `BusDevice` arm. Per user
//! direction:
//!
//! 1. Host registers an ioeventfd at a dedicated MMIO GPA inside
//! the existing MMIO gap (e.g. `MMIO_GAP_START + 0x3000`) via
//! `KVM_IOEVENTFD`. The exact GPA is arch-dependent —
//! `MMIO_GAP_START + 0x3000` on x86_64,
//! `VIRTIO_NET_MMIO_BASE + VIRTIO_MMIO_SIZE` on aarch64. The
//! fd is owned by the freeze coordinator and polled alongside
//! its existing wake sources.
//! 2. Guest [`Op::CaptureSnapshot`](crate::scenario::ops::Op::CaptureSnapshot)
//! handler `mmap`s `/dev/mem` to reach the doorbell GPA (same
//! pattern the SHM ring already uses) and writes the tag value
//! plus a serial counter into a small per-call slot, then
//! writes the doorbell. KVM dispatches the write in-kernel and
//! raises the eventfd; the vCPU thread does NOT exit to
//! userspace for the doorbell write itself.
//! 3. The freeze coordinator wakes on `eventfd_signal`, reads the
//! tag from the slot, runs `freeze_and_capture`, builds the
//! `crate::monitor::dump::FailureDumpReport`, and stores it on the bridge keyed by
//! that tag. Reply to the guest is implicit — the
//! [`SnapshotBridge::capture`] callback installed in the
//! executor's thread-local blocks on a per-request reply
//! eventfd / completion channel paired with the doorbell.
//!
//! This shape keeps the capture trigger off the vCPU userspace
//! exit path (cleaner — no MMIO `BusDevice` round-trip) and is
//! extensible to higher-rate triggers without redesigning the
//! wire. The [`SnapshotBridge`] surface defined below is the
//! integration point; `ioeventfd` is the wake mechanism that
//! drives the `CaptureCallback` from the guest side. The guest
//! [`Op::WatchSnapshot`](crate::scenario::ops::Op::WatchSnapshot)
//! registration uses the same doorbell at scenario setup
//! (separate tag namespace) so symbol resolution + user
//! watchpoint slot allocation happen on the host without a vCPU
//! userspace exit.
//!
//! # No-bridge fallback
//!
//! When `Op::CaptureSnapshot` runs in a context with no installed bridge
//! (e.g. unit tests that exercise the executor without spinning up
//! a VM), the op is a no-op with a `tracing::warn!`. Existing
//! scenarios that do not declare snapshot ops keep working
//! unchanged.
//!
//! # Field accessor traversal
//!
//! [`SnapshotMap`], [`SnapshotEntry`], and [`SnapshotField`] form a
//! lazy borrow chain over the report. Dotted-path lookups (e.g.
//! `entry.get("ctx.weight.value")`) walk
//! `RenderedValue::Struct` members by name and follow
//! `RenderedValue::Ptr` dereferences transparently — the test
//! author writes the dotted path the BTF source would suggest;
//! pointer chasing is invisible.
//!
//! Missing fields land in [`SnapshotField::Missing`] with an
//! actionable error string identifying the path component that
//! could not be resolved AND the available alternatives at that
//! level. Terminal accessors (`as_u64`, `as_i64`, `as_bool`,
//! `as_str`) return `Result<T, SnapshotError>` so an absent /
//! type-mismatched field bubbles up as a recoverable error rather
//! than panicking.
//!
//! # Cross-surface accessor vocabulary
//!
//! [`SnapshotField`], [`JsonField`], and
//! `crate::monitor::btf_render::RenderedValue` share a uniform
//! method vocabulary so a test author moves between the
//! BTF-rendered (BPF maps + globals), JSON-rendered (scheduler
//! stats), and raw-tree surfaces without re-learning syntax:
//!
//! | Method | What it does |
//! |-----------------------|------------------------------------------------------------------|
//! | `.as_u64()`/`.as_i64()`/`.as_f64()`/`.as_bool()` | Typed scalar extract. |
//! | `.as_str()` | UTF-8 string extract (Enum variant / JSON string). |
//! | `.as_u64_array()` / `.as_u32_array()` / `.as_i64_array()` / `.as_f64_array()` / `.as_bool_array()` | Element-typed array extract. |
//! | `.get(path)` | Dotted-path walk (`"a.b.c"`); returns a typed sub-view. |
//! | `.member(name)` | Single-step struct-member walk (RenderedValue only; no dots). |
//! | `.index(i)` | Array element by 0-indexed position (RenderedValue only). |
//! | `.raw()` | Drop into the underlying RenderedValue for raw Option-returning navigation. |
//!
//! The wrapper types ([`SnapshotField`], [`JsonField`]) return
//! `Result` with rich [`SnapshotError`] context; the raw
//! `RenderedValue` layer returns `Option` (the caller has already
//! pattern-matched into a known variant, so absence is a
//! programming-error class handled locally). Convert between
//! layers with `SnapshotField::raw()`.
//!
//! For multi-scheduler scenarios (after
//! [`crate::scenario::ops::Op::ReplaceScheduler`] or two
//! [`crate::scenario::ops::Op::AttachScheduler`] calls), use
//! [`Snapshot::active`] to project the view to the currently-
//! attached scheduler's maps and chain the standard accessors
//! against it. [`Snapshot::live_var`] is the shorthand for
//! `self.active()?.var(name)`; [`Snapshot::vars`] iterates every
//! captured copy when the framework cannot determine "active"
//! automatically.
/// Maximum number of rendered keys captured into
/// [`SnapshotError::NoMatch::available_keys`] during a failed
/// `find` / `max_by` traversal. Three is a balance between
/// disambiguation power (enough to suggest the keyspace shape) and
/// failure-message readability (does not overrun a terminal line).
pub const NO_MATCH_KEY_SAMPLE: usize = 3;
/// Maximum number of characters each rendered key in
/// [`SnapshotError::NoMatch::available_keys`] retains before being
/// truncated with a trailing `…`. Wide struct keys (e.g. a
/// 50-field `task_ctx`) would otherwise produce kilobytes of
/// failure text per sampled key.
pub const NO_MATCH_KEY_CHAR_CAP: usize = 80;
/// Discriminator that `render_entry_key`'s fallback path prepends
/// to the raw `key_hex` bytes when an entry's BTF-rendered key was
/// missing at capture time. [`SnapshotError::NoMatch`]'s `Display`
/// impl uses the same prefix as the gate for its BTF-missing hint
/// (when every sampled key starts with this string, BTF was
/// uniformly absent for the map's key type and the hint points the
/// operator at `CONFIG_DEBUG_INFO_BTF=y`). Naming the producer +
/// consumer contract once here keeps a future rename of one side
/// from silently desynchronising the other. Test sites in this
/// module intentionally retain the literal `"hex:"` so they pin the
/// value separately from the const that synchronises production.
pub const HEX_KEY_PREFIX: &str = "hex:";
pub use ;
pub use ;
pub use SnapshotEntry;
pub use SnapshotField;
pub use walk_dotted_path;
pub use ;
pub use ;
// ---------------------------------------------------------------------------
// Snapshot view over a captured FailureDumpReport
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// SnapshotEntry
// ---------------------------------------------------------------------------