Skip to main content

vortex_core/
context.rs

1//! Simulation context and event logging.
2//!
3//! [`SimContext`] is the central state holder for a single simulation run.
4//! It owns the seed, logical clock, fault configuration, and the structured
5//! event log. Every simulated action flows through the context so that
6//! two runs with the same seed produce identical [`SimEventLog`]s.
7
8use crate::rng::DetRng;
9use crate::seed::{SeedTree, VortexSeed};
10use std::fmt;
11
12/// Monotonic sequence counter for event ordering within a simulation.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct SeqNo(pub u64);
15
16/// A single recorded simulation event.
17#[derive(Debug, Clone)]
18pub struct SimLogEntry {
19    /// Monotonic sequence number (unique within a simulation run).
20    pub seq: SeqNo,
21    /// Logical timestamp (microseconds) when this event occurred.
22    pub timestamp_us: u64,
23    /// Which subsystem produced this event.
24    pub subsystem: Subsystem,
25    /// Human-readable description of what happened.
26    pub description: String,
27    /// Optional structured payload (for machine comparison).
28    pub payload: Option<Vec<u8>>,
29}
30
31/// Subsystem tags for event categorisation.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum Subsystem {
34    /// Async executor / task scheduling.
35    Executor,
36    /// Virtual file system.
37    Fs,
38    /// Logical clock / timers.
39    Clock,
40    /// Memory allocator.
41    Alloc,
42    /// Network (existing distributed sim).
43    Network,
44    /// Subprocess / process spawning.
45    Process,
46    /// Storage (existing KV sim).
47    Storage,
48    /// User / custom subsystem.
49    Custom,
50}
51
52impl fmt::Display for Subsystem {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        match self {
55            Subsystem::Executor => write!(f, "executor"),
56            Subsystem::Fs => write!(f, "fs"),
57            Subsystem::Clock => write!(f, "clock"),
58            Subsystem::Alloc => write!(f, "alloc"),
59            Subsystem::Network => write!(f, "network"),
60            Subsystem::Process => write!(f, "process"),
61            Subsystem::Storage => write!(f, "storage"),
62            Subsystem::Custom => write!(f, "custom"),
63        }
64    }
65}
66
67/// Structured event log for a simulation run.
68///
69/// Records every simulated action with monotonic sequence numbers.
70/// Two runs with the same seed should produce identical logs — use
71/// [`SimEventLog::diff`] to verify this and pinpoint the first divergence.
72pub struct SimEventLog {
73    entries: Vec<SimLogEntry>,
74    next_seq: u64,
75}
76
77impl SimEventLog {
78    /// Create a new, empty event log.
79    pub fn new() -> Self {
80        Self {
81            entries: Vec::new(),
82            next_seq: 0,
83        }
84    }
85
86    /// Record an event. Returns the assigned sequence number.
87    pub fn record(
88        &mut self,
89        timestamp_us: u64,
90        subsystem: Subsystem,
91        description: String,
92        payload: Option<Vec<u8>>,
93    ) -> SeqNo {
94        let seq = SeqNo(self.next_seq);
95        self.next_seq += 1;
96        self.entries.push(SimLogEntry {
97            seq,
98            timestamp_us,
99            subsystem,
100            description,
101            payload,
102        });
103        seq
104    }
105
106    /// Record a simple event (no payload).
107    pub fn record_simple(
108        &mut self,
109        timestamp_us: u64,
110        subsystem: Subsystem,
111        description: impl Into<String>,
112    ) -> SeqNo {
113        self.record(timestamp_us, subsystem, description.into(), None)
114    }
115
116    /// Get all entries.
117    pub fn entries(&self) -> &[SimLogEntry] {
118        &self.entries
119    }
120
121    /// Number of recorded events.
122    pub fn len(&self) -> usize {
123        self.entries.len()
124    }
125
126    /// Whether the log is empty.
127    pub fn is_empty(&self) -> bool {
128        self.entries.is_empty()
129    }
130
131    /// Compare two event logs and find the first point of divergence.
132    ///
133    /// Returns `None` if the logs are identical (same length, same descriptions,
134    /// same subsystems, same payloads at every sequence number).
135    /// Returns `Some(divergence)` with details about where they first differ.
136    pub fn diff(a: &SimEventLog, b: &SimEventLog) -> Option<LogDivergence> {
137        let min_len = a.entries.len().min(b.entries.len());
138        for i in 0..min_len {
139            let ea = &a.entries[i];
140            let eb = &b.entries[i];
141            if ea.subsystem != eb.subsystem
142                || ea.description != eb.description
143                || ea.timestamp_us != eb.timestamp_us
144                || ea.payload != eb.payload
145            {
146                return Some(LogDivergence {
147                    seq: SeqNo(i as u64),
148                    entry_a: ea.clone(),
149                    entry_b: eb.clone(),
150                    kind: DivergenceKind::ContentMismatch,
151                });
152            }
153        }
154        if a.entries.len() != b.entries.len() {
155            let longer = if a.entries.len() > b.entries.len() {
156                "a"
157            } else {
158                "b"
159            };
160            return Some(LogDivergence {
161                seq: SeqNo(min_len as u64),
162                entry_a: a
163                    .entries
164                    .get(min_len)
165                    .cloned()
166                    .unwrap_or_else(|| SimLogEntry {
167                        seq: SeqNo(min_len as u64),
168                        timestamp_us: 0,
169                        subsystem: Subsystem::Custom,
170                        description: format!("<log {longer} has no more entries>"),
171                        payload: None,
172                    }),
173                entry_b: b
174                    .entries
175                    .get(min_len)
176                    .cloned()
177                    .unwrap_or_else(|| SimLogEntry {
178                        seq: SeqNo(min_len as u64),
179                        timestamp_us: 0,
180                        subsystem: Subsystem::Custom,
181                        description: format!("<log {longer} has no more entries>"),
182                        payload: None,
183                    }),
184                kind: DivergenceKind::LengthMismatch {
185                    len_a: a.entries.len(),
186                    len_b: b.entries.len(),
187                },
188            });
189        }
190        None
191    }
192}
193
194impl Default for SimEventLog {
195    fn default() -> Self {
196        Self::new()
197    }
198}
199
200/// Description of where two event logs first diverge.
201#[derive(Debug, Clone)]
202pub struct LogDivergence {
203    /// Sequence number at which divergence was detected.
204    pub seq: SeqNo,
205    /// Entry from log A at that position.
206    pub entry_a: SimLogEntry,
207    /// Entry from log B at that position.
208    pub entry_b: SimLogEntry,
209    /// What kind of divergence.
210    pub kind: DivergenceKind,
211}
212
213/// The kind of divergence found between two logs.
214#[derive(Debug, Clone)]
215pub enum DivergenceKind {
216    /// The entries at the same sequence number have different content.
217    ContentMismatch,
218    /// The logs have different lengths.
219    LengthMismatch { len_a: usize, len_b: usize },
220}
221
222impl fmt::Display for LogDivergence {
223    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224        match &self.kind {
225            DivergenceKind::ContentMismatch => {
226                write!(
227                    f,
228                    "Divergence at seq {}: A=[{}] {:?} vs B=[{}] {:?}",
229                    self.seq.0,
230                    self.entry_a.subsystem,
231                    self.entry_a.description,
232                    self.entry_b.subsystem,
233                    self.entry_b.description,
234                )
235            }
236            DivergenceKind::LengthMismatch { len_a, len_b } => {
237                write!(
238                    f,
239                    "Length mismatch at seq {}: log A has {} entries, log B has {} entries",
240                    self.seq.0, len_a, len_b,
241                )
242            }
243        }
244    }
245}
246
247/// Central simulation context.
248///
249/// Holds the master seed, seed tree, logical clock, and event log for a
250/// single simulation run. Pass a reference to `SimContext` into every
251/// subsystem that needs deterministic randomness or event logging.
252pub struct SimContext {
253    seed: VortexSeed,
254    seed_tree: SeedTree,
255    logical_time_us: u64,
256    event_log: SimEventLog,
257}
258
259impl SimContext {
260    /// Create a new simulation context from a master seed.
261    pub fn new(seed: VortexSeed) -> Self {
262        Self {
263            seed,
264            seed_tree: SeedTree::new(seed),
265            logical_time_us: 0,
266            event_log: SimEventLog::new(),
267        }
268    }
269
270    /// Create from a simple u64 seed.
271    pub fn from_u64(seed: u64) -> Self {
272        Self::new(VortexSeed::from_u64(seed))
273    }
274
275    /// Get the master seed.
276    pub fn seed(&self) -> VortexSeed {
277        self.seed
278    }
279
280    /// Get the seed tree for deriving subsystem-specific RNGs.
281    pub fn seed_tree(&self) -> &SeedTree {
282        &self.seed_tree
283    }
284
285    /// Create a [`DetRng`] for a specific subsystem domain.
286    pub fn rng_for(&self, domain: &str) -> DetRng {
287        let sub_seed = self.seed_tree.derive(domain);
288        DetRng::new(sub_seed.to_u64())
289    }
290
291    /// Get the current logical time in microseconds.
292    pub fn logical_time_us(&self) -> u64 {
293        self.logical_time_us
294    }
295
296    /// Advance logical time by the given microseconds.
297    pub fn advance_time_us(&mut self, delta_us: u64) {
298        self.logical_time_us += delta_us;
299    }
300
301    /// Set logical time to an absolute value.
302    pub fn set_time_us(&mut self, time_us: u64) {
303        self.logical_time_us = time_us;
304    }
305
306    /// Record an event in the event log.
307    pub fn log_event(&mut self, subsystem: Subsystem, description: impl Into<String>) -> SeqNo {
308        self.event_log
309            .record_simple(self.logical_time_us, subsystem, description)
310    }
311
312    /// Record an event with a payload.
313    pub fn log_event_with_payload(
314        &mut self,
315        subsystem: Subsystem,
316        description: impl Into<String>,
317        payload: Vec<u8>,
318    ) -> SeqNo {
319        self.event_log.record(
320            self.logical_time_us,
321            subsystem,
322            description.into(),
323            Some(payload),
324        )
325    }
326
327    /// Get a reference to the event log.
328    pub fn event_log(&self) -> &SimEventLog {
329        &self.event_log
330    }
331
332    /// Take ownership of the event log (consumes the context).
333    pub fn into_event_log(self) -> SimEventLog {
334        self.event_log
335    }
336}
337
338impl fmt::Debug for SimContext {
339    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340        f.debug_struct("SimContext")
341            .field("seed", &self.seed)
342            .field("logical_time_us", &self.logical_time_us)
343            .field("events_recorded", &self.event_log.len())
344            .finish()
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351
352    #[test]
353    fn test_context_creation() {
354        let ctx = SimContext::from_u64(42);
355        assert_eq!(ctx.seed(), VortexSeed::from_u64(42));
356        assert_eq!(ctx.logical_time_us(), 0);
357        assert!(ctx.event_log().is_empty());
358    }
359
360    #[test]
361    fn test_context_time_advance() {
362        let mut ctx = SimContext::from_u64(42);
363        ctx.advance_time_us(1000);
364        assert_eq!(ctx.logical_time_us(), 1000);
365        ctx.advance_time_us(500);
366        assert_eq!(ctx.logical_time_us(), 1500);
367    }
368
369    #[test]
370    fn test_context_event_logging() {
371        let mut ctx = SimContext::from_u64(42);
372        ctx.advance_time_us(100);
373        let seq1 = ctx.log_event(Subsystem::Fs, "open /data/wal.log");
374        ctx.advance_time_us(50);
375        let seq2 = ctx.log_event(Subsystem::Fs, "write 4096 bytes");
376
377        assert_eq!(seq1, SeqNo(0));
378        assert_eq!(seq2, SeqNo(1));
379        assert_eq!(ctx.event_log().len(), 2);
380        assert_eq!(ctx.event_log().entries()[0].timestamp_us, 100);
381        assert_eq!(ctx.event_log().entries()[1].timestamp_us, 150);
382    }
383
384    #[test]
385    fn test_context_deterministic_rngs() {
386        let ctx1 = SimContext::from_u64(42);
387        let ctx2 = SimContext::from_u64(42);
388
389        let mut rng1 = ctx1.rng_for("fs");
390        let mut rng2 = ctx2.rng_for("fs");
391        let seq1: Vec<u64> = (0..100).map(|_| rng1.next_u64()).collect();
392        let seq2: Vec<u64> = (0..100).map(|_| rng2.next_u64()).collect();
393        assert_eq!(seq1, seq2);
394    }
395
396    #[test]
397    fn test_context_different_domains_different_rngs() {
398        let ctx = SimContext::from_u64(42);
399        let mut fs_rng = ctx.rng_for("fs");
400        let mut net_rng = ctx.rng_for("net");
401        assert_ne!(fs_rng.next_u64(), net_rng.next_u64());
402    }
403
404    #[test]
405    fn test_event_log_diff_identical() {
406        let mut log1 = SimEventLog::new();
407        let mut log2 = SimEventLog::new();
408        log1.record_simple(0, Subsystem::Fs, "open file");
409        log1.record_simple(100, Subsystem::Fs, "write data");
410        log2.record_simple(0, Subsystem::Fs, "open file");
411        log2.record_simple(100, Subsystem::Fs, "write data");
412        assert!(SimEventLog::diff(&log1, &log2).is_none());
413    }
414
415    #[test]
416    fn test_event_log_diff_content_mismatch() {
417        let mut log1 = SimEventLog::new();
418        let mut log2 = SimEventLog::new();
419        log1.record_simple(0, Subsystem::Fs, "open file A");
420        log2.record_simple(0, Subsystem::Fs, "open file B");
421
422        let d = SimEventLog::diff(&log1, &log2).unwrap();
423        assert_eq!(d.seq, SeqNo(0));
424        assert!(matches!(d.kind, DivergenceKind::ContentMismatch));
425    }
426
427    #[test]
428    fn test_event_log_diff_length_mismatch() {
429        let mut log1 = SimEventLog::new();
430        let mut log2 = SimEventLog::new();
431        log1.record_simple(0, Subsystem::Fs, "open file");
432        log1.record_simple(100, Subsystem::Fs, "write data");
433        log2.record_simple(0, Subsystem::Fs, "open file");
434
435        let d = SimEventLog::diff(&log1, &log2).unwrap();
436        assert_eq!(d.seq, SeqNo(1));
437        assert!(matches!(
438            d.kind,
439            DivergenceKind::LengthMismatch { len_a: 2, len_b: 1 }
440        ));
441    }
442
443    #[test]
444    fn test_event_log_diff_display() {
445        let mut log1 = SimEventLog::new();
446        let mut log2 = SimEventLog::new();
447        log1.record_simple(0, Subsystem::Executor, "wake task A");
448        log2.record_simple(0, Subsystem::Executor, "wake task B");
449
450        let d = SimEventLog::diff(&log1, &log2).unwrap();
451        let display = format!("{d}");
452        assert!(display.contains("Divergence"));
453        assert!(display.contains("task A"));
454        assert!(display.contains("task B"));
455    }
456}