Skip to main content

must/
observer.rs

1//! Watching a run as it explores.
2//!
3//! `explore` returns nothing; everything you learn about a run comes through an
4//! [`Observer`]. The explorer notifies it at every interesting step: an event added, an rf
5//! choice, an inconsistent graph dropped, a backward revisit performed or rejected, a
6//! terminal execution reached, a thread found blocked. Every method has an empty default
7//! body, so an observer implements only what it cares about.
8//!
9//! Callbacks take `&self`, so a single observer is shared across every worker thread of a
10//! parallel run (`explore` requires `Observer + Sync`). An observer therefore holds its own
11//! interior mutability and picks its own synchronisation: [`NullObserver`] needs none,
12//! [`CountingObserver`] shards its counters per worker (no cross-thread contention on the
13//! hot path), and the recording observers use a mutex.
14//!
15//! Four implementations come with the crate: [`NullObserver`] (ignores everything),
16//! [`CountingObserver`] (running totals), [`RecordingObserver`] (a flat log of owned step
17//! snapshots), and [`ExecutionCollector`] (keeps the terminal executions grouped by
18//! outcome). Two observers compose as a tuple `(A, B)`.
19
20use std::cell::Cell;
21use std::collections::BTreeSet;
22use std::sync::atomic::{AtomicUsize, Ordering};
23use std::sync::{Mutex, OnceLock};
24
25use crate::event::{EventId, Tid};
26use crate::explorer::{Execution, ExecutionKind};
27use crate::graph::ExecutionGraph;
28
29/// Callbacks fired by the explorer. Every method defaults to a no-op.
30pub trait Observer {
31    /// A fresh event `e`, maximal in insertion order, was added to `g`.
32    fn on_event_added(&self, _g: &ExecutionGraph, _e: EventId) {}
33    /// Receive `r` was pointed at source `src` (`None` means no message) before a
34    /// consistency check.
35    fn on_rf_choice(&self, _g: &ExecutionGraph, _r: EventId, _src: Option<EventId>) {}
36    /// A graph was found inconsistent and dropped.
37    fn on_inconsistent(&self, _g: &ExecutionGraph) {}
38    /// A backward revisit that sets `rf(r)` to `s` is about to run; `deleted` lists the
39    /// events it removes (still including `s`).
40    fn on_backward_revisit(
41        &self,
42        _g: &ExecutionGraph,
43        _r: EventId,
44        _s: EventId,
45        _deleted: &BTreeSet<EventId>,
46    ) {
47    }
48    /// A candidate backward revisit setting `rf(r)` to `s` was rejected.
49    fn on_revisit_rejected(&self, _g: &ExecutionGraph, _r: EventId, _s: EventId) {}
50    /// A terminal execution (full / blocked / error) was reached.
51    fn on_execution(&self, _exec: &Execution, _kind: ExecutionKind) {}
52    /// Thread `tid` is blocked on a receive with no message in `g`.
53    fn on_thread_blocked(&self, _g: &ExecutionGraph, _tid: Tid) {}
54}
55
56// -- Sharding: routing a shared observer to a per-worker slot ----------------------
57
58thread_local! {
59    /// Which worker this thread is, so a shared observer can pick a per-worker shard.
60    /// Zero on the main thread and every sequential run.
61    static WORKER_ID: Cell<usize> = const { Cell::new(0) };
62}
63
64/// Called once by each parallel worker so a shared [`CountingObserver`] routes that
65/// worker's tallies to its own shard.
66pub(crate) fn set_worker_id(w: usize) {
67    WORKER_ID.with(|c| c.set(w));
68}
69
70/// This thread's worker id (`0` on the main thread and every sequential run). Lets a
71/// shared observer route to a per-worker shard; see [`CountingObserver`] and
72/// [`crate::viz::TraceObserver`].
73pub(crate) fn worker_id() -> usize {
74    WORKER_ID.with(Cell::get)
75}
76
77/// Shard count a freshly built [`CountingObserver`] uses: the machine's parallelism, so a
78/// shared counter never contends on one cache line during a parallel run. Computed once.
79/// Shared with [`crate::viz::TraceObserver`], which shards its trace buffers the same way.
80pub(crate) fn default_shards() -> usize {
81    static N: OnceLock<usize> = OnceLock::new();
82    *N.get_or_init(|| std::thread::available_parallelism().map_or(1, |n| n.get()))
83}
84
85/// Observer that ignores everything.
86#[derive(Clone, Copy, Debug, Default)]
87pub struct NullObserver;
88
89impl Observer for NullObserver {}
90
91/// One worker's tallies. Aligned to a cache line so two workers writing adjacent shards
92/// never trigger false sharing; each shard is written by a single thread, so its atomics
93/// are always uncontended.
94#[repr(align(64))]
95#[derive(Debug, Default)]
96struct Shard {
97    events_added: AtomicUsize,
98    rf_choices: AtomicUsize,
99    inconsistent: AtomicUsize,
100    backward_revisits: AtomicUsize,
101    revisits_rejected: AtomicUsize,
102    full: AtomicUsize,
103    blocked: AtomicUsize,
104    errors: AtomicUsize,
105    threads_blocked: AtomicUsize,
106}
107
108/// Running totals of every callback, sharded per worker so counting adds no cross-thread
109/// contention under a parallel run. Read a total with the accessor methods, which sum the
110/// shards.
111#[derive(Debug)]
112pub struct CountingObserver {
113    shards: Vec<Shard>,
114}
115
116impl Default for CountingObserver {
117    fn default() -> Self {
118        Self::new()
119    }
120}
121
122impl CountingObserver {
123    /// A counter sized for the machine's parallelism, ready to be shared across a parallel
124    /// run without contention.
125    pub fn new() -> Self {
126        Self::with_shards(default_shards())
127    }
128
129    /// A counter with an explicit shard count (at least one). Use this to match a known
130    /// worker count exactly; [`new`](Self::new) picks a sensible default.
131    pub fn with_shards(shards: usize) -> Self {
132        CountingObserver {
133            shards: (0..shards.max(1)).map(|_| Shard::default()).collect(),
134        }
135    }
136
137    /// This thread's shard (`worker_id mod shard_count`).
138    fn shard(&self) -> &Shard {
139        &self.shards[worker_id() % self.shards.len()]
140    }
141
142    fn total(&self, pick: impl Fn(&Shard) -> &AtomicUsize) -> usize {
143        self.shards
144            .iter()
145            .map(|s| pick(s).load(Ordering::Relaxed))
146            .sum()
147    }
148
149    pub fn events_added(&self) -> usize {
150        self.total(|s| &s.events_added)
151    }
152    pub fn rf_choices(&self) -> usize {
153        self.total(|s| &s.rf_choices)
154    }
155    pub fn inconsistent(&self) -> usize {
156        self.total(|s| &s.inconsistent)
157    }
158    pub fn backward_revisits(&self) -> usize {
159        self.total(|s| &s.backward_revisits)
160    }
161    pub fn revisits_rejected(&self) -> usize {
162        self.total(|s| &s.revisits_rejected)
163    }
164    pub fn full(&self) -> usize {
165        self.total(|s| &s.full)
166    }
167    pub fn blocked(&self) -> usize {
168        self.total(|s| &s.blocked)
169    }
170    pub fn errors(&self) -> usize {
171        self.total(|s| &s.errors)
172    }
173    pub fn threads_blocked(&self) -> usize {
174        self.total(|s| &s.threads_blocked)
175    }
176
177    /// Total terminal executions (full + blocked).
178    pub fn terminal(&self) -> usize {
179        self.full() + self.blocked()
180    }
181}
182
183impl Observer for CountingObserver {
184    fn on_event_added(&self, _g: &ExecutionGraph, _e: EventId) {
185        self.shard().events_added.fetch_add(1, Ordering::Relaxed);
186    }
187    fn on_rf_choice(&self, _g: &ExecutionGraph, _r: EventId, _src: Option<EventId>) {
188        self.shard().rf_choices.fetch_add(1, Ordering::Relaxed);
189    }
190    fn on_inconsistent(&self, _g: &ExecutionGraph) {
191        self.shard().inconsistent.fetch_add(1, Ordering::Relaxed);
192    }
193    fn on_backward_revisit(
194        &self,
195        _g: &ExecutionGraph,
196        _r: EventId,
197        _s: EventId,
198        _deleted: &BTreeSet<EventId>,
199    ) {
200        self.shard()
201            .backward_revisits
202            .fetch_add(1, Ordering::Relaxed);
203    }
204    fn on_revisit_rejected(&self, _g: &ExecutionGraph, _r: EventId, _s: EventId) {
205        self.shard()
206            .revisits_rejected
207            .fetch_add(1, Ordering::Relaxed);
208    }
209    fn on_execution(&self, _exec: &Execution, kind: ExecutionKind) {
210        let shard = self.shard();
211        match kind {
212            ExecutionKind::Full => &shard.full,
213            ExecutionKind::Blocked => &shard.blocked,
214            ExecutionKind::Error => &shard.errors,
215        }
216        .fetch_add(1, Ordering::Relaxed);
217    }
218    fn on_thread_blocked(&self, _g: &ExecutionGraph, _tid: Tid) {
219        self.shard().threads_blocked.fetch_add(1, Ordering::Relaxed);
220    }
221}
222
223/// One recorded step, as an owned snapshot (no borrow of the live graph).
224#[derive(Clone, Debug)]
225pub struct Step {
226    pub kind: StepKind,
227    /// The graph as it stood at this step (owned).
228    pub graph: ExecutionGraph,
229}
230
231/// The event a [`Step`] captured.
232#[derive(Clone, Debug)]
233pub enum StepKind {
234    EventAdded {
235        e: EventId,
236    },
237    RfChoice {
238        r: EventId,
239        src: Option<EventId>,
240    },
241    Inconsistent,
242    BackwardRevisit {
243        r: EventId,
244        s: EventId,
245        deleted: Vec<EventId>,
246    },
247    RevisitRejected {
248        r: EventId,
249        s: EventId,
250    },
251    Execution {
252        kind: ExecutionKind,
253    },
254    ThreadBlocked {
255        tid: Tid,
256    },
257}
258
259/// Flat log of every step, for later rendering. Best for sequential runs — a parallel run
260/// interleaves the workers' steps into one meaningless log.
261#[derive(Debug, Default)]
262pub struct RecordingObserver {
263    steps: Mutex<Vec<Step>>,
264}
265
266impl RecordingObserver {
267    pub fn new() -> Self {
268        Self::default()
269    }
270
271    pub fn len(&self) -> usize {
272        self.steps.lock().unwrap().len()
273    }
274
275    pub fn is_empty(&self) -> bool {
276        self.steps.lock().unwrap().is_empty()
277    }
278
279    /// A snapshot of the steps recorded so far.
280    pub fn steps(&self) -> Vec<Step> {
281        self.steps.lock().unwrap().clone()
282    }
283
284    fn record(&self, kind: StepKind, graph: &ExecutionGraph) {
285        self.steps.lock().unwrap().push(Step {
286            kind,
287            graph: graph.clone(),
288        });
289    }
290}
291
292impl Observer for RecordingObserver {
293    fn on_event_added(&self, g: &ExecutionGraph, e: EventId) {
294        self.record(StepKind::EventAdded { e }, g);
295    }
296    fn on_rf_choice(&self, g: &ExecutionGraph, r: EventId, src: Option<EventId>) {
297        self.record(StepKind::RfChoice { r, src }, g);
298    }
299    fn on_inconsistent(&self, g: &ExecutionGraph) {
300        self.record(StepKind::Inconsistent, g);
301    }
302    fn on_backward_revisit(
303        &self,
304        g: &ExecutionGraph,
305        r: EventId,
306        s: EventId,
307        deleted: &BTreeSet<EventId>,
308    ) {
309        let deleted = deleted.iter().copied().collect();
310        self.record(StepKind::BackwardRevisit { r, s, deleted }, g);
311    }
312    fn on_revisit_rejected(&self, g: &ExecutionGraph, r: EventId, s: EventId) {
313        self.record(StepKind::RevisitRejected { r, s }, g);
314    }
315    fn on_execution(&self, exec: &Execution, kind: ExecutionKind) {
316        self.record(StepKind::Execution { kind }, exec.graph());
317    }
318    fn on_thread_blocked(&self, g: &ExecutionGraph, tid: Tid) {
319        self.record(StepKind::ThreadBlocked { tid }, g);
320    }
321}
322
323/// Keeps the terminal executions grouped by outcome. Pass one to `explore` when you want
324/// the graphs, canonical keys or pending sends of each outcome, not just how many there
325/// were.
326///
327/// Under a parallel run the executions are collected in a nondeterministic order (the
328/// counts are still exact); compare canonical-key *sets*, not their order.
329#[derive(Debug, Default)]
330pub struct ExecutionCollector {
331    inner: Mutex<Collected>,
332}
333
334#[derive(Debug, Default)]
335struct Collected {
336    full: Vec<Execution>,
337    blocked: Vec<Execution>,
338    errors: Vec<Execution>,
339}
340
341impl ExecutionCollector {
342    pub fn new() -> Self {
343        Self::default()
344    }
345
346    /// The full executions collected so far.
347    pub fn full(&self) -> Vec<Execution> {
348        self.inner.lock().unwrap().full.clone()
349    }
350    /// The blocked executions (maximal consistent prefixes).
351    pub fn blocked(&self) -> Vec<Execution> {
352        self.inner.lock().unwrap().blocked.clone()
353    }
354    /// The erroneous executions.
355    pub fn errors(&self) -> Vec<Execution> {
356        self.inner.lock().unwrap().errors.clone()
357    }
358    /// Full followed by blocked - the terminal executions the search must not duplicate.
359    pub fn terminals(&self) -> Vec<Execution> {
360        let c = self.inner.lock().unwrap();
361        c.full.iter().chain(c.blocked.iter()).cloned().collect()
362    }
363
364    pub fn full_count(&self) -> usize {
365        self.inner.lock().unwrap().full.len()
366    }
367    pub fn blocked_count(&self) -> usize {
368        self.inner.lock().unwrap().blocked.len()
369    }
370    pub fn error_count(&self) -> usize {
371        self.inner.lock().unwrap().errors.len()
372    }
373    /// Full + blocked, the terminal count.
374    pub fn terminal_count(&self) -> usize {
375        let c = self.inner.lock().unwrap();
376        c.full.len() + c.blocked.len()
377    }
378
379    /// Canonical keys of the full executions.
380    pub fn full_keys(&self) -> Vec<String> {
381        self.inner
382            .lock()
383            .unwrap()
384            .full
385            .iter()
386            .map(Execution::canonical_key)
387            .collect()
388    }
389    /// Canonical keys over full plus blocked, for the duplicate check.
390    pub fn terminal_keys(&self) -> Vec<String> {
391        let c = self.inner.lock().unwrap();
392        c.full
393            .iter()
394            .chain(c.blocked.iter())
395            .map(Execution::canonical_key)
396            .collect()
397    }
398    /// Canonical keys of the erroneous executions.
399    pub fn error_keys(&self) -> Vec<String> {
400        self.inner
401            .lock()
402            .unwrap()
403            .errors
404            .iter()
405            .map(Execution::canonical_key)
406            .collect()
407    }
408}
409
410impl Observer for ExecutionCollector {
411    fn on_execution(&self, exec: &Execution, kind: ExecutionKind) {
412        let mut c = self.inner.lock().unwrap();
413        match kind {
414            ExecutionKind::Full => c.full.push(exec.clone()),
415            ExecutionKind::Blocked => c.blocked.push(exec.clone()),
416            ExecutionKind::Error => c.errors.push(exec.clone()),
417        }
418    }
419}
420
421/// Compose two observers: every callback fans out to both, `A` before `B`.
422impl<A: Observer, B: Observer> Observer for (A, B) {
423    fn on_event_added(&self, g: &ExecutionGraph, e: EventId) {
424        self.0.on_event_added(g, e);
425        self.1.on_event_added(g, e);
426    }
427    fn on_rf_choice(&self, g: &ExecutionGraph, r: EventId, src: Option<EventId>) {
428        self.0.on_rf_choice(g, r, src);
429        self.1.on_rf_choice(g, r, src);
430    }
431    fn on_inconsistent(&self, g: &ExecutionGraph) {
432        self.0.on_inconsistent(g);
433        self.1.on_inconsistent(g);
434    }
435    fn on_backward_revisit(
436        &self,
437        g: &ExecutionGraph,
438        r: EventId,
439        s: EventId,
440        deleted: &BTreeSet<EventId>,
441    ) {
442        self.0.on_backward_revisit(g, r, s, deleted);
443        self.1.on_backward_revisit(g, r, s, deleted);
444    }
445    fn on_revisit_rejected(&self, g: &ExecutionGraph, r: EventId, s: EventId) {
446        self.0.on_revisit_rejected(g, r, s);
447        self.1.on_revisit_rejected(g, r, s);
448    }
449    fn on_execution(&self, exec: &Execution, kind: ExecutionKind) {
450        self.0.on_execution(exec, kind);
451        self.1.on_execution(exec, kind);
452    }
453    fn on_thread_blocked(&self, g: &ExecutionGraph, tid: Tid) {
454        self.0.on_thread_blocked(g, tid);
455        self.1.on_thread_blocked(g, tid);
456    }
457}