Skip to main content

detcore/
preemptions.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9//! A datatype to abstract a record of thread preemptions, as generated during chaos mode execution.
10
11pub mod boundary_recording;
12pub mod callback_history;
13mod record_serde;
14mod reserved_input;
15
16use std::collections::BTreeMap;
17use std::fs::File;
18use std::iter::FromIterator;
19use std::path::Path;
20use std::path::PathBuf;
21
22use chrono::DateTime;
23use chrono::Utc;
24use detcore_model::collections::ReplayCursor;
25use detcore_model::host_input::HostInputSet;
26pub use reserved_input::RecordAdmissionRefusal;
27pub(crate) use reserved_input::ReservedInputLayout;
28pub use reserved_input::ReservedInputMetadata;
29pub use reserved_input::read_recorded_reserved_input;
30pub use reserved_input::validate_config_replay_inputs;
31use serde::Deserialize;
32use serde::Serialize;
33use tracing::trace;
34
35use crate::resources::ChaosEpochTransition;
36use crate::scheduler::Priority;
37use crate::scheduler::runqueue::DEFAULT_PRIORITY;
38use crate::scheduler::runqueue::FIRST_PRIORITY;
39use crate::scheduler::runqueue::LAST_PRIORITY;
40use crate::scheduler::runqueue::is_ordinary_priority;
41use crate::types::DetTid;
42use crate::types::LogicalTime;
43use crate::types::SchedEvent;
44
45/// A record of all the preemptions and other scheduling events that occur during execution.
46#[derive(PartialEq, Default, Debug, Eq, Clone, Hash)]
47pub struct PreemptionRecord {
48    // TODO(T106838933): switch the keys from DetTid to Pedigree.
49    /// A sorted list of end-of-timeslice preemption times for each thread.
50    per_thread: BTreeMap<DetTid, ThreadHistory>,
51    global: Vec<SchedEvent>,
52    /// The virtual-time epoch of the run that produced this record.
53    ///
54    /// Every `LogicalTime` above is an absolute virtual instant, i.e. an offset
55    /// from this epoch, so the record only describes a run whose clock starts
56    /// here. A replay started from any other epoch would see every recorded
57    /// timeslice end shifted by the difference. `None` for records written
58    /// before the epoch was stored, and for records synthesized in memory.
59    epoch: Option<DateTime<Utc>>,
60    /// The `--pipe-wake-latency` of the run that produced this record
61    /// (<https://github.com/rrnewton/hermit/issues/4380>). A hand-off moves
62    /// turns and their charged time, so a replay under another value cannot
63    /// reproduce the run, and is refused. `None` when the option was off,
64    /// which writes the record exactly as before the field existed, and for
65    /// records written before it. `Some(0)` is distinct from `None`.
66    pipe_wake_latency: Option<u64>,
67    /// Whether Detcore's signal-state copy was on in the run that produced
68    /// this record (`Config::signal_state_copy`, A5 step 2d). With it on,
69    /// some signal decisions read the copy, and ptrace reports each handler
70    /// return, which can move a preemption in the cases docs/SIGNAL_STATE_CHECK.md
71    /// lists, so a replay under the other setting is refused. `None` for
72    /// records written before the field existed, and for records synthesized
73    /// in memory, which are replayed under either setting.
74    signal_state_copy: Option<bool>,
75    /// Whether the run that produced this record charged preemptions at most
76    /// the time their slice ran (`--cap-preemption-charge`). Turn charges
77    /// move timers and timeouts, so a replay under the other setting cannot
78    /// reproduce the run, and is refused. Off writes the record exactly as
79    /// before the field existed.
80    cap_preemption_charge: bool,
81    /// The `--max-timeslice` the run that produced this record drew under
82    /// `--chaos-timeslice-draw=run` (`Config::chaos_timeslice`), in virtual
83    /// nanoseconds. A replay of the record runs under it (`hermit run` takes
84    /// it unless the replay gives another `--max-timeslice`, which is
85    /// refused). `None`, which writes nothing, for a run that drew none and
86    /// for records written before the field existed.
87    chaos_timeslice: Option<u64>,
88    /// How the run that produced this record numbered its guest's process
89    /// and thread IDs (`Config::guest_ids`), when no PID namespace of its own
90    /// did (`--no-namespace`). Every thread above is keyed by those IDs, so a
91    /// replay under another numbering is refused. `None` for a run its PID
92    /// namespace numbered, which writes the record exactly as before the
93    /// field existed, and for records written before it.
94    guest_ids: Option<detcore_model::pid::GuestIds>,
95    /// Set in a record written by a preemption replay that left a change point
96    /// (and its slowdown-epoch transitions) out of it because it would not
97    /// have come after the thread's last one: the global event count where it
98    /// first did. A record's rows must increase (`Self::validate`), so such a
99    /// change point cannot be written, and the record no longer holds the
100    /// priority the thread had at that instant: it is not an exact record of
101    /// the run, and faithful replay refuses it. That happens to a replay that left the
102    /// record it replays, and to a replay of an edited record
103    /// (`--replay-edited-preemptions`), which keeps a slice that ends at once.
104    /// Absence preserves legacy admission; it does not prove historical
105    /// completeness. Newly marked JSON uses a mandatory envelope that old
106    /// readers reject, and marked non-human serialization is refused.
107    replay_departed_at: Option<u64>,
108    /// The actual private-input layout of the producing target images. This
109    /// standalone JSON member is deliberately absent from ThreadHistory/RPC.
110    /// Unknown values are retained for editing and non-Chosen replay.
111    reserved_input: ReservedInputMetadata,
112    /// Actual callback expirations associated with existing priority-history rows.
113    /// Absent in old/edited recordings; never inferred from time or branch counts.
114    callback_checkpoints: BTreeMap<DetTid, Vec<callback_history::Checkpoint>>,
115    /// The `--rng-seed` of the run that produced this record.
116    ///
117    /// It decides what the guest reads as randomness, so the recorded schedule
118    /// only describes a guest that read the same values: `--seed` sets it too, so
119    /// a replay at another `--seed` would otherwise hand the guest other values.
120    /// `None` for records written before the field existed, and for records
121    /// synthesized in memory.
122    rng_seed: Option<u64>,
123    /// Whether the run that produced this record charged a run of
124    /// consecutive expired timed waits one scheduler turn
125    /// (`Scheduler::expired_wait_runs_charged_once`): a run under
126    /// `--expired-wait-charge=once`, by default one outside race search.
127    /// `false`, which writes the record exactly as before the field existed
128    /// and is what a record without it means, is the rule of every run
129    /// before it and of `each` runs: each expired wait is charged. A replay
130    /// that leaves `--expired-wait-charge` unset charges as its record says,
131    /// so that a chaos recording replayed without `--chaos` (or a plain one
132    /// replayed with it) keeps its recorded virtual times.
133    expired_wait_runs_charged_once: bool,
134    /// The host files the recorded run's guest opened
135    /// (`detcore_model::host_input::HostInputSet`), so that a replay can
136    /// report one that changed since (`HERMIT_HOST_INPUT_CHANGED_SINCE_RECORDING`).
137    /// Information only: a replay never refuses or alters its schedule for
138    /// it. `None` for records written before the field existed, and for
139    /// records synthesized in memory.
140    host_inputs: Option<HostInputSet>,
141    /// The store-exclusives that failed in the run that produced this record
142    /// (aarch64, `STREX_FAIL_SPEC`). Each retried a load/store exclusive loop
143    /// and so added retired branches the run's preemption points count, so a
144    /// replay of this record may diverge; a replay warns at its start when it
145    /// is nonzero. Zero, and absent, for a run that counted none, for other
146    /// architectures, and for records written before the field existed.
147    store_exclusive_failures: HostObservedCount,
148    /// The record's format ([`PREEMPTION_RECORD_FORMAT`]): `None` for format 1.
149    format_version: Option<u32>,
150    /// Every jump of global time to a deadline that the scheduler made because
151    /// nothing else could run (`TimeSkip`), in order; absent when there was
152    /// none. A record of format 2 or later lists all its jumps, so a replay of
153    /// it makes exactly those jumps, each where the recording made it
154    /// (`TimeSkip::event`), and computes none of its own. A record of format 1
155    /// (written before jumps were recorded) replays as before: it makes no
156    /// idle-poller jump, and a FIFO open's fast-forward recomputes its jump.
157    /// Analyze and bisect carry a source record's jumps into the trial
158    /// schedules they splice from it (`with_time_skips`).
159    time_skips: Option<Vec<TimeSkip>>,
160}
161
162/// A count a record's run observed on its host rather than decided: it is
163/// kept in the record but compares and hashes as equal whatever its value,
164/// so that records of two runs of one schedule stay equal.
165#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize)]
166#[serde(transparent)]
167struct HostObservedCount(u64);
168
169impl HostObservedCount {
170    fn is_zero(&self) -> bool {
171        self.0 == 0
172    }
173}
174
175impl PartialEq for HostObservedCount {
176    fn eq(&self, _: &Self) -> bool {
177        true
178    }
179}
180
181impl Eq for HostObservedCount {}
182
183impl std::hash::Hash for HostObservedCount {
184    fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
185}
186
187/// The newest preemption-record format this Hermit writes and reads
188/// (`PreemptionRecord::format_version`), one counter for every format change:
189/// a record of format N has every property of the formats before it.
190///
191/// - Format 1 has no version field: every record written before the field
192///   existed.
193/// - Format 2 ([`TIME_SKIPS_FORMAT`]) lists every jump of global time its run
194///   made (`time_skips`, absent when there was none).
195/// - Format 3 ([`SWITCH_IN_CHARGE_FORMAT`]) was written by a run that charged
196///   every stretch of guest code a turn, so its replay charges a replayed
197///   switch-in under which guest code runs.
198///
199/// A record of a newer format is refused (`PreemptionRecord::validate_format`).
200pub const PREEMPTION_RECORD_FORMAT: u32 = SWITCH_IN_CHARGE_FORMAT;
201
202/// The format from which a record lists all its jumps of global time.
203pub const TIME_SKIPS_FORMAT: u32 = 2;
204
205/// The format from which a schedule replay charges a scheduler turn for every
206/// replayed switch to a thread that then runs guest code
207/// (`Scheduler::bump_global_time`), as the run that wrote the record charged
208/// the yield turn the switch stands for. A run charges every such stretch when
209/// it follows no record, or follows one of this format. A replay of an older
210/// record charges as before: its writer cannot say whether its own run
211/// followed a schedule, and so left those stretches uncharged.
212///
213/// This restores the switch-in charge when guest events retire. A bare
214/// `TraceReplay` turn does not become `previous_turn_tid`: main's ordinary
215/// turn rule continues to decide subsequent signal charges. This format alone
216/// does not claim equality of every record/replay clock observation.
217pub const SWITCH_IN_CHARGE_FORMAT: u32 = 3;
218
219/// A record whose format, or jumps of global time, this Hermit refuses to
220/// replay (`PreemptionRecord::validate_format`): a policy refusal, not a
221/// malformed file.
222#[derive(Debug)]
223pub struct RecordFormatRefusal(pub String);
224
225impl std::fmt::Display for RecordFormatRefusal {
226    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
227        f.write_str(&self.0)
228    }
229}
230
231impl std::error::Error for RecordFormatRefusal {}
232
233/// Whether `PreemptionRecord::time_skips` has nothing to write.
234fn lists_no_time_skips(skips: &Option<Vec<TimeSkip>>) -> bool {
235    skips.as_ref().is_none_or(Vec::is_empty)
236}
237
238/// Why the scheduler moved global time ahead to a deadline.
239#[derive(PartialEq, Debug, Eq, Clone, Copy, Hash, Serialize, Deserialize)]
240pub enum TimeSkipReason {
241    /// A FIFO open waits for a peer while every other thread is an idle
242    /// guest-state poller or parked.
243    FifoOpenAlone,
244    /// A guest-state poller retried without progress while every other thread
245    /// is an idle guest-state poller or parked.
246    IdleGuestPollers,
247}
248
249/// One jump of global time. A replay of the record makes it where the
250/// recording did, as replay drives context switches, and checks it: its target
251/// must still lie ahead, and no finite deadline the replay knows, then or
252/// later, may come before it, or the replay reports a desync
253/// (`Scheduler::make_recorded_time_skip`).
254#[derive(PartialEq, Debug, Eq, Clone, Copy, Hash, Serialize, Deserialize)]
255pub struct TimeSkip {
256    /// Where in the recorded stream the jump falls: the number of stored
257    /// records before it (global events in the original format, and every
258    /// record, boundaries included, in the boundary protocol). Taken where the
259    /// jump is made, at the start of a scheduler pass, when every thread is
260    /// parked, so no event can race it.
261    pub event: u64,
262    /// The exact deadline global time moved to.
263    pub target: LogicalTime,
264    /// Why nothing else could run.
265    pub reason: TimeSkipReason,
266}
267
268impl std::fmt::Display for PreemptionRecord {
269    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
270        let str = serde_json::to_string(&self).unwrap();
271        write!(f, "{}", str)
272    }
273}
274
275impl PreemptionRecord {
276    /// TODO: This is not ideal because PreemptionRecord shouldn't be doing double duty as an eventlog.
277    pub fn from_sched_events(events: Vec<SchedEvent>) -> Self {
278        Self {
279            per_thread: Default::default(),
280            global: events,
281            epoch: None,
282            pipe_wake_latency: None,
283            signal_state_copy: None,
284            cap_preemption_charge: false,
285            chaos_timeslice: None,
286            guest_ids: None,
287            replay_departed_at: None,
288            reserved_input: ReservedInputMetadata::default(),
289            callback_checkpoints: BTreeMap::new(),
290            rng_seed: None,
291            expired_wait_runs_charged_once: false,
292            host_inputs: None,
293            store_exclusive_failures: HostObservedCount::default(),
294            format_version: None,
295            time_skips: None,
296        }
297    }
298
299    /// Whether the run this record came from charged a run of expired timed
300    /// waits one turn (`false` for each wait charged, and for records that
301    /// do not say).
302    pub fn expired_wait_runs_charged_once(&self) -> bool {
303        self.expired_wait_runs_charged_once
304    }
305
306    /// The same record, stating how the run it came from charged runs of
307    /// expired timed waits.
308    pub fn with_expired_wait_runs_charged_once(mut self, once: bool) -> Self {
309        self.expired_wait_runs_charged_once = once;
310        self
311    }
312
313    /// The record a replay of only the first `len` scheduled events of this
314    /// one follows: those events, and the callback checkpoints they reach.
315    ///
316    /// A checkpoint names the callback that ended one of a thread's slices
317    /// and the thread's progress there (the sum of its events' counts, as
318    /// replay counts it, [`callback_history::Progress`]). The checkpoints of
319    /// threads the prefix runs, up to the progress each thread reaches in it,
320    /// end the prefix's slices where the recording did, so a replay of the
321    /// prefix keeps the software-expiry turns they mark. A schedule replayed
322    /// without them has no record of those turns, and its virtual time drifts
323    /// from the recording's. The scheduler runs freely after the prefix, so
324    /// later checkpoints are dropped, and so are those of threads the prefix
325    /// does not run, progress-0 ones included.
326    ///
327    /// Progress alone places a checkpoint, so one taken at exactly the
328    /// progress a thread reaches at the prefix end is kept: it ended the
329    /// thread's last slice in the prefix. Another one at the same progress,
330    /// from a later callback of the same handler after other threads ran past
331    /// the prefix, is kept too, and the replay applies it only when its
332    /// recorded time and branch count are reached.
333    ///
334    /// The thread that runs the prefix's last event is the exception while
335    /// another event follows: its checkpoints at the progress that event
336    /// reaches end its slice at the prefix's own end, which decides what runs
337    /// next, so they belong to the event after the prefix and are dropped. A
338    /// replay of the prefix then lets that thread run on, as it does past any
339    /// event the prefix leaves out. A prefix of the whole schedule keeps the
340    /// record's checkpoints.
341    ///
342    /// The per-thread histories stay whole, for the rows the checkpoints
343    /// name; a schedule replay reads only the events and the checkpoints.
344    pub fn schedule_prefix(&self, len: usize) -> PreemptionRecord {
345        let PreemptionRecord {
346            per_thread,
347            global,
348            epoch,
349            pipe_wake_latency,
350            signal_state_copy,
351            cap_preemption_charge,
352            chaos_timeslice,
353            guest_ids,
354            reserved_input,
355            callback_checkpoints,
356            rng_seed,
357            expired_wait_runs_charged_once,
358            host_inputs,
359            store_exclusive_failures: _,
360            replay_departed_at,
361            format_version,
362            time_skips,
363        } = self;
364        let followed = len < global.len();
365        let global = global[..len.min(global.len())].to_vec();
366        let mut reached = BTreeMap::<DetTid, callback_history::Progress>::new();
367        for event in &global {
368            reached
369                .entry(event.dettid)
370                .or_default()
371                .observe_event(event);
372        }
373        // The thread whose slice is still running at the prefix's end: the
374        // last event's, when that event advances it and another one follows.
375        let running = global
376            .last()
377            .filter(|last| followed && last.count > 0)
378            .map(|last| last.dettid);
379        let callback_checkpoints = callback_checkpoints
380            .iter()
381            .filter_map(|(tid, points)| {
382                let reached = reached.get(tid)?.count;
383                let within = |progress: u64| {
384                    if running == Some(*tid) {
385                        progress < reached
386                    } else {
387                        progress <= reached
388                    }
389                };
390                let kept: Vec<_> = points
391                    .iter()
392                    .filter(|point| within(point.origin.progress))
393                    .cloned()
394                    .collect();
395                (!kept.is_empty()).then_some((*tid, kept))
396            })
397            .collect();
398        PreemptionRecord {
399            per_thread: per_thread.clone(),
400            global,
401            epoch: *epoch,
402            pipe_wake_latency: *pipe_wake_latency,
403            signal_state_copy: *signal_state_copy,
404            cap_preemption_charge: *cap_preemption_charge,
405            chaos_timeslice: *chaos_timeslice,
406            guest_ids: *guest_ids,
407            reserved_input: reserved_input.clone(),
408            callback_checkpoints,
409            rng_seed: *rng_seed,
410            expired_wait_runs_charged_once: *expired_wait_runs_charged_once,
411            host_inputs: host_inputs.clone(),
412            // A count of this run's host, not part of the preemptions.
413            store_exclusive_failures: HostObservedCount::default(),
414            // A prefix of an inexact record is inexact too.
415            replay_departed_at: *replay_departed_at,
416            format_version: *format_version,
417            // The jumps the prefix reaches, in stream order, as analyze and
418            // bisect carry them (`hermit-cli`'s `time_skip_carry`): one at its
419            // end still waits there for a FIFO open's poll, and one beyond it
420            // was never reached.
421            time_skips: time_skips
422                .as_ref()
423                .map(|skips| {
424                    skips
425                        .iter()
426                        .take_while(|skip| {
427                            usize::try_from(skip.event).is_ok_and(|event| event <= len)
428                        })
429                        .copied()
430                        .collect::<Vec<_>>()
431                })
432                .filter(|skips| !skips.is_empty()),
433        }
434    }
435
436    /// The same record with `events` as its schedule, or `None` if its
437    /// callback checkpoints do not fit those events (see [`Self::validate`]).
438    /// For a schedule that replayed this record's events and went on past
439    /// them: its own recording keeps no checkpoints, since a replay records
440    /// none, but the replayed part ended its slices where this record did.
441    pub fn with_schedule(&self, events: Vec<SchedEvent>) -> Option<PreemptionRecord> {
442        let mut record = self.clone();
443        record.global = events;
444        record.validate().is_ok().then_some(record)
445    }
446
447    /// Attach the metadata of two interpolation inputs to `events`.
448    ///
449    /// The caller first reconciles the existing guest-ID, signal-copy and
450    /// other option policies. An exact source schedule keeps that source's
451    /// complete record, including callback checkpoints and time jumps. Both
452    /// formats must agree: a single output cannot follow two charging rules.
453    ///
454    /// A genuinely mixed order has no established mapping for either source's
455    /// jumps or checkpoints, so refuse those inputs. For jump/checkpoint-free
456    /// inputs, every remaining metadata field must agree. Keep those fields,
457    /// but clear the per-thread priority histories, as an event-order edit
458    /// follows its global stream rather than either original priority history.
459    /// Host-observed failure counts must agree too; retain their warning,
460    /// without claiming that an unrun edit has measured that many failures.
461    pub fn interpolate_events(
462        &self,
463        other: &Self,
464        events: Vec<SchedEvent>,
465    ) -> Result<Self, String> {
466        self.validate()?;
467        other.validate()?;
468        self.validate_combined_reserved_inputs(other)
469            .map_err(|refusal| refusal.to_string())?;
470        if self.format_version != other.format_version {
471            return Err(format!(
472                "cannot interpolate record formats {:?} and {:?}: their replay rules differ; record both inputs with the same Hermit version and timing format",
473                self.format_version, other.format_version
474            ));
475        }
476        if events == self.global {
477            return Ok(self.clone());
478        }
479        if events == other.global {
480            return Ok(other.clone());
481        }
482        if !lists_no_time_skips(&self.time_skips) || !lists_no_time_skips(&other.time_skips) {
483            return Err(
484                "cannot interpolate a mixed schedule with recorded time jumps: no source's complete event order matches the result; replay an unchanged endpoint instead"
485                    .to_owned(),
486            );
487        }
488        if !self.callback_checkpoints.is_empty() || !other.callback_checkpoints.is_empty() {
489            return Err(
490                "cannot interpolate a mixed schedule with callback checkpoints: no mapping to the edited event order is established; replay an unchanged endpoint instead"
491                    .to_owned(),
492            );
493        }
494        let metadata = |source: &Self| {
495            let mut record = source.clone();
496            record.global.clear();
497            record.per_thread.clear();
498            record
499        };
500        let mut record = metadata(self);
501        let mut other_metadata = metadata(other);
502        if !matches!(
503            self.guest_ids,
504            Some(detcore_model::pid::GuestIds::Chosen { .. })
505        ) && !matches!(
506            other.guest_ids,
507            Some(detcore_model::pid::GuestIds::Chosen { .. })
508        ) {
509            // Opaque provenance adds no admission rule to non-Chosen edits.
510            // Keep the selected source's value without interpreting a future
511            // descriptor or demanding that previously ignored fields agree.
512            other_metadata.reserved_input = record.reserved_input.clone();
513        }
514        // HostObservedCount deliberately compares equal for ordinary replay
515        // comparisons, so interpolation must check its stored value explicitly.
516        if record != other_metadata
517            || self.store_exclusive_failures() != other.store_exclusive_failures()
518        {
519            return Err(
520                "cannot interpolate a mixed schedule with incompatible recording metadata (epoch, charging policy, RNG, timeslice or host inputs); record inputs with matching metadata or replay an unchanged endpoint"
521                    .to_owned(),
522            );
523        }
524        record.global = events;
525        record.validate()?;
526        Ok(record)
527    }
528
529    /// The `--max-timeslice` the run that produced this record drew under
530    /// `--chaos-timeslice-draw=run`, if it drew one.
531    pub fn chaos_timeslice(&self) -> Option<u64> {
532        self.chaos_timeslice
533    }
534
535    /// The same record, stating the timeslice its run drew.
536    pub fn with_chaos_timeslice(mut self, timeslice: Option<u64>) -> Self {
537        self.chaos_timeslice = timeslice;
538        self
539    }
540
541    /// The same record, listing `skips` as its jumps of global time, in format
542    /// 2 at least when there are any. Analyze and bisect carry a source
543    /// record's jumps into a trial schedule spliced from its prefix.
544    pub fn with_time_skips(mut self, skips: Vec<TimeSkip>) -> Self {
545        self.time_skips = (!skips.is_empty()).then_some(skips);
546        if self.time_skips.is_some() && !self.lists_its_time_skips() {
547            self.format_version = Some(TIME_SKIPS_FORMAT);
548        }
549        self
550    }
551
552    /// The same record in the newest format ([`PREEMPTION_RECORD_FORMAT`]),
553    /// which a record from a run of this Hermit has.
554    pub fn with_current_format(self) -> Self {
555        self.with_format(Some(PREEMPTION_RECORD_FORMAT))
556    }
557
558    /// The same record in `format` (`None` for format 1). Analyze and bisect
559    /// require one shared timing format across the recordings they read
560    /// (`hermit-cli`'s `time_skip_carry`).
561    pub fn with_format(mut self, format: Option<u32>) -> Self {
562        self.format_version = format;
563        self
564    }
565
566    /// The record's format: `None` for format 1.
567    pub fn format_version(&self) -> Option<u32> {
568        self.format_version
569    }
570
571    /// Whether a schedule replay of this record charges a replayed switch-in
572    /// under which guest code runs (format 3 or later,
573    /// [`SWITCH_IN_CHARGE_FORMAT`]).
574    pub fn charges_replayed_switch_ins(&self) -> bool {
575        self.format_version
576            .is_some_and(|format| format >= SWITCH_IN_CHARGE_FORMAT)
577    }
578
579    /// Whether the record lists every jump of global time its run made
580    /// (format 2 or later), so that a replay makes exactly those.
581    pub fn lists_its_time_skips(&self) -> bool {
582        self.format_version
583            .is_some_and(|format| format >= TIME_SKIPS_FORMAT)
584    }
585
586    /// The same record, stating the `--pipe-wake-latency` of the trials it
587    /// came from. Every record analyze, minimize or bisect writes from trials
588    /// carries it, so that a replay under another value is refused.
589    pub fn with_pipe_wake_latency(mut self, latency: Option<u64>) -> Self {
590        self.pipe_wake_latency = latency;
591        self
592    }
593
594    /// The `--pipe-wake-latency` this record was produced under (`None` for
595    /// off).
596    pub fn pipe_wake_latency(&self) -> Option<u64> {
597        self.pipe_wake_latency
598    }
599
600    /// The virtual-time epoch this record was produced under, if it was stored.
601    pub fn epoch(&self) -> Option<DateTime<Utc>> {
602        self.epoch
603    }
604
605    /// Whether the signal-state copy was on in the run this record came
606    /// from, if the record says.
607    pub fn signal_state_copy(&self) -> Option<bool> {
608        self.signal_state_copy
609    }
610
611    /// The host files the recorded run's guest opened, if the record says.
612    pub fn host_inputs(&self) -> Option<&HostInputSet> {
613        self.host_inputs.as_ref()
614    }
615
616    /// The store-exclusives that failed in the run this record came from
617    /// (zero where none were counted).
618    pub fn store_exclusive_failures(&self) -> u64 {
619        self.store_exclusive_failures.0
620    }
621
622    /// A retained source event count where a replay omitted a change point.
623    /// After transformations it need not index this record's global events.
624    pub fn replay_departed_at(&self) -> Option<u64> {
625        self.replay_departed_at
626    }
627
628    /// Refuse a known omission before replay or analysis can certify it.
629    /// Unmarked legacy records retain their existing admission policy; their
630    /// absence of this field is not proof of historical completeness.
631    pub fn require_faithful_replay(&self) -> anyhow::Result<()> {
632        if let Some(event) = self.replay_departed_at {
633            anyhow::bail!(
634                "HERMIT_INCOMPLETE_REPLAY_RECORD: replay_departed_at={event}: this record omits \
635                 a replay change point and cannot certify faithful replay. Re-record a complete \
636                 execution; --replay-edited-preemptions permits only non-verifying history exploration."
637            );
638        }
639        Ok(())
640    }
641
642    /// Retain an omission observed in a source record, never clearing one.
643    /// When sources differ, the smaller count is only a retained source
644    /// observation: it is neither a common causal order nor an event index
645    /// in a transformed output. Admission depends only on its presence.
646    pub fn retain_replay_departure(mut self, source: Option<u64>) -> Self {
647        self.replay_departed_at = match (self.replay_departed_at, source) {
648            (Some(a), Some(b)) => Some(a.min(b)),
649            (a, b) => a.or(b),
650        };
651        self
652    }
653
654    /// The same record, stating whether the copy was on in the trials it
655    /// came from (`None` for unknown). Every record analyze, minimize or
656    /// bisect writes from trials carries it, as it carries the pipe-wake
657    /// latency.
658    pub fn with_signal_state_copy(mut self, on: Option<bool>) -> Self {
659        self.signal_state_copy = on;
660        self
661    }
662
663    /// The same record, stating whether the trials it came from capped
664    /// preemption charges, as it states the pipe-wake latency.
665    pub fn with_cap_preemption_charge(mut self, on: bool) -> Self {
666        self.cap_preemption_charge = on;
667        self
668    }
669
670    /// Whether this record was produced under `--cap-preemption-charge`.
671    pub fn cap_preemption_charge(&self) -> bool {
672        self.cap_preemption_charge
673    }
674
675    /// How the run this record came from numbered its guest, if no PID
676    /// namespace of its own did.
677    pub fn guest_ids(&self) -> Option<detcore_model::pid::GuestIds> {
678        self.guest_ids
679    }
680
681    /// The same record, retaining the guest numbering of schedules used to
682    /// construct it. Namespace numbering keeps the legacy omitted field.
683    pub fn with_guest_ids(mut self, ids: detcore_model::pid::GuestIds) -> Self {
684        self.guest_ids = (ids != detcore_model::pid::GuestIds::Namespace).then_some(ids);
685        self
686    }
687
688    /// Make a copy of everything in the `PreemptionRecord` in a public format.
689    pub fn extract_all(&self) -> BTreeMap<DetTid, ThreadHistory> {
690        self.per_thread.clone()
691    }
692
693    /// Inverse of from_vecs.
694    pub fn as_vecs(&self) -> BTreeMap<DetTid, Vec<(LogicalTime, Priority)>> {
695        let mut bt = BTreeMap::new();
696        for (tid, th) in &self.per_thread {
697            bt.insert(*tid, th.as_vec());
698        }
699        bt
700    }
701    /// TODO: provide a better way to create an empty PreemptionRecord but with entries for a given set of tids.
702    pub fn strip_contents(mut self) -> Self {
703        self.callback_checkpoints.clear();
704        for history in self.per_thread.values_mut() {
705            history.prio_changes = Vec::new();
706            history.preemption_rcbs = Vec::new();
707            history.chaos_epochs = Vec::new();
708            history.final_prio = 1000;
709        }
710        self.global = Vec::new();
711        self
712    }
713
714    /// Iterates over each sched event and applies function event -> *event yielding resulting iterator as new events
715    /// This effectively moves content of global allowing to split or delete events based on the function provided
716    pub fn split_map<F, R>(&mut self, splitter: F)
717    where
718        R: IntoIterator<Item = SchedEvent>,
719        F: Fn(SchedEvent, &ReplayCursor<SchedEvent>) -> R,
720    {
721        self.callback_checkpoints.clear();
722        let mut result = Vec::new();
723        let global = std::mem::take(&mut self.global);
724        let mut cursor = ReplayCursor::from_iter(global);
725
726        while let Some(event) = cursor.next() {
727            for new_event in splitter(event, &cursor) {
728                result.push(new_event);
729            }
730        }
731        self.global = result;
732    }
733
734    /// Iterate over the schedevents.
735    pub fn schedevents_iter_mut(&mut self) -> std::slice::IterMut<'_, SchedEvent> {
736        self.callback_checkpoints.clear();
737        self.global.iter_mut()
738    }
739
740    /// Leave only the per-thread preemption records, not the global schedule.
741    pub fn preemptions_only(&mut self) {
742        self.callback_checkpoints.clear();
743        self.global.clear();
744    }
745
746    /// Make a copy just of the preemptions (cheap) rather than the whole schedule trace (expensive)
747    pub fn clone_preemptions_only(&self) -> Self {
748        PreemptionRecord {
749            per_thread: self.per_thread.clone(),
750            global: Vec::new(),
751            epoch: self.epoch,
752            pipe_wake_latency: self.pipe_wake_latency,
753            signal_state_copy: self.signal_state_copy,
754            cap_preemption_charge: self.cap_preemption_charge,
755            chaos_timeslice: self.chaos_timeslice,
756            guest_ids: self.guest_ids,
757            // A record derived from an inexact one is inexact too.
758            replay_departed_at: self.replay_departed_at,
759            reserved_input: self.reserved_input.clone(),
760            callback_checkpoints: BTreeMap::new(),
761            rng_seed: self.rng_seed,
762            expired_wait_runs_charged_once: self.expired_wait_runs_charged_once,
763            host_inputs: self.host_inputs.clone(),
764            // A count of this run's host, not part of the preemptions.
765            store_exclusive_failures: HostObservedCount::default(),
766            format_version: None,
767            time_skips: None,
768        }
769    }
770
771    /// Read-only access to the raw event schedule.
772    pub fn schedevents(&self) -> &Vec<SchedEvent> {
773        &self.global
774    }
775
776    /// Returns true if the schedule trace (SchedEvent) record is nonempty.
777    pub fn contains_schedevents(&self) -> bool {
778        !self.global.is_empty()
779    }
780
781    /// Convert from a flat vector representation (Time,Priority_AFTER_Time) into the internal representation.
782    /// The very first timeslice should have a zero timestamp, but it is ignored if nonzero.
783    pub fn from_vecs(bt: &BTreeMap<DetTid, Vec<(LogicalTime, Priority)>>) -> Self {
784        let mut bt2 = BTreeMap::new();
785        for (tid, vec) in bt {
786            let th = if vec.is_empty() {
787                ThreadHistory {
788                    final_prio: DEFAULT_PRIORITY,
789                    prio_changes: Vec::new(),
790                    preemption_rcbs: Vec::new(),
791                    chaos_epochs: Vec::new(),
792                }
793            } else {
794                // We could insist on this invariant, but it's a little more flexible not to:
795                // assert!(vec.get(0).unwrap().per_thread.is_zero());
796                let (_final_end, final_prio) = vec.last().unwrap();
797                let final_prio = *final_prio;
798
799                let mut prio_changes = Vec::new();
800                let mut it = vec.iter().peekable();
801                while let Some((_this_ns, this_p)) = it.next() {
802                    if let Some((next_ns, _next_p)) = it.peek() {
803                        prio_changes.push((*next_ns, *this_p));
804                    } else {
805                        // Don't need the last one, because we've already used it's
806                        // timestamp, and retrieved final_prio.
807                        break;
808                    }
809                }
810                assert!(final_prio >= FIRST_PRIORITY);
811                assert!(final_prio <= LAST_PRIORITY);
812                ThreadHistory {
813                    final_prio,
814                    prio_changes,
815                    preemption_rcbs: Vec::new(),
816                    chaos_epochs: Vec::new(),
817                }
818            };
819            bt2.insert(*tid, th);
820        }
821        PreemptionRecord {
822            per_thread: bt2,
823            global: Vec::new(),
824            epoch: None,
825            pipe_wake_latency: None,
826            signal_state_copy: None,
827            cap_preemption_charge: false,
828            chaos_timeslice: None,
829            guest_ids: None,
830            replay_departed_at: None,
831            reserved_input: ReservedInputMetadata::default(),
832            callback_checkpoints: BTreeMap::new(),
833            rng_seed: None,
834            expired_wait_runs_charged_once: false,
835            host_inputs: None,
836            store_exclusive_failures: HostObservedCount::default(),
837            format_version: None,
838            time_skips: None,
839        }
840    }
841
842    /// The jumps of global time the recorder saw (`time_skips`), or `None`
843    /// when the record does not say.
844    pub fn time_skips(&self) -> Option<&[TimeSkip]> {
845        self.time_skips.as_deref()
846    }
847
848    pub(crate) fn callback_checkpoints(
849        &self,
850    ) -> &BTreeMap<DetTid, Vec<callback_history::Checkpoint>> {
851        &self.callback_checkpoints
852    }
853
854    /// Timestamp stripping preserves the event/progress relation, unlike an editor.
855    pub(crate) fn strip_event_times(&mut self) {
856        for event in &mut self.global {
857            event.end_time = None;
858        }
859    }
860
861    /// Extracts the inner global events.
862    pub fn into_global(self) -> Vec<SchedEvent> {
863        self.global
864    }
865
866    /// Save to disk.
867    pub fn write_to_disk(&self, path: &Path) -> Result<(), String> {
868        let mut str: String = self.to_string();
869        str.push('\n');
870        match File::create(path) {
871            Ok(mut file) => match std::io::Write::write_all(&mut file, str.as_bytes()) {
872                Ok(_) => Ok(()),
873                Err(err) => Err(format!(
874                    "Failed to write preemption record to file {:?}, error: {}",
875                    path, err
876                )),
877            },
878            Err(err) => Err(format!(
879                "Failed to create file for preemption record {:?}, error: {}",
880                path, err
881            )),
882        }
883    }
884
885    /// Perform internal invariant checks on the PreemptionRecord and return an error if
886    /// it is not well formed.
887    pub fn validate(&self) -> Result<(), String> {
888        self.validate_contents()?;
889        self.validate_format()
890    }
891
892    /// Check everything [`Self::validate`] does but the format
893    /// ([`Self::validate_format`]), whose failure is a refusal rather than a
894    /// malformed record, and which readers check separately for that reason.
895    pub fn validate_contents(&self) -> Result<(), String> {
896        self.validate_callback_checkpoints()?;
897        self.validate_histories()
898    }
899
900    /// Check the record's format and its jumps of global time: a format this
901    /// Hermit reads, with the list that format requires, in stream order, to
902    /// later and finite deadlines. A failure is a refusal of the record, not a
903    /// malformed file (`RecordFormatRefusal`).
904    pub fn validate_format(&self) -> Result<(), String> {
905        if let Some(skip) = self
906            .time_skips
907            .iter()
908            .flatten()
909            .find(|skip| skip.target.is_indefinite())
910        {
911            return Err(format!(
912                "time skip {skip:?} moves to the indefinite deadline, which nothing reaches"
913            ));
914        }
915        let skips = match &self.time_skips {
916            Some(skips) if skips.is_empty() => {
917                return Err("an empty list of time skips is written as no list".to_string());
918            }
919            skips => skips.as_deref().unwrap_or_default(),
920        };
921        let known = self
922            .format_version
923            .is_none_or(|format| (TIME_SKIPS_FORMAT..=PREEMPTION_RECORD_FORMAT).contains(&format));
924        if !known || (!skips.is_empty() && !self.lists_its_time_skips()) {
925            return Err(format!(
926                "record format {:?} with {} time skips: this Hermit reads formats 1 to \
927                 {PREEMPTION_RECORD_FORMAT}, and time skips need format {TIME_SKIPS_FORMAT} or later",
928                self.format_version,
929                skips.len()
930            ));
931        }
932        if let Some(pair) = skips
933            .windows(2)
934            .find(|pair| pair[1].event < pair[0].event || pair[1].target <= pair[0].target)
935        {
936            return Err(format!(
937                "time skips must come in stream order and move to later deadlines: {:?} then {:?}",
938                pair[0], pair[1]
939            ));
940        }
941        Ok(())
942    }
943
944    /// Check every thread's priority history.
945    fn validate_histories(&self) -> Result<(), String> {
946        for (tid, history) in &self.per_thread {
947            if !is_ordinary_priority(history.final_prio) {
948                return Err(format!(
949                    "final priority for thread {} invalid: {}",
950                    tid, history.final_prio
951                ));
952            }
953            {
954                let mut time_last = None;
955                for (count, (ns, prio)) in history.prio_changes.iter().enumerate() {
956                    if let Some(last) = time_last {
957                        if !is_ordinary_priority(*prio) {
958                            return Err(format!(
959                                "preemption priority #{} for thread {} invalid: {}",
960                                count, tid, prio
961                            ));
962                        }
963                        if *ns <= last {
964                            return Err(format!(
965                                "thread {}'s change point {} at {} is not after change point {} \
966                                 at {} (counting from 1): a thread's change points must be \
967                                 strictly increasing in time",
968                                tid,
969                                count + 1,
970                                ns,
971                                count,
972                                last
973                            ));
974                        }
975                    }
976                    time_last = Some(*ns);
977                }
978            }
979            if !history.preemption_rcbs.is_empty()
980                && history.preemption_rcbs.len() != history.prio_changes.len()
981            {
982                return Err(format!(
983                    "thread {} has {} preemption times but {} RCB targets",
984                    tid,
985                    history.prio_changes.len(),
986                    history.preemption_rcbs.len()
987                ));
988            }
989            if history
990                .preemption_rcbs
991                .windows(2)
992                .any(|pair| pair[1] < pair[0])
993            {
994                return Err(format!(
995                    "preemption RCB targets failed to increase for thread {}",
996                    tid
997                ));
998            }
999            let mut transition_last = None;
1000            let mut epoch_last = None;
1001            for transition in &history.chaos_epochs {
1002                if transition.factor.as_f64() <= 0.0 {
1003                    return Err(format!(
1004                        "chaos epoch factor for thread {} must be positive",
1005                        tid
1006                    ));
1007                }
1008                if transition_last.is_some_and(|last| transition.logical_time <= last) {
1009                    return Err(format!(
1010                        "chaos epoch transition times failed to increase for thread {}",
1011                        tid
1012                    ));
1013                }
1014                if epoch_last.is_some_and(|last| transition.epoch <= last) {
1015                    return Err(format!(
1016                        "chaos epoch numbers failed to increase for thread {}",
1017                        tid
1018                    ));
1019                }
1020                transition_last = Some(transition.logical_time);
1021                epoch_last = Some(transition.epoch);
1022            }
1023        }
1024        Ok(())
1025    }
1026
1027    fn validate_callback_checkpoints(&self) -> Result<(), String> {
1028        let mut endpoints: BTreeMap<DetTid, BTreeMap<u64, crate::types::Op>> = BTreeMap::new();
1029        let mut progress = BTreeMap::<DetTid, u64>::new();
1030        for event in self.global.iter().filter(|event| {
1031            event.count > 0 && self.callback_checkpoints.contains_key(&event.dettid)
1032        }) {
1033            let count = progress.entry(event.dettid).or_default();
1034            *count = count
1035                .checked_add(u64::from(event.count))
1036                .ok_or("callback progress overflow")?;
1037            endpoints
1038                .entry(event.dettid)
1039                .or_default()
1040                .insert(*count, event.op);
1041        }
1042        for (tid, checkpoints) in &self.callback_checkpoints {
1043            let history = self
1044                .per_thread
1045                .get(tid)
1046                .ok_or("callback history has no thread")?;
1047            let mut previous = None;
1048            for checkpoint in checkpoints {
1049                let row = checkpoint.history_index;
1050                if previous.is_some_and(|(last_row, last_progress)| {
1051                    row <= last_row || checkpoint.origin.progress < last_progress
1052                }) || history.prio_changes.get(row).map(|x| x.0) != Some(checkpoint.origin.time)
1053                    || history.preemption_rcbs.get(row).copied() != Some(checkpoint.origin.rcbs)
1054                {
1055                    return Err(format!("invalid callback history row for {tid}: {row}"));
1056                }
1057                previous = Some((row, checkpoint.origin.progress));
1058                let found = if checkpoint.origin.progress == 0 {
1059                    checkpoint.origin.last_op.is_none()
1060                } else {
1061                    endpoints
1062                        .get(tid)
1063                        .and_then(|points| points.get(&checkpoint.origin.progress))
1064                        == checkpoint.origin.last_op.as_ref()
1065                        && checkpoint.origin.last_op.is_some()
1066                };
1067                if !found {
1068                    return Err(format!(
1069                        "callback history has no event endpoint for {tid}: {row}"
1070                    ));
1071                }
1072            }
1073        }
1074        Ok(())
1075    }
1076
1077    /// Normalize priorities for readibility.
1078    pub fn normalize(&self) -> PreemptionRecord {
1079        let mut clone = self.clone();
1080
1081        let mut priomap: BTreeMap<Priority, Priority> = BTreeMap::new();
1082        for history in clone.per_thread.values_mut() {
1083            let _ = priomap.insert(history.final_prio, 0);
1084            for (_ns, prio) in &history.prio_changes {
1085                let _ = priomap.insert(*prio, 0);
1086            }
1087        }
1088
1089        for (cur_prio, val) in (DEFAULT_PRIORITY..).zip(priomap.values_mut()) {
1090            assert!(cur_prio <= LAST_PRIORITY);
1091            *val = cur_prio;
1092        }
1093        for history in clone.per_thread.values_mut() {
1094            history.final_prio = *priomap.get(&history.final_prio).unwrap();
1095            for (_ns, prio) in &mut history.prio_changes {
1096                *prio = *priomap.get(prio).unwrap();
1097            }
1098        }
1099
1100        // One more pass to remove anything that just has default priority its whole lifetime.
1101        let mut finalmap = BTreeMap::new();
1102        for (tid, history) in clone.per_thread.into_iter() {
1103            if history.final_prio != DEFAULT_PRIORITY
1104                || !history.prio_changes.is_empty()
1105                || !history.chaos_epochs.is_empty()
1106            {
1107                assert!(finalmap.insert(tid, history).is_none());
1108            }
1109        }
1110        clone.per_thread = finalmap;
1111        clone
1112    }
1113
1114    /// Remove whichever is the latest preemption across all thread histories. If
1115    /// there are no priority changes in any thread history, the entire last thread
1116    /// (highest thread number) is completely removed.
1117    /// Note this function assumes the last priority change in the list has the latest
1118    /// preempt time.
1119    pub fn with_latest_preempt_removed(&self) -> PreemptionRecord {
1120        let mut preempts_latest_prio_changes: Vec<(DetTid, LogicalTime)> = self
1121            .per_thread
1122            .clone()
1123            .into_iter()
1124            .map(|(tid, th)| {
1125                (
1126                    tid,
1127                    th.prio_changes
1128                        .last() // Assume last prio change in the list has the latest preempt time
1129                        .map_or(LogicalTime::ZERO, |prio_change| prio_change.0),
1130                )
1131            })
1132            .collect();
1133        preempts_latest_prio_changes.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
1134
1135        let tid_with_preempt_to_drop = preempts_latest_prio_changes
1136            .into_iter()
1137            .map(|(tid, _th)| tid)
1138            .next();
1139
1140        let mut clone = self.clone();
1141        clone.callback_checkpoints.clear();
1142        if let Some(tid) = tid_with_preempt_to_drop {
1143            let thread_history = &mut clone.per_thread.get_mut(&tid).unwrap();
1144            if !thread_history.prio_changes.is_empty() {
1145                // There were prio changes for at least one of the threads, remove that
1146                // preemption as the one to drop
1147                let removed_prio = thread_history.prio_changes.pop().unwrap().1;
1148                if !thread_history.preemption_rcbs.is_empty() {
1149                    thread_history.preemption_rcbs.pop();
1150                }
1151                thread_history.final_prio = removed_prio;
1152            } else {
1153                // There were no prio changes for any threads, so just remove the last
1154                // thread completely as the drop
1155                clone.per_thread.pop_last();
1156            }
1157        }
1158        clone
1159    }
1160}
1161
1162/// The record of priorities and preemptions for a particular thread.
1163#[derive(
1164    PartialEq, // Silly protection from rustfmt disagreements.
1165    Debug,
1166    Eq,
1167    Clone,
1168    Hash,
1169    Serialize,
1170    Deserialize,
1171)]
1172pub struct ThreadHistory {
1173    /// The priority of the last timeslice, after the last preemption.
1174    /// (This is also the initial priority if there are no preemptions.)
1175    /// Plain nochaos/default-heuristic raw replay retains this logical policy
1176    /// value; its temporary foreground/deferred dispatch bands are not changes
1177    /// to the recorded policy.
1178    pub final_prio: Priority,
1179
1180    /// The preemption points and priority just BEFORE the preemption occurred.  It's
1181    /// private, and thus you must go through the iterator, as this will change in the
1182    /// future.
1183    prio_changes: Vec<(LogicalTime, Priority)>,
1184
1185    // AUTONOMOUS-BOT-IMPLEMENTED
1186    // TODO-HUMAN-REVIEW(PR-1151)
1187    /// Exact per-thread PMU RCB target for each matching priority change. Empty
1188    /// in older JSON artifacts, which fall back to logical-time replay.
1189    // `ThreadHistory` crosses a non-self-describing bincode RPC; never skip it.
1190    #[serde(default)]
1191    preemption_rcbs: Vec<u64>,
1192
1193    // AUTONOMOUS-BOT-IMPLEMENTED
1194    // TODO-HUMAN-REVIEW(PR-1151)
1195    /// Exact per-thread slowdown transitions. Empty artifacts from older Hermit
1196    /// versions remain wire-compatible when read through Serde's default.
1197    // `ThreadHistory` also crosses Reverie's non-self-describing bincode RPC,
1198    // so this field must never be skipped when it is empty.
1199    #[serde(default)]
1200    chaos_epochs: Vec<ChaosEpochTransition>,
1201}
1202
1203impl ThreadHistory {
1204    /// An empty thread history, with default priority only.
1205    pub fn new() -> Self {
1206        ThreadHistory {
1207            final_prio: DEFAULT_PRIORITY,
1208            prio_changes: Vec::new(),
1209            preemption_rcbs: Vec::new(),
1210            chaos_epochs: Vec::new(),
1211        }
1212    }
1213
1214    /// Turns the thread history into an iterator that produces a stream of preemption events.
1215    #[allow(clippy::should_implement_trait)]
1216    pub fn into_iter(self) -> ThreadHistoryIterator {
1217        ThreadHistoryIterator {
1218            full_history: self,
1219            ix: 0,
1220            chaos_epoch_ix: 0,
1221        }
1222    }
1223
1224    #[cfg(test)]
1225    pub(crate) fn with_chaos_epochs(mut self, chaos_epochs: Vec<ChaosEpochTransition>) -> Self {
1226        self.chaos_epochs = chaos_epochs;
1227        self
1228    }
1229
1230    #[cfg(test)]
1231    pub(crate) fn with_prio_changes(mut self, prio_changes: Vec<(LogicalTime, Priority)>) -> Self {
1232        self.prio_changes = prio_changes;
1233        self
1234    }
1235
1236    #[cfg(test)]
1237    pub(crate) fn with_preemption_rcbs(mut self, preemption_rcbs: Vec<u64>) -> Self {
1238        self.preemption_rcbs = preemption_rcbs;
1239        self
1240    }
1241
1242    /// Flatten the time series into a uniform representation.
1243    /// This is a `(Time, Priority_AFTER_Time)` representation.
1244    /// The very first timeslice will have a zero timestamp.
1245    // Note that this is different than the internal representation which groups together
1246    // `Priority_UNTIL_Time, Time`, i.e. shifted by one.
1247    pub fn as_vec(&self) -> Vec<(LogicalTime, Priority)> {
1248        let mut vec = Vec::new();
1249        let mut time0 = LogicalTime::from_nanos(0);
1250
1251        for (time1, prio) in &self.prio_changes {
1252            vec.push((time0, *prio));
1253            time0 = *time1;
1254        }
1255        vec.push((time0, self.final_prio));
1256        vec
1257    }
1258
1259    /// Return the priority of the initial timeslice after the thread starts running.
1260    pub fn initial_priority(&self) -> Priority {
1261        if let Some((_ns, pr)) = self.prio_changes.first() {
1262            *pr
1263        } else {
1264            self.final_prio
1265        }
1266    }
1267}
1268
1269impl Default for ThreadHistory {
1270    fn default() -> Self {
1271        Self::new()
1272    }
1273}
1274
1275/// An iterator over the preemption history of a thread.  Does not include the final
1276/// timeslice, which does not have an ending time.
1277// TODO: in the future this will perform "lazy IO" and pull from disk in batches.
1278#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
1279pub struct ThreadHistoryIterator {
1280    /// For now we store the full history in memory:
1281    full_history: ThreadHistory,
1282    /// Index that tracks our position.
1283    ix: usize,
1284    // AUTONOMOUS-BOT-IMPLEMENTED
1285    // TODO-HUMAN-REVIEW(PR-1151)
1286    #[serde(default)]
1287    chaos_epoch_ix: usize,
1288}
1289
1290impl ThreadHistoryIterator {
1291    /// Return the priority of the initial slice after the thread starts running.
1292    pub fn initial_priority(&self) -> Priority {
1293        self.full_history.initial_priority()
1294    }
1295
1296    /// Return the priority of the last slice after the last preemption.
1297    pub fn final_priority(&self) -> Priority {
1298        self.full_history.final_prio
1299    }
1300
1301    // AUTONOMOUS-BOT-IMPLEMENTED
1302    // TODO-HUMAN-REVIEW(PR-1151)
1303    /// Consume recorded epoch transitions up to `current_time`, returning a
1304    /// transition only when the active recorded factor changed.
1305    pub fn advance_chaos_epoch(
1306        &mut self,
1307        current_time: LogicalTime,
1308    ) -> Option<ChaosEpochTransition> {
1309        let mut changed = None;
1310        while let Some(transition) = self.full_history.chaos_epochs.get(self.chaos_epoch_ix)
1311            && transition.logical_time <= current_time
1312        {
1313            changed = Some(*transition);
1314            self.chaos_epoch_ix += 1;
1315        }
1316        changed
1317    }
1318
1319    /// Whether this artifact carries recorded slowdown epochs.
1320    pub fn has_chaos_epochs(&self) -> bool {
1321        !self.full_history.chaos_epochs.is_empty()
1322    }
1323
1324    // AUTONOMOUS-BOT-IMPLEMENTED
1325    // TODO-HUMAN-REVIEW(PR-1151)
1326    /// Return the next recorded logical boundary and its exact RCB target when
1327    /// the artifact was produced by a version that records one.
1328    pub fn next_with_rcbs(&mut self) -> Option<(LogicalTime, Priority, Option<u64>)> {
1329        let ix = self.ix;
1330        self.next().map(|(time, priority)| {
1331            (
1332                time,
1333                priority,
1334                self.full_history.preemption_rcbs.get(ix).copied(),
1335            )
1336        })
1337    }
1338
1339    /// How many recorded rows have been installed as slice ends. The row the
1340    /// current slice ends is the last of them: `next_with_rcbs` advances past
1341    /// a row as it installs it, before that row's priority request is filed.
1342    pub fn installed_rows(&self) -> usize {
1343        self.ix
1344    }
1345}
1346
1347impl Iterator for ThreadHistoryIterator {
1348    type Item = (LogicalTime, Priority);
1349
1350    fn next(&mut self) -> Option<Self::Item> {
1351        let vec = &self.full_history.prio_changes;
1352        if vec.len() > self.ix {
1353            let elt = vec[self.ix];
1354            self.ix += 1;
1355            Some(elt)
1356        } else {
1357            None
1358        }
1359    }
1360}
1361
1362#[cfg(test)]
1363mod tests {
1364    use detcore_model::schedule::Op;
1365    use pretty_assertions::assert_eq;
1366    use test_case::test_case;
1367
1368    use super::*;
1369    use crate::types::RcbTimeMultiplier;
1370
1371    /// A record written by a replay that left a change point out of it says
1372    /// so, with the global event count where it first did; records derived
1373    /// from it keep the mark; a record with nothing left out has none.
1374    #[test]
1375    fn marked_record_serialization_requires_an_envelope() {
1376        let ordinary = PreemptionWriter::new(None).into_string();
1377        assert_eq!(ordinary, "{\"per_thread\":{},\"global\":[]}");
1378        let mut writer = PreemptionWriter::new(None);
1379        writer.mark_replay_departed(7);
1380        let text = writer.into_string();
1381        let value: serde_json::Value = serde_json::from_str(&text).unwrap();
1382        assert!(
1383            value.get("per_thread").is_none(),
1384            "old readers must not accept {text}"
1385        );
1386        assert!(
1387            value.get("global").is_none(),
1388            "old readers must not accept {text}"
1389        );
1390        assert_eq!(value["incomplete_preemption_record"]["version"], 1);
1391        assert_eq!(
1392            value["incomplete_preemption_record"]["replay_departed_at"],
1393            7
1394        );
1395        let back: PreemptionRecord = serde_json::from_str(&text).unwrap();
1396        assert_eq!(back.replay_departed_at(), Some(7));
1397    }
1398
1399    #[test]
1400    fn marked_records_refuse_binary_serialization_before_writing() {
1401        let mut writer = PreemptionWriter::new(None);
1402        writer.mark_replay_departed(7);
1403        let record: PreemptionRecord = serde_json::from_str(&writer.into_string()).unwrap();
1404        let mut bytes = Vec::new();
1405        let result =
1406            bincode::serde::encode_into_std_write(&record, &mut bytes, bincode::config::legacy());
1407        assert!(
1408            result.is_err(),
1409            "a marked record has no unversioned binary encoding"
1410        );
1411        assert!(bytes.is_empty(), "refuse before writing a partial record");
1412    }
1413
1414    #[test]
1415    fn a_departed_replays_record_says_it_is_not_exact() {
1416        let mut writer = PreemptionWriter::new(None);
1417        writer.mark_replay_departed(7);
1418        writer.mark_replay_departed(9);
1419        let written = writer.into_string();
1420        let back: PreemptionRecord = serde_json::from_str(&written).unwrap();
1421        assert_eq!(back.replay_departed_at(), Some(7), "the first departure");
1422        assert_eq!(back.clone_preemptions_only().replay_departed_at(), Some(7));
1423        let dir = tempfile::tempdir().unwrap();
1424        let path = dir.path().join("record.json");
1425        std::fs::write(&path, &written).unwrap();
1426        assert_eq!(read_recorded_replay_departure(&path), Ok(Some(7)));
1427        let exact = PreemptionWriter::new(None).into_string();
1428        assert!(!exact.contains("replay_departed_at"), "{exact}");
1429        std::fs::write(&path, &exact).unwrap();
1430        assert_eq!(read_recorded_replay_departure(&path), Ok(None));
1431    }
1432
1433    /// `--pipe-wake-latency` rides in a preemption record as the epoch does:
1434    /// omitted when off, kept through JSON and through every record a run or
1435    /// an analysis writes, and compared exactly, so that `Some(0)` is not off
1436    /// (https://github.com/rrnewton/hermit/issues/4380).
1437    #[test]
1438    fn the_pipe_wake_latency_round_trips_and_is_compared_exactly() {
1439        let off = PreemptionRecord::from_sched_events(Vec::new());
1440        assert!(
1441            !serde_json::to_string(&off)
1442                .unwrap()
1443                .contains("pipe_wake_latency")
1444        );
1445        let dir = tempfile::tempdir().unwrap();
1446        for latency in [Some(0), Some(3_000_000)] {
1447            let record =
1448                PreemptionRecord::from_sched_events(Vec::new()).with_pipe_wake_latency(latency);
1449            let json = serde_json::to_string(&record).unwrap();
1450            let back: PreemptionRecord = serde_json::from_str(&json).unwrap();
1451            assert_eq!(back.pipe_wake_latency(), latency);
1452            assert_eq!(record.clone_preemptions_only().pipe_wake_latency(), latency);
1453            let minimized =
1454                PreemptionRecord::from_vecs(&BTreeMap::new()).with_pipe_wake_latency(latency);
1455            assert_eq!(minimized.pipe_wake_latency(), latency);
1456            let path = dir.path().join("record.json");
1457            std::fs::write(&path, &json).unwrap();
1458            assert_eq!(read_recorded_pipe_wake_latency(&path), Ok(latency));
1459            let written = PreemptionWriter::new(None)
1460                .with_pipe_wake_latency(latency)
1461                .into_string();
1462            assert!(
1463                written.contains(&format!("\"pipe_wake_latency\":{}", latency.unwrap())),
1464                "{written}"
1465            );
1466        }
1467        for (recorded, replay, refused) in [
1468            (None, None, false),
1469            (Some(0), Some(0), false),
1470            (Some(0), None, true),
1471            (None, Some(0), true),
1472            (Some(0), Some(3_000_000), true),
1473        ] {
1474            assert_eq!(
1475                pipe_wake_latency_mismatch("--replay-schedule-from=x", recorded, replay).is_some(),
1476                refused,
1477                "{recorded:?} replayed with {replay:?}"
1478            );
1479        }
1480    }
1481
1482    /// How a run charged runs of expired timed waits rides in a preemption
1483    /// record only when it charged them once (`--expired-wait-charge=once`,
1484    /// by default outside race search), so an `each` record is written
1485    /// exactly as before; a record that does not say means every expired
1486    /// wait was charged.
1487    #[test]
1488    fn the_expired_wait_charge_rule_round_trips() {
1489        let unsaid = PreemptionRecord::from_sched_events(Vec::new());
1490        assert!(!unsaid.expired_wait_runs_charged_once());
1491        let dir = tempfile::tempdir().unwrap();
1492        let path = dir.path().join("record.json");
1493        for once in [false, true] {
1494            let written = PreemptionWriter::new(None)
1495                .with_expired_wait_runs_charged_once(once)
1496                .into_string();
1497            assert_eq!(
1498                written.contains("expired_wait_runs_charged_once"),
1499                once,
1500                "{written}"
1501            );
1502            let back: PreemptionRecord = serde_json::from_str(&written).unwrap();
1503            assert_eq!(back.expired_wait_runs_charged_once(), once);
1504            assert_eq!(
1505                back.clone_preemptions_only()
1506                    .expired_wait_runs_charged_once(),
1507                once
1508            );
1509            std::fs::write(&path, &written).unwrap();
1510            assert_eq!(
1511                read_recorded_expired_wait_runs_charged_once(&path),
1512                Ok(once)
1513            );
1514        }
1515        std::fs::write(&path, serde_json::to_string(&unsaid).unwrap()).unwrap();
1516        assert_eq!(
1517            read_recorded_expired_wait_runs_charged_once(&path),
1518            Ok(false)
1519        );
1520    }
1521
1522    /// The guest numbering of a run without a PID namespace rides in its
1523    /// record, and a replay that numbers otherwise is refused; a namespace
1524    /// run's record is written as before and reads back as namespace-numbered.
1525    #[test]
1526    fn the_guest_numbering_round_trips_and_is_compared() {
1527        use detcore_model::pid::GuestIds;
1528        let chosen = |base, refuse_host_ids| GuestIds::Chosen {
1529            base,
1530            refuse_host_ids,
1531        };
1532        let dir = tempfile::tempdir().unwrap();
1533        let path = dir.path().join("record.json");
1534        let namespace = PreemptionWriter::new(None)
1535            .with_guest_ids(GuestIds::Namespace)
1536            .into_string();
1537        assert!(!namespace.contains("guest_ids"), "{namespace}");
1538        std::fs::write(&path, &namespace).unwrap();
1539        assert_eq!(read_recorded_guest_ids(&path), Ok(GuestIds::Namespace));
1540        for ids in [GuestIds::Host, chosen(4000, false)] {
1541            let written = PreemptionWriter::new(None)
1542                .with_guest_ids(ids)
1543                .into_string();
1544            let back: PreemptionRecord = serde_json::from_str(&written).unwrap();
1545            assert_eq!(back.guest_ids(), Some(ids));
1546            assert_eq!(back.clone_preemptions_only().guest_ids(), Some(ids));
1547            std::fs::write(&path, &written).unwrap();
1548            assert_eq!(read_recorded_guest_ids(&path), Ok(ids));
1549        }
1550        for (recorded, replay, refused) in [
1551            (GuestIds::Namespace, GuestIds::Namespace, false),
1552            (chosen(4000, false), chosen(4000, true), false),
1553            (chosen(4000, false), chosen(4001, false), true),
1554            (chosen(4000, false), GuestIds::Namespace, true),
1555            (GuestIds::Namespace, chosen(4000, true), true),
1556            (GuestIds::Host, GuestIds::Host, false),
1557            (GuestIds::Host, GuestIds::Namespace, true),
1558        ] {
1559            assert_eq!(
1560                guest_ids_mismatch("--replay-schedule-from=x", recorded, replay).is_some(),
1561                refused,
1562                "{recorded:?} replayed with {replay:?}"
1563            );
1564        }
1565    }
1566
1567    /// The signal-state copy setting rides in a preemption record as the
1568    /// pipe-wake latency does, and a replay under the other setting is
1569    /// refused; a record that does not say (older, or synthesized) is not.
1570    /// `--chaos-timeslice-draw=run` draws a pure function of the scheduler
1571    /// seed, inside the range, spread evenly over its decades (log-uniform),
1572    /// and a record stores the draw only when there is one.
1573    #[test]
1574    fn a_drawn_chaos_timeslice_is_seeded_log_uniform_and_recorded() {
1575        let range: detcore_model::config::TimesliceRange = "1000..1000000000".parse().unwrap();
1576        let mut decades = [0u32; 6];
1577        for seed in 0..3000 {
1578            let drawn = draw_chaos_timeslice(range, seed).get();
1579            assert_eq!(draw_chaos_timeslice(range, seed).get(), drawn);
1580            assert!((1_000..=1_000_000_000).contains(&drawn), "{drawn}");
1581            decades[((drawn as f64).log10() as usize - 3).min(5)] += 1;
1582        }
1583        // 500 expected in each decade.
1584        assert!(
1585            decades.iter().all(|count| (400..=600).contains(count)),
1586            "{decades:?}"
1587        );
1588        assert_ne!(
1589            draw_chaos_timeslice(range, 1),
1590            draw_chaos_timeslice(range, 2)
1591        );
1592        let point = "4321..4321".parse().unwrap();
1593        assert_eq!(draw_chaos_timeslice(point, 9).get(), 4321);
1594
1595        let unsaid = PreemptionWriter::new(None).into_string();
1596        assert!(!unsaid.contains("chaos_timeslice"), "{unsaid}");
1597        let written = PreemptionWriter::new(None)
1598            .with_chaos_timeslice(std::num::NonZeroU64::new(4321))
1599            .into_string();
1600        assert!(written.contains("\"chaos_timeslice\":4321"), "{written}");
1601        let back: PreemptionRecord = serde_json::from_str(&written).unwrap();
1602        assert_eq!(back.chaos_timeslice(), Some(4321));
1603        assert_eq!(back.clone_preemptions_only().chaos_timeslice(), Some(4321));
1604        let dir = tempfile::tempdir().unwrap();
1605        let path = dir.path().join("record.json");
1606        std::fs::write(&path, &written).unwrap();
1607        assert_eq!(read_recorded_chaos_timeslice(&path), Ok(Some(4321)));
1608        std::fs::write(&path, &unsaid).unwrap();
1609        assert_eq!(read_recorded_chaos_timeslice(&path), Ok(None));
1610    }
1611
1612    #[test]
1613    fn the_signal_state_copy_setting_round_trips_and_is_compared() {
1614        let unsaid = PreemptionRecord::from_sched_events(Vec::new());
1615        assert_eq!(unsaid.signal_state_copy(), None);
1616        assert!(
1617            !serde_json::to_string(&unsaid)
1618                .unwrap()
1619                .contains("signal_state_copy")
1620        );
1621        let dir = tempfile::tempdir().unwrap();
1622        for on in [true, false] {
1623            let written = PreemptionWriter::new(None)
1624                .with_signal_state_copy(on)
1625                .into_string();
1626            assert!(
1627                written.contains(&format!("\"signal_state_copy\":{on}")),
1628                "{written}"
1629            );
1630            let back: PreemptionRecord = serde_json::from_str(&written).unwrap();
1631            assert_eq!(back.signal_state_copy(), Some(on));
1632            assert_eq!(back.clone_preemptions_only().signal_state_copy(), Some(on));
1633            let path = dir.path().join("record.json");
1634            std::fs::write(&path, &written).unwrap();
1635            assert_eq!(read_recorded_signal_state_copy(&path), Ok(Some(on)));
1636        }
1637        for (recorded, replay, refused) in [
1638            (None, true, false),
1639            (None, false, false),
1640            (Some(true), true, false),
1641            (Some(false), false, false),
1642            (Some(true), false, true),
1643            (Some(false), true, true),
1644        ] {
1645            assert_eq!(
1646                signal_state_copy_mismatch("--replay-schedule-from=x", recorded, replay).is_some(),
1647                refused,
1648                "{recorded:?} replayed with {replay}"
1649            );
1650        }
1651    }
1652
1653    /// `--cap-preemption-charge` round-trips through the writer, the record
1654    /// and the settings reader; off writes the record as before; a replay
1655    /// under the other setting is refused.
1656    #[test]
1657    fn the_cap_preemption_charge_setting_round_trips_and_is_compared() {
1658        let off = PreemptionWriter::new(None).into_string();
1659        assert!(!off.contains("cap_preemption_charge"), "{off}");
1660        let written = PreemptionWriter::new(None)
1661            .with_cap_preemption_charge(true)
1662            .into_string();
1663        assert!(
1664            written.contains("\"cap_preemption_charge\":true"),
1665            "{written}"
1666        );
1667        let back: PreemptionRecord = serde_json::from_str(&written).unwrap();
1668        assert!(back.cap_preemption_charge());
1669        assert!(back.clone_preemptions_only().cap_preemption_charge());
1670        let dir = tempfile::tempdir().unwrap();
1671        let path = dir.path().join("record.json");
1672        std::fs::write(&path, &written).unwrap();
1673        assert_eq!(read_recorded_cap_preemption_charge(&path), Ok(true));
1674        std::fs::write(&path, &off).unwrap();
1675        assert_eq!(read_recorded_cap_preemption_charge(&path), Ok(false));
1676        for (recorded, replay, refused) in [
1677            (false, false, false),
1678            (true, true, false),
1679            (true, false, true),
1680            (false, true, true),
1681        ] {
1682            assert_eq!(
1683                cap_preemption_charge_mismatch("--replay-schedule-from=x", recorded, replay)
1684                    .is_some(),
1685                refused,
1686                "{recorded} replayed with {replay}"
1687            );
1688        }
1689    }
1690
1691    /// The run's failed store-exclusives ride in a record so that a replay
1692    /// can label a divergence: written only when nonzero, so a record of a
1693    /// run without failures is byte-for-byte what it was before the field,
1694    /// and read as zero from records that lack it. The count is the host's,
1695    /// so it never makes two records unequal, and a copy of the preemptions
1696    /// alone does not carry it.
1697    #[test]
1698    fn store_exclusive_failures_round_trip_and_legacy_records_have_none() {
1699        let none = PreemptionWriter::new(None).with_signal_state_copy(true);
1700        let legacy = none.into_string();
1701        assert!(!legacy.contains("store_exclusive_failures"), "{legacy}");
1702        let back: PreemptionRecord = serde_json::from_str(&legacy).unwrap();
1703        assert_eq!(back.store_exclusive_failures(), 0);
1704
1705        let mut zero = PreemptionWriter::new(None).with_signal_state_copy(true);
1706        zero.record_store_exclusive_failures(0);
1707        assert_eq!(zero.into_string(), legacy);
1708
1709        let mut writer = PreemptionWriter::new(None).with_signal_state_copy(true);
1710        writer.record_store_exclusive_failures(20);
1711        let written = writer.into_string();
1712        assert!(
1713            written.contains("\"store_exclusive_failures\":20"),
1714            "{written}"
1715        );
1716        let back: PreemptionRecord = serde_json::from_str(&written).unwrap();
1717        assert_eq!(back.store_exclusive_failures(), 20);
1718        assert_eq!(back.to_string(), written);
1719        assert_eq!(back.clone_preemptions_only().store_exclusive_failures(), 0);
1720
1721        let without: PreemptionRecord = serde_json::from_str(&legacy).unwrap();
1722        assert_eq!(back, without);
1723        let hash = |record: &PreemptionRecord| {
1724            use std::hash::Hash;
1725            use std::hash::Hasher;
1726            let mut hasher = std::collections::hash_map::DefaultHasher::new();
1727            record.hash(&mut hasher);
1728            hasher.finish()
1729        };
1730        assert_eq!(hash(&back), hash(&without));
1731    }
1732
1733    // AUTONOMOUS-BOT-IMPLEMENTED
1734    // TODO-HUMAN-REVIEW(PR-1151)
1735    #[test]
1736    fn chaos_epoch_transitions_round_trip_and_replay_exact_factors() {
1737        let tid = DetTid::from_raw(2);
1738        let first = ChaosEpochTransition {
1739            logical_time: LogicalTime::from_nanos(100),
1740            epoch: 0,
1741            factor: RcbTimeMultiplier::from_f64(2.5),
1742        };
1743        let second = ChaosEpochTransition {
1744            logical_time: LogicalTime::from_nanos(500),
1745            epoch: 1,
1746            factor: RcbTimeMultiplier::from_f64(0.75),
1747        };
1748
1749        let mut writer = PreemptionWriter::new(None);
1750        writer.register_thread(tid, DEFAULT_PRIORITY);
1751        writer.insert_chaos_epoch(tid, first);
1752        writer.insert_chaos_epoch(tid, second);
1753        let encoded = writer.into_string();
1754        assert!(encoded.contains("chaos_epochs"));
1755
1756        let decoded: PreemptionRecord = serde_json::from_str(&encoded).unwrap();
1757        decoded.validate().unwrap();
1758        let mut history = decoded.extract_all().remove(&tid).unwrap().into_iter();
1759        assert_eq!(
1760            history.advance_chaos_epoch(LogicalTime::from_nanos(99)),
1761            None
1762        );
1763        assert_eq!(
1764            history.advance_chaos_epoch(LogicalTime::from_nanos(100)),
1765            Some(first)
1766        );
1767        assert_eq!(
1768            history.advance_chaos_epoch(LogicalTime::from_nanos(499)),
1769            None
1770        );
1771        assert_eq!(
1772            history.advance_chaos_epoch(LogicalTime::from_nanos(500)),
1773            Some(second)
1774        );
1775        assert_eq!(history.advance_chaos_epoch(LogicalTime::MAX), None);
1776    }
1777
1778    /// <https://github.com/rrnewton/hermit/issues/3411>: the epoch a record was
1779    /// made under survives serialization, the preemptions-only copy, and the
1780    /// non-panicking CLI reader; records written before it was stored read as
1781    /// `None`, and a malformed file is an error rather than a panic.
1782    #[test]
1783    fn recorded_epoch_round_trips_and_legacy_records_have_none() {
1784        let epoch: DateTime<Utc> = "2000-12-31T23:59:59.123456789Z".parse().unwrap();
1785        let tid = DetTid::from_raw(3);
1786        let mut writer = PreemptionWriter::new(None).with_epoch(epoch);
1787        writer.register_thread(tid, DEFAULT_PRIORITY);
1788        writer.insert_reprioritization(
1789            tid,
1790            LogicalTime::from_nanos(978_307_199_223_456_789),
1791            7,
1792            DEFAULT_PRIORITY,
1793            5,
1794        );
1795        let encoded = writer.into_string();
1796
1797        let decoded: PreemptionRecord = serde_json::from_str(&encoded).unwrap();
1798        decoded.validate().unwrap();
1799        assert_eq!(decoded.epoch(), Some(epoch));
1800        assert_eq!(decoded.clone_preemptions_only().epoch(), Some(epoch));
1801
1802        let directory = tempfile::tempdir().unwrap();
1803        let current = directory.path().join("current.json");
1804        std::fs::write(&current, &encoded).unwrap();
1805        assert_eq!(read_recorded_epoch(&current), Ok(Some(epoch)));
1806
1807        let mut legacy_value: serde_json::Value = serde_json::from_str(&encoded).unwrap();
1808        legacy_value
1809            .as_object_mut()
1810            .unwrap()
1811            .remove("epoch")
1812            .unwrap();
1813        let legacy = directory.path().join("legacy.json");
1814        std::fs::write(&legacy, legacy_value.to_string()).unwrap();
1815        assert_eq!(read_recorded_epoch(&legacy), Ok(None));
1816        let legacy_record: PreemptionRecord = serde_json::from_value(legacy_value.clone()).unwrap();
1817        assert_eq!(legacy_record.epoch(), None);
1818        assert!(
1819            !serde_json::to_string(&legacy_record)
1820                .unwrap()
1821                .contains("\"epoch\"")
1822        );
1823
1824        let malformed = directory.path().join("malformed.json");
1825        std::fs::write(&malformed, "{\"epoch\": 7}").unwrap();
1826        assert!(read_recorded_epoch(&malformed).is_err());
1827        assert!(read_recorded_epoch(&directory.path().join("absent.json")).is_err());
1828    }
1829
1830    #[test]
1831    fn print_preemptionrecord() {
1832        let (file, path) = tempfile::NamedTempFile::new().unwrap().keep().unwrap();
1833        drop(file);
1834        let mut pw = PreemptionWriter::new(Some(path.clone()));
1835        let tid1 = DetTid::from_raw(2);
1836        let tid2 = DetTid::from_raw(4);
1837        pw.register_thread(tid1, 1000);
1838        pw.register_thread(tid2, 1000);
1839
1840        pw.insert_reprioritization(tid1, LogicalTime::from_nanos(3), 3, 1000, 3);
1841        pw.insert_reprioritization(tid1, LogicalTime::from_nanos(30), 30, 3, 30);
1842        pw.insert_reprioritization(tid1, LogicalTime::from_nanos(300), 300, 30, 300);
1843        pw.insert_reprioritization(tid2, LogicalTime::from_nanos(2), 2, 1000, 2);
1844        pw.insert_reprioritization(tid2, LogicalTime::from_nanos(20), 20, 2, 20);
1845        pw.insert_reprioritization(tid2, LogicalTime::from_nanos(200), 200, 20, 200);
1846
1847        // First, round trip through a pretty-printed string
1848        // ------------------------------------------------
1849        let str: String = serde_json::to_string_pretty(&pw.inner).unwrap();
1850        eprintln!("{}", str);
1851        let pr2: PreemptionRecord = serde_json::from_str(&str).unwrap();
1852        eprintln!("Round trip {:?}", pr2);
1853        assert_eq!(pw.inner, pr2);
1854
1855        // Second, actually write it out to disk
1856        // -------------------------------------
1857        pw.flush().unwrap();
1858        let reader = PreemptionReader::new(&path);
1859        let th1 = reader.extract_thread_record(&tid1).unwrap();
1860        let th2 = reader.extract_thread_record(&tid2).unwrap();
1861        assert_eq!(th1.final_prio, 300);
1862        assert_eq!(th2.final_prio, 200);
1863
1864        let mut exact = th1.clone().into_iter();
1865        assert_eq!(
1866            exact.next_with_rcbs(),
1867            Some((LogicalTime::from_nanos(3), 1000, Some(3)))
1868        );
1869        assert_eq!(
1870            exact.next_with_rcbs(),
1871            Some((LogicalTime::from_nanos(30), 3, Some(30)))
1872        );
1873
1874        let it1 = th1.into_iter();
1875        let it2 = th2.into_iter();
1876
1877        assert_eq!(it1.initial_priority(), 1000);
1878        assert_eq!(it2.initial_priority(), 1000);
1879
1880        let v1: Vec<(LogicalTime, Priority)> = it1.collect();
1881        let v2: Vec<(LogicalTime, Priority)> = it2.collect();
1882        assert_eq!(
1883            v1,
1884            vec![
1885                (LogicalTime::from_nanos(3), 1000),
1886                (LogicalTime::from_nanos(30), 3),
1887                (LogicalTime::from_nanos(300), 30)
1888            ]
1889        );
1890        assert_eq!(
1891            v2,
1892            vec![
1893                (LogicalTime::from_nanos(2), 1000),
1894                (LogicalTime::from_nanos(20), 2),
1895                (LogicalTime::from_nanos(200), 20)
1896            ]
1897        );
1898        std::fs::remove_file(path).unwrap();
1899    }
1900
1901    #[test]
1902    fn round_trip_vec_representations() {
1903        let str = r#"{"per_thread":{"2":{"final_prio":1716,"prio_changes":[[946684799000013020,7301],[946684799000034020,9081],[946684799000041600,9238],[946684799000054790,865],
1904[946684799000057440,751],[946684799000061970,275],[946684799000062730,5135],[946684799000069530,6339],
1905[946684799000082850,1123],[946684799000101140,7875],[946684799000140625,4203],[946684799000171780,8611],
1906[946684799000183550,6306],[946684799000184440,7958],[946684799000195750,8919],[946684799000226150,69],
1907[946684799000236380,5915],[946684799000278180,3514],[946684799000320050,30],[946684799000334630,4629],
1908[946684799000344650,2926],[946684799000355020,710],[946684799000365030,3513],[946684799000386350,4881],
1909[946684799000396360,4852],[946684799000406840,4935],[946684799000426980,6672],[946684799000437970,7727],
1910[946684799000452410,7017],[946684799000462430,1572],[946684799000546210,6395],[946684799000548120,3726],
1911[946684799000562700,846],[946684799000583090,7838],[946684799000603310,8291],[946684799000655180,210],
1912[946684799000666230,4576],[946684799000680910,4974],[946684799000723020,9160],[946684799000776780,439],
1913[946684799000777080,6791],[946684799000787220,3015],[946684799000809090,7489],[946684799000840870,7165],
1914[946684799000852855,4326],[946684799000854355,358],[946684799000866575,4448],[946684799000903415,6848],
1915[946684799000913455,1899],[946684799000923630,2117],[946684799000963980,7705],[946684799001007090,8683],
1916[946684799001016950,3317],[946684799001017980,5261],[946684799001027540,3478],[946684799001029990,6474],
1917[946684799001053545,4823],[946684799001068395,4508],[946684799001073095,194],[946684799001121035,5944],
1918[946684799001171285,8408],[946684799001171295,4493],[946684799001192845,3481]]}},"global":[]}"#;
1919        let pr: PreemptionRecord = serde_json::from_str(str).unwrap();
1920        let vecs = pr.as_vecs();
1921        let pr2 = PreemptionRecord::from_vecs(&vecs);
1922        assert_eq!(pr, pr2);
1923    }
1924
1925    #[test]
1926    fn normalize_preemption_record() {
1927        let str = r#"{"per_thread":{
1928"3":{"final_prio":1000,"prio_changes":[]},
1929"5":{"final_prio":1000,"prio_changes":[]},
1930"7":{"final_prio":9722,"prio_changes":[]},
1931"9":{"final_prio":9982,"prio_changes":[[946684799006227400,7839]]},
1932"11":{"final_prio":1000,"prio_changes":[]}},"global":[]}"#;
1933        let pr: PreemptionRecord = serde_json::from_str(str).unwrap();
1934        let pr2 = pr.normalize();
1935        pr2.validate().unwrap();
1936        let bmap = pr2.as_vecs();
1937        // Normalization may remove the useless default-prio entry:
1938        if let Some(x) = bmap.get(&DetTid::from_raw(3)) {
1939            assert_eq!(x, &vec![(LogicalTime::from_nanos(0), 1000)]);
1940        }
1941        if let Some(x) = bmap.get(&DetTid::from_raw(5)) {
1942            assert_eq!(x, &vec![(LogicalTime::from_nanos(0), 1000)]);
1943        }
1944        assert_eq!(
1945            bmap.get(&DetTid::from_raw(7)).unwrap(),
1946            &vec![(LogicalTime::from_nanos(0), 1002)]
1947        );
1948        assert_eq!(
1949            bmap.get(&DetTid::from_raw(9)).unwrap(),
1950            &vec![
1951                (LogicalTime::from_nanos(0), 1001),
1952                (LogicalTime::from_nanos(946684799006227400), 1003)
1953            ]
1954        );
1955        if let Some(x) = bmap.get(&DetTid::from_raw(11)) {
1956            assert_eq!(x, &vec![(LogicalTime::from_nanos(0), 1000)]);
1957        }
1958    }
1959
1960    #[test_case(
1961        r#"{"per_thread":{
1962"3":{"final_prio":1000,"prio_changes":[]},
1963"5":{"final_prio":1002,"prio_changes":[[946684799006227410,1000],[946684799006227415,1001]]},
1964"9":{"final_prio":1002,"prio_changes":[[946684799006227400,1000],[946684799006227405,1001]]},
1965"11":{"final_prio":1000,"prio_changes":[]}},"global":[]}"#,
1966        r#"{"per_thread":{
1967"3":{"final_prio":1000,"prio_changes":[]},
1968"5":{"final_prio":1001,"prio_changes":[[946684799006227410,1000]]},
1969"9":{"final_prio":1002,"prio_changes":[[946684799006227400,1000],[946684799006227405,1001]]},
1970"11":{"final_prio":1000,"prio_changes":[]}},"global":[]}"#
1971        ; "removes latest priority change and coalesces into final priority"
1972    )]
1973    #[test_case(
1974        r#"{"per_thread":{
1975"3":{"final_prio":1000,"prio_changes":[]},
1976"5":{"final_prio":1000,"prio_changes":[]},
1977"9":{"final_prio":1001,"prio_changes":[[946684799006227400,1000]]},
1978"11":{"final_prio":1000,"prio_changes":[]}},"global":[]}"#,
1979        r#"{"per_thread":{
1980"3":{"final_prio":1000,"prio_changes":[]},
1981"5":{"final_prio":1000,"prio_changes":[]},
1982"9":{"final_prio":1000,"prio_changes":[]},
1983"11":{"final_prio":1000,"prio_changes":[]}},"global":[]}"#
1984        ; "removes only priority change and coalesces into final priority"
1985    )]
1986    #[test_case(
1987        r#"{"per_thread":{
1988"3":{"final_prio":1000,"prio_changes":[]},
1989"5":{"final_prio":1000,"prio_changes":[]},
1990"9":{"final_prio":1000,"prio_changes":[]},
1991"11":{"final_prio":1000,"prio_changes":[]}},"global":[]}"#,
1992        r#"{"per_thread":{
1993"3":{"final_prio":1000,"prio_changes":[]},
1994"5":{"final_prio":1000,"prio_changes":[]},
1995"9":{"final_prio":1000,"prio_changes":[]}},"global":[]}"#
1996        ; "removes entire history for last tid if no non-default priority changes"
1997    )]
1998    fn with_latest_preempt_removed(pr_json: &str, expected_pr_json: &str) {
1999        let pr: PreemptionRecord = serde_json::from_str(pr_json).unwrap();
2000        let expected_pr: PreemptionRecord = serde_json::from_str(expected_pr_json).unwrap();
2001
2002        let pr_with_latest_removed = pr.with_latest_preempt_removed();
2003
2004        pr_with_latest_removed.validate().unwrap();
2005        self::assert_eq!(pr_with_latest_removed, expected_pr);
2006    }
2007
2008    /// Prefix trials must keep the recording's replay settings and host
2009    /// inputs even when none, or only some, of its events are replayed.
2010    #[test]
2011    fn a_schedule_prefix_keeps_the_recorded_replay_settings_and_host_inputs() {
2012        use detcore_model::host_input::HostFileIdentity;
2013        use detcore_model::host_input::HostInput;
2014
2015        let tid = DetTid::from_raw(3);
2016        let mut record = PreemptionRecord::from_sched_events(vec![
2017            SchedEvent::branches(tid, 10),
2018            SchedEvent::branches(tid, 20),
2019        ])
2020        .with_cap_preemption_charge(true)
2021        .with_expired_wait_runs_charged_once(true);
2022        record.rng_seed = Some(1234567);
2023        record.host_inputs = Some(HostInputSet {
2024            digest: "ab".repeat(32),
2025            files: vec![HostInput {
2026                path: "/recorded/input".to_owned(),
2027                identity: HostFileIdentity {
2028                    dev: 2,
2029                    ino: 3,
2030                    size: 4,
2031                    mtime_sec: 5,
2032                    mtime_nsec: 6,
2033                },
2034                content: Some("cd".repeat(32)),
2035            }],
2036        });
2037        for len in [0, 1, 2, 9] {
2038            let prefix = record.schedule_prefix(len);
2039            assert_eq!(prefix.global, record.global[..len.min(record.global.len())]);
2040            assert_eq!(
2041                (
2042                    prefix.cap_preemption_charge,
2043                    prefix.rng_seed,
2044                    prefix.expired_wait_runs_charged_once,
2045                    &prefix.host_inputs,
2046                ),
2047                (true, Some(1234567), true, &record.host_inputs),
2048                "prefix of {len} events"
2049            );
2050        }
2051    }
2052
2053    /// A schedule prefix keeps the record's format and the jumps of global
2054    /// time its events reach, as analyze and bisect carry them: up to and
2055    /// including one at the prefix's end, none beyond it.
2056    #[test]
2057    fn a_schedule_prefix_keeps_the_jumps_its_events_reach() {
2058        let tid = DetTid::from_raw(3);
2059        let jump = |event: u64| TimeSkip {
2060            event,
2061            target: LogicalTime::from_nanos(1_000 * (event + 1)),
2062            reason: TimeSkipReason::IdleGuestPollers,
2063        };
2064        let record = PreemptionRecord::from_sched_events(vec![
2065            SchedEvent::branches(tid, 10),
2066            SchedEvent::branches(tid, 20),
2067            SchedEvent::branches(tid, 30),
2068        ])
2069        .with_current_format()
2070        .with_time_skips(vec![jump(1), jump(2), jump(3)]);
2071        for (len, kept) in [
2072            (0, vec![]),
2073            (1, vec![jump(1)]),
2074            (2, vec![jump(1), jump(2)]),
2075            (9, vec![jump(1), jump(2), jump(3)]),
2076        ] {
2077            let prefix = record.schedule_prefix(len);
2078            assert!(prefix.lists_its_time_skips(), "prefix of {len} events");
2079            assert_eq!(
2080                prefix.time_skips().unwrap_or_default(),
2081                kept.as_slice(),
2082                "prefix of {len} events"
2083            );
2084        }
2085    }
2086
2087    /// The timeslice a `--chaos-timeslice-draw=run` run drew is a setting of
2088    /// the whole run, as the other run settings are: a prefix of the
2089    /// schedule keeps it, whether it is shorter or whole.
2090    #[test]
2091    fn a_schedule_prefix_keeps_the_drawn_timeslice() {
2092        let tid = DetTid::from_raw(3);
2093        let record = PreemptionRecord::from_sched_events(vec![
2094            SchedEvent::branches(tid, 10),
2095            SchedEvent::branches(tid, 20),
2096        ])
2097        .with_chaos_timeslice(Some(4321));
2098        for len in [0, 1, 2, 9] {
2099            assert_eq!(
2100                record.schedule_prefix(len).chaos_timeslice,
2101                Some(4321),
2102                "prefix of {len} events"
2103            );
2104        }
2105    }
2106
2107    /// A replay of a schedule's first events keeps the callback checkpoints
2108    /// of the slices those events end, for the threads they run, and no later
2109    /// ones: the slice end at the prefix's own end belongs to the event after
2110    /// it. The result still validates, and lends its checkpoints to a
2111    /// schedule that replayed it and went on.
2112    #[test]
2113    fn a_schedule_prefix_keeps_the_checkpoints_of_the_slices_its_events_end() {
2114        use callback_history::Checkpoint;
2115        use callback_history::Origin;
2116        use callback_history::Site;
2117
2118        use crate::slice_boundary::callbacks::Callback;
2119        let (main, other, late) = (
2120            DetTid::from_raw(3),
2121            DetTid::from_raw(5),
2122            DetTid::from_raw(7),
2123        );
2124        // Progress per thread: main 10, 11 (after the OtherInstructions), 21;
2125        // other 4; late 6.
2126        let mut record = PreemptionRecord::from_sched_events(vec![
2127            SchedEvent::branches(main, 10),
2128            SchedEvent::branches(other, 4),
2129            SchedEvent {
2130                op: Op::OtherInstructions,
2131                ..SchedEvent::branches(main, 1)
2132            },
2133            SchedEvent::branches(main, 10),
2134            SchedEvent::branches(late, 6),
2135        ]);
2136        let mut point = |tid: DetTid, row: usize, progress: u64, last_op: Op| {
2137            let time = LogicalTime::from_nanos(100 * (row as u64 + 1));
2138            let history = record.per_thread.entry(tid).or_default();
2139            history.prio_changes.push((time, 1000));
2140            history.preemption_rcbs.push(progress);
2141            Checkpoint {
2142                history_index: row,
2143                origin: Origin {
2144                    callback: Callback::PmuTimer,
2145                    site: Site::Prehook,
2146                    progress,
2147                    last_op: Some(last_op),
2148                    time,
2149                    rcbs: progress,
2150                },
2151            }
2152        };
2153        let main_points = vec![
2154            point(main, 0, 10, Op::Branch),
2155            point(main, 1, 11, Op::OtherInstructions),
2156            point(main, 2, 21, Op::Branch),
2157            // A later callback at the same progress, after the schedule ends.
2158            point(main, 3, 21, Op::Branch),
2159        ];
2160        let other_points = vec![point(other, 0, 4, Op::Branch)];
2161        let late_points = vec![point(late, 0, 6, Op::Branch)];
2162        record
2163            .callback_checkpoints
2164            .insert(main, main_points.clone());
2165        record
2166            .callback_checkpoints
2167            .insert(other, other_points.clone());
2168        record
2169            .callback_checkpoints
2170            .insert(late, late_points.clone());
2171        record.validate().unwrap();
2172
2173        // An exact interpolation keeps the histories/checkpoints themselves,
2174        // not a reconstructed record that happens to have the same events.
2175        let exact = record
2176            .interpolate_events(&record, record.global.clone())
2177            .unwrap();
2178        assert_eq!(
2179            serde_json::to_value(&exact).unwrap(),
2180            serde_json::to_value(&record).unwrap()
2181        );
2182        let mut other_record = record.clone();
2183        other_record.global.push(SchedEvent::branches(late, 1));
2184        let mut mixed = record.global.clone();
2185        mixed.push(SchedEvent::branches(main, 1));
2186        assert!(
2187            record
2188                .interpolate_events(&other_record, mixed)
2189                .unwrap_err()
2190                .contains("callback checkpoints")
2191        );
2192
2193        let kept = |len: usize| {
2194            let prefix = record.schedule_prefix(len);
2195            prefix.validate().unwrap();
2196            assert_eq!(prefix.global, record.global[..len]);
2197            prefix.callback_checkpoints
2198        };
2199        assert!(kept(0).is_empty());
2200        // Each prefix of 1 to 4 events stops where its last event's slice
2201        // ends (main's at 10, other's at 4, main's at 11, main's two at 21),
2202        // so it drops those checkpoints and keeps the earlier ones.
2203        assert!(kept(1).is_empty());
2204        assert_eq!(kept(2), BTreeMap::from([(main, main_points[..1].to_vec())]));
2205        assert_eq!(
2206            kept(3),
2207            BTreeMap::from([
2208                (main, main_points[..1].to_vec()),
2209                (other, other_points.clone()),
2210            ])
2211        );
2212        assert_eq!(
2213            kept(4),
2214            BTreeMap::from([
2215                (main, main_points[..2].to_vec()),
2216                (other, other_points.clone()),
2217            ])
2218        );
2219        // The whole schedule has no event after it, nor has a longer prefix.
2220        assert_eq!(kept(5), record.callback_checkpoints);
2221        assert_eq!(
2222            record.schedule_prefix(9).callback_checkpoints,
2223            record.callback_checkpoints
2224        );
2225
2226        // A schedule that replayed the prefix and went on takes the prefix's
2227        // checkpoints; one whose events they do not fit does not.
2228        let prefix = record.schedule_prefix(3);
2229        let mut continued = record.global[..3].to_vec();
2230        continued.push(SchedEvent::branches(other, 9));
2231        assert_eq!(
2232            prefix
2233                .with_schedule(continued.clone())
2234                .map(|record| (record.global, record.callback_checkpoints)),
2235            Some((continued, prefix.callback_checkpoints.clone()))
2236        );
2237        assert!(prefix.with_schedule(Vec::new()).is_none());
2238    }
2239
2240    #[test]
2241    fn callback_checkpoint_metadata_validates_and_only_time_stripping_preserves_it() {
2242        use callback_history::Checkpoint;
2243        use callback_history::Origin;
2244        use callback_history::Site;
2245        use reverie::syscalls::Sysno;
2246
2247        use crate::slice_boundary::callbacks::Callback;
2248        let tid = DetTid::from_raw(3);
2249        let mut record = PreemptionRecord::from_sched_events(vec![SchedEvent::branches(tid, 10)]);
2250        let time = LogicalTime::from_nanos(100);
2251        let mut history = ThreadHistory::new();
2252        history.prio_changes.push((time, 1000));
2253        history.preemption_rcbs.push(10);
2254        record.per_thread.insert(tid, history);
2255        let point = Checkpoint {
2256            history_index: 0,
2257            origin: Origin {
2258                callback: Callback::Syscall(Sysno::close),
2259                site: Site::Posthook,
2260                progress: 10,
2261                last_op: Some(Op::Branch),
2262                time,
2263                rcbs: 10,
2264            },
2265        };
2266        record.callback_checkpoints.insert(tid, vec![point.clone()]);
2267        record.validate().unwrap();
2268        let mut stripped = record.clone();
2269        stripped.strip_event_times();
2270        assert_eq!(stripped.callback_checkpoints, record.callback_checkpoints);
2271        stripped.validate().unwrap();
2272        assert_eq!(
2273            record.normalize().callback_checkpoints,
2274            record.callback_checkpoints
2275        );
2276        for edited in [
2277            record.clone().strip_contents(),
2278            record.clone_preemptions_only(),
2279            record.with_latest_preempt_removed(),
2280        ] {
2281            assert!(edited.callback_checkpoints.is_empty());
2282        }
2283        let mut edited = record.clone();
2284        edited.split_map(|event, _| [event]);
2285        assert!(edited.callback_checkpoints.is_empty());
2286        let mut edited = record.clone();
2287        let _ = edited.schedevents_iter_mut();
2288        assert!(edited.callback_checkpoints.is_empty());
2289        let mut edited = record.clone();
2290        edited.preemptions_only();
2291        assert!(edited.callback_checkpoints.is_empty());
2292        for field in ["history_index", "progress", "time", "rcbs"] {
2293            let mut bad = record.clone();
2294            let point = &mut bad.callback_checkpoints.get_mut(&tid).unwrap()[0];
2295            match field {
2296                "history_index" => point.history_index = 1,
2297                "progress" => point.origin.progress = 9,
2298                "time" => point.origin.time = LogicalTime::from_nanos(101),
2299                "rcbs" => point.origin.rcbs = 11,
2300                _ => unreachable!(),
2301            }
2302            assert!(bad.validate().is_err(), "{field}");
2303        }
2304        let mut bad = record.clone();
2305        bad.callback_checkpoints.get_mut(&tid).unwrap().push(point);
2306        assert!(bad.validate().is_err());
2307        let encoded = serde_json::to_string(&record).unwrap();
2308        let decoded: PreemptionRecord = serde_json::from_str(&encoded).unwrap();
2309        assert_eq!(decoded, record);
2310        let empty = PreemptionRecord::from_sched_events(Vec::new());
2311        assert_eq!(
2312            serde_json::to_string(&empty).unwrap(),
2313            r#"{"per_thread":{},"global":[]}"#
2314        );
2315        let old: PreemptionRecord =
2316            serde_json::from_str(r#"{"per_thread":{},"global":[]}"#).unwrap();
2317        assert_eq!(old, empty);
2318        let bad = encoded.replace("Posthook", "UnknownHook");
2319        assert!(serde_json::from_str::<PreemptionRecord>(&bad).is_err());
2320    }
2321
2322    #[test]
2323    fn test_split_map() {
2324        let mut pr: PreemptionRecord = serde_json::from_str(
2325            r#"
2326            {
2327                "per_thread" : {
2328                },
2329                "global" : [
2330                    {
2331                        "dettid": 3,
2332                        "op": "OtherInstructions",
2333                        "count": 1,
2334                        "start_rip": null,
2335                        "end_rip": null,
2336                        "end_time": 946684799000000000
2337                    },
2338                    {
2339                        "dettid": 3,
2340                        "op": "Branch",
2341                        "count": 311,
2342                        "start_rip": null,
2343                        "end_rip": null,
2344                        "end_time": 946684799000003110
2345                    },
2346                    {
2347                        "dettid": 3,
2348                        "op": "OtherInstructions",
2349                        "count": 1,
2350                        "start_rip": null,
2351                        "end_rip": null,
2352                        "end_time": 946684799000003110
2353                    }
2354                ]
2355            }
2356        "#,
2357        )
2358        .unwrap();
2359        let original = pr.global.clone();
2360        pr.split_map(|e, _| vec![e]); // shouldn't change contents with this
2361        assert_eq!(original, pr.global);
2362
2363        pr.split_map(|e, _| match &e {
2364            // this will split event with Op::Branch into two
2365            SchedEvent { op: Op::Branch, .. } => vec![e.clone(), e],
2366            _ => vec![e],
2367        });
2368
2369        assert_eq!(
2370            pr.global.iter().map(|e| e.op).collect::<Vec<_>>(),
2371            vec![
2372                Op::OtherInstructions,
2373                Op::Branch,
2374                Op::Branch,
2375                Op::OtherInstructions
2376            ]
2377        );
2378
2379        pr.split_map(|_, _| vec![]); // this will remove all entries
2380        assert_eq!(pr.global, Vec::new());
2381    }
2382}
2383
2384// Reader and Writer implementations:
2385// =============================================================================
2386
2387/// A writer for a stream of preemption events.
2388#[derive(Debug)]
2389pub struct PreemptionWriter {
2390    inner: PreemptionRecord,
2391    reserved_input_qualification: reserved_input::ProducerQualification,
2392    boundary_records: Option<Vec<detcore_model::schedule::boundary::Record>>,
2393    dest: Option<PathBuf>,
2394    flushed: bool,
2395}
2396
2397impl PreemptionWriter {
2398    /// A new, empty record of preemptions, with an optional location on disk that it will be
2399    /// written to.  If no path is supplied, the results will accumulate in memory only.
2400    pub fn new(path: Option<PathBuf>) -> Self {
2401        PreemptionWriter {
2402            inner: Default::default(),
2403            reserved_input_qualification: Default::default(),
2404            boundary_records: None,
2405            dest: path,
2406            flushed: false,
2407        }
2408    }
2409
2410    /// Observe the actual authenticated target-image setup through the local
2411    /// GlobalTool hook, never requested Config or a launcher bootstrap. A failed
2412    /// or unproved executed image permanently removes this producer's mark.
2413    pub(crate) fn observe_reserved_input_setup(
2414        &mut self,
2415        receipt: &reverie::reserved_input::InputSetupReceipt,
2416    ) -> Result<(), RecordAdmissionRefusal> {
2417        use reverie::reserved_input::InputAbi;
2418        use reverie::reserved_input::InputSetupOutcome;
2419        let layout = match &receipt.outcome {
2420            InputSetupOutcome::Qualified(layout) => {
2421                Some(ReservedInputLayout::from_authenticated_setup(
2422                    match layout.abi {
2423                        InputAbi::X86_64 => "x86_64",
2424                        InputAbi::Aarch64 => "aarch64",
2425                    },
2426                    layout.base,
2427                    layout.page_bytes as u64,
2428                ))
2429            }
2430            InputSetupOutcome::Unqualified(_) => None,
2431        };
2432        match layout.transpose() {
2433            Ok(layout) => {
2434                self.inner.reserved_input = self.reserved_input_qualification.observe(layout);
2435                Ok(())
2436            }
2437            Err(refusal) => {
2438                self.inner.reserved_input = self.reserved_input_qualification.observe(None);
2439                Err(refusal)
2440            }
2441        }
2442    }
2443
2444    /// Write the record in `format` (`None` for format 1): the newest
2445    /// ([`PREEMPTION_RECORD_FORMAT`]) for a run that follows no record, or the
2446    /// format of the record a replay follows, whose rules it ran by.
2447    pub fn with_format(mut self, format: Option<u32>) -> Self {
2448        self.inner.format_version = format;
2449        self
2450    }
2451
2452    /// Record a jump of global time (`PreemptionRecord::time_skips`), which
2453    /// puts the record in format 2 at least ([`TIME_SKIPS_FORMAT`]).
2454    pub fn insert_time_skip(&mut self, skip: TimeSkip) {
2455        if !self.inner.lists_its_time_skips() {
2456            self.inner.format_version = Some(TIME_SKIPS_FORMAT);
2457        }
2458        self.inner
2459            .time_skips
2460            .get_or_insert_with(Vec::new)
2461            .push(skip);
2462    }
2463
2464    /// The position the next stored record takes in the recorded stream:
2465    /// where a jump made now falls (`TimeSkip::event`).
2466    pub fn stream_position(&self) -> u64 {
2467        let position = match &self.boundary_records {
2468            Some(records) => records.len(),
2469            None => self.inner.global.len(),
2470        };
2471        position as u64
2472    }
2473
2474    /// Select the explicit boundary protocol before recording any global
2475    /// event. Metadata stays in the legacy history, with exactly one global
2476    /// stream in the new envelope. Existing writers remain legacy by default.
2477    pub fn with_slice_boundaries(mut self) -> Result<Self, String> {
2478        if !self.inner.global.is_empty() || self.boundary_records.is_some() {
2479            self.flushed = true;
2480            return Err("slice-boundary protocol must be selected before recording events".into());
2481        }
2482        self.boundary_records = Some(Vec::new());
2483        Ok(self)
2484    }
2485
2486    /// Whether this writer must retain the complete versioned boundary stream.
2487    pub fn records_slice_boundaries(&self) -> bool {
2488        self.boundary_records.is_some()
2489    }
2490
2491    /// The next complete stored index, before accepting an annotation. The
2492    /// controller validates index/stack requests before consuming its cursor.
2493    pub(crate) fn next_boundary_index(&self) -> Option<usize> {
2494        self.boundary_records.as_ref().map(Vec::len)
2495    }
2496
2497    /// Store the virtual-time epoch the recorded logical times are measured
2498    /// from, so a replay can start its clock at the same instant.
2499    pub fn with_epoch(mut self, epoch: DateTime<Utc>) -> Self {
2500        self.inner.epoch = Some(epoch);
2501        self
2502    }
2503
2504    /// Store the run's `--pipe-wake-latency` (`None` for off, which leaves
2505    /// the record as it was before the field existed).
2506    pub fn with_pipe_wake_latency(mut self, latency: Option<u64>) -> Self {
2507        self.inner.pipe_wake_latency = latency;
2508        self
2509    }
2510
2511    /// Store whether the run kept the signal-state copy
2512    /// (`Config::signal_state_copy`).
2513    pub fn with_signal_state_copy(mut self, on: bool) -> Self {
2514        self.inner.signal_state_copy = Some(on);
2515        self
2516    }
2517
2518    /// Store the seed of the guest's randomness, so a replay can hand the guest
2519    /// the same values.
2520    pub fn with_rng_seed(mut self, rng_seed: u64) -> Self {
2521        self.inner.rng_seed = Some(rng_seed);
2522        self
2523    }
2524
2525    /// Store whether the run charged a run of expired timed waits one turn
2526    /// (`Scheduler::expired_wait_runs_charged_once`; `false` leaves the record
2527    /// as it was before the field existed).
2528    pub fn with_expired_wait_runs_charged_once(mut self, once: bool) -> Self {
2529        self.inner.expired_wait_runs_charged_once = once;
2530        self
2531    }
2532
2533    /// Store whether the run capped preemption charges
2534    /// (`--cap-preemption-charge`; off leaves the record as it was before
2535    /// the field existed).
2536    pub fn with_cap_preemption_charge(mut self, on: bool) -> Self {
2537        self.inner.cap_preemption_charge = on;
2538        self
2539    }
2540
2541    /// Store the host files the run's guest opened, once the run has ended
2542    /// and before the record is flushed.
2543    pub fn set_host_inputs(&mut self, inputs: HostInputSet) {
2544        self.inner.host_inputs = Some(inputs);
2545    }
2546
2547    /// Store the store-exclusives that failed in the run (aarch64), counted
2548    /// once every thread has exited.
2549    pub fn record_store_exclusive_failures(&mut self, failures: u64) {
2550        self.inner.store_exclusive_failures = HostObservedCount(failures);
2551    }
2552
2553    /// Store the `--max-timeslice` the run drew under `--chaos-timeslice-draw=run`
2554    /// (`Config::chaos_timeslice`; `None` writes nothing).
2555    pub fn with_chaos_timeslice(mut self, timeslice: Option<std::num::NonZeroU64>) -> Self {
2556        self.inner.chaos_timeslice = timeslice.map(std::num::NonZeroU64::get);
2557        self
2558    }
2559
2560    /// Store how the run numbered its guest (`Config::guest_ids`), unless its
2561    /// PID namespace did, which leaves the record as it was before the field
2562    /// existed.
2563    pub fn with_guest_ids(mut self, ids: detcore_model::pid::GuestIds) -> Self {
2564        self.set_guest_ids(ids);
2565        self
2566    }
2567
2568    /// [`Self::with_guest_ids`] on a writer in use: a run whose guest got a
2569    /// host ID after all states `GuestIds::Host`.
2570    pub fn set_guest_ids(&mut self, ids: detcore_model::pid::GuestIds) {
2571        self.inner.guest_ids = (ids != detcore_model::pid::GuestIds::Namespace).then_some(ids);
2572    }
2573
2574    /// Does the record have zero entries?
2575    pub fn is_empty(&self) -> bool {
2576        self.inner.per_thread.is_empty()
2577    }
2578
2579    /// Total number of elements in the record, across all threads.
2580    pub fn len(&self) -> usize {
2581        let mut count = 0;
2582        for v in self.inner.per_thread.values() {
2583            count += v.prio_changes.len() + v.chaos_epochs.len();
2584        }
2585        count
2586    }
2587
2588    /// Register a thread with its default/final priority.
2589    pub fn register_thread(&mut self, tid: DetTid, prio: Priority) {
2590        if self
2591            .inner
2592            .per_thread
2593            .insert(
2594                tid,
2595                ThreadHistory {
2596                    final_prio: prio,
2597                    prio_changes: Vec::new(),
2598                    preemption_rcbs: Vec::new(),
2599                    chaos_epochs: Vec::new(),
2600                },
2601            )
2602            .is_some()
2603        {
2604            panic!(
2605                "PreemptionRecord: error, cannot re-register thread id already registered: {}",
2606                tid
2607            )
2608        }
2609    }
2610
2611    /// Insert a new preemption point for the given thread.
2612    /// It must monotonically increase in time.
2613    ///
2614    /// For now it (redundantly) takes both the priority of the timeslice just finished,
2615    /// and the next one coming up.  This serves as a sanity check.
2616    pub fn insert_reprioritization(
2617        &mut self,
2618        tid: DetTid,
2619        time: LogicalTime,
2620        rcbs: u64,
2621        prior_prio: Priority,
2622        next_prio: Priority,
2623    ) {
2624        let history = self.inner.per_thread.get_mut(&tid).unwrap_or_else(|| {
2625            panic!(
2626                "PreemptionRecord: Cannot insert a preemption before registering thread {}",
2627                tid
2628            )
2629        });
2630        assert_eq!(history.final_prio, prior_prio);
2631
2632        if let Some((last, _prio)) = history.prio_changes.last() {
2633            assert!(&time > last);
2634        }
2635        history.prio_changes.push((time, prior_prio));
2636        history.preemption_rcbs.push(rcbs);
2637        history.final_prio = next_prio;
2638    }
2639
2640    /// Mark this record as written by a preemption replay that left a change
2641    /// point out of it, first at global event count `event`
2642    /// ([`PreemptionRecord::replay_departed_at`]). Warns once, naming the
2643    /// file, so nobody replays it as an exact record.
2644    pub fn mark_replay_departed(&mut self, event: u64) {
2645        if self.inner.replay_departed_at.is_some() {
2646            return;
2647        }
2648        self.inner.replay_departed_at = Some(event);
2649        if let Some(dest) = &self.dest {
2650            tracing::warn!(
2651                "the preemption record {} retains an omitted replay change point observed at source \
2652                 event {}; replay_departed_at preserves that provenance and faithful replay refuses it",
2653                dest.display(),
2654                event
2655            );
2656        }
2657    }
2658
2659    /// The time of the last change point recorded for `tid`, if any. A new one
2660    /// must be strictly later ([`Self::insert_reprioritization`]).
2661    pub fn last_reprioritization_time(&self, tid: DetTid) -> Option<LogicalTime> {
2662        self.inner
2663            .per_thread
2664            .get(&tid)
2665            .and_then(|history| history.prio_changes.last())
2666            .map(|(time, _)| *time)
2667    }
2668
2669    /// How many change points are recorded for `tid`.
2670    pub(crate) fn reprioritization_count(&self, tid: DetTid) -> usize {
2671        self.inner
2672            .per_thread
2673            .get(&tid)
2674            .map_or(0, |history| history.prio_changes.len())
2675    }
2676
2677    /// Attach only an observed callback expiry to the row the original helper just wrote.
2678    pub(crate) fn insert_callback_checkpoint(
2679        &mut self,
2680        tid: DetTid,
2681        origin: callback_history::Origin,
2682    ) {
2683        let history = &self.inner.per_thread[&tid];
2684        let history_index = history
2685            .prio_changes
2686            .len()
2687            .checked_sub(1)
2688            .expect("callback follows priority row");
2689        assert_eq!(history.prio_changes[history_index].0, origin.time);
2690        assert_eq!(history.preemption_rcbs[history_index], origin.rcbs);
2691        self.inner
2692            .callback_checkpoints
2693            .entry(tid)
2694            .or_default()
2695            .push(callback_history::Checkpoint {
2696                history_index,
2697                origin,
2698            });
2699    }
2700
2701    // AUTONOMOUS-BOT-IMPLEMENTED
2702    // TODO-HUMAN-REVIEW(PR-1151)
2703    /// Record a deterministic slowdown epoch transition for replay.
2704    pub fn insert_chaos_epoch(&mut self, tid: DetTid, transition: ChaosEpochTransition) {
2705        let history = self.inner.per_thread.get_mut(&tid).unwrap_or_else(|| {
2706            panic!(
2707                "PreemptionRecord: Cannot insert a chaos epoch before registering thread {}",
2708                tid
2709            )
2710        });
2711        if let Some(last) = history.chaos_epochs.last() {
2712            assert!(transition.logical_time > last.logical_time);
2713            assert!(transition.epoch > last.epoch);
2714        }
2715        history.chaos_epochs.push(transition);
2716    }
2717
2718    /// Add a SchedEvent to the global log of thread behavior.
2719    pub fn insert_schedevent(&mut self, ev: SchedEvent) {
2720        if let Some(records) = &mut self.boundary_records {
2721            assert!(
2722                ev.count > 0,
2723                "slice-boundary protocol refuses zero-count events"
2724            );
2725            records.push(detcore_model::schedule::boundary::Record::Event(ev));
2726            return;
2727        }
2728        if ev.count > 0 {
2729            self.inner.global.push(ev)
2730            // TODO, aggregation: possibly check if the last event was the SAME, and combine them,
2731            // increasing the count.
2732        } else {
2733            trace!("NOT recording scheduled event with zero count!");
2734        }
2735    }
2736
2737    /// Append the observation of an existing slice-end request at its exact
2738    /// stream position. Refuse use with a legacy writer rather than silently
2739    /// dropping the boundary. Full stream ordering is checked before output.
2740    pub fn insert_slice_boundary(
2741        &mut self,
2742        boundary: detcore_model::schedule::boundary::Boundary,
2743    ) -> Result<usize, String> {
2744        let records = self
2745            .boundary_records
2746            .as_mut()
2747            .ok_or_else(|| "cannot append a slice boundary to a legacy recording".to_string())?;
2748        let index = records.len();
2749        boundary.validate_shape(index).map_err(|e| e.to_string())?;
2750        records.push(detcore_model::schedule::boundary::Record::TimesliceEnd(
2751            boundary,
2752        ));
2753        Ok(index)
2754    }
2755
2756    /// Current recorded priority of a genuinely registered thread. During plain
2757    /// raw-schedule replay this is logical state, not a dispatch-control band.
2758    pub(crate) fn current_priority(&self, tid: DetTid) -> Priority {
2759        self.inner
2760            .per_thread
2761            .get(&tid)
2762            .unwrap_or_else(|| {
2763                panic!(
2764                    "PreemptionRecord: Cannot read current priority before registering thread {}",
2765                    tid
2766                )
2767            })
2768            .final_prio
2769    }
2770
2771    /// Set the priority of the current thread after a change.
2772    pub fn set_current(&mut self, tid: DetTid, new_prio: Priority) {
2773        let history = self.inner.per_thread.get_mut(&tid).unwrap_or_else(|| {
2774            panic!(
2775                "PreemptionRecord: Cannot set current priority before registering thread {}",
2776                tid
2777            )
2778        });
2779        history.final_prio = new_prio;
2780    }
2781
2782    /// Abort writing to disk and instead gather the output thusfar into a string.
2783    pub fn into_string(mut self) -> String {
2784        self.flushed = true;
2785        self.serialized_string()
2786            .expect("invalid preemption recording")
2787    }
2788
2789    /// Finish any IO necessary to flush the record to disk.
2790    /// This will also happen automatically on drop.
2791    pub fn flush(mut self) -> Result<(), String> {
2792        self.flushed = true;
2793        self.write_to_disk()
2794    }
2795
2796    fn write_to_disk(&mut self) -> Result<(), String> {
2797        if let Some(path) = &self.dest {
2798            if self.boundary_records.is_none() {
2799                return self.inner.write_to_disk(path);
2800            }
2801            // Validate the complete stream before opening an existing file.
2802            let mut bytes = self.serialized_string()?.into_bytes();
2803            bytes.push(b'\n');
2804            std::fs::write(path, bytes)
2805                .map_err(|e| format!("Failed to write boundary recording {}: {e}", path.display()))
2806        } else {
2807            Err(
2808                "Cannot write_to_disk because this PreemptionWriter was created without a backing file.".to_string()
2809            )
2810        }
2811    }
2812
2813    fn serialized_string(&self) -> Result<String, String> {
2814        let Some(records) = &self.boundary_records else {
2815            return Ok(self.inner.to_string());
2816        };
2817        let schedule = detcore_model::schedule::boundary::Schedule::new(records.clone())
2818            .map_err(|e| e.to_string())?;
2819        let recording = boundary_recording::Recording::Boundaries(
2820            boundary_recording::BoundaryRecording::new(self.inner.clone(), schedule)
2821                .map_err(|e| e.to_string())?,
2822        );
2823        let mut bytes = recording.encode().map_err(|e| e.to_string())?;
2824        assert_eq!(bytes.pop(), Some(b'\n'));
2825        String::from_utf8(bytes).map_err(|e| e.to_string())
2826    }
2827}
2828
2829impl Drop for PreemptionWriter {
2830    fn drop(&mut self) {
2831        if !self.flushed
2832            && self.dest.is_some()
2833            && let Err(e) = self.write_to_disk()
2834        {
2835            panic!("Error while dropping PreemptionWriter: {}", e);
2836        }
2837    }
2838}
2839
2840/// Read only whether a preemption record was written by a replay that left a
2841/// change point out of it ([`PreemptionRecord::replay_departed_at`]).
2842pub fn read_recorded_replay_departure(path: &Path) -> Result<Option<u64>, String> {
2843    #[derive(Deserialize)]
2844    struct DepartureOnly {
2845        #[serde(default)]
2846        replay_departed_at: Option<u64>,
2847    }
2848    Ok(boundary_recording::read_settings::<DepartureOnly>(path)?.replay_departed_at)
2849}
2850
2851/// A reader for a stream of preemption events.
2852#[derive(Debug)]
2853pub struct PreemptionReader {
2854    inner: PreemptionRecord,
2855}
2856
2857// Deprecated: use schedevents()
2858/// Read a full trace from disk.  Panic if it doesn't load.
2859pub fn read_trace(path: &Path) -> Vec<SchedEvent> {
2860    let pr = read_preemption_record(path);
2861    pr.global
2862}
2863
2864// TODO: we should implement streaming and not read this all at once.
2865fn read_preemption_record(path: &Path) -> PreemptionRecord {
2866    let string = std::fs::read_to_string(path)
2867        .unwrap_or_else(|e| panic!("Error reading file {:?}:\n {}", path, e));
2868    let pr: PreemptionRecord = serde_json::from_str(&string).unwrap_or_else(|e| {
2869        panic!(
2870            "Error parsing PreemptionRecord from JSON: {}\nJSON contents:\n{}",
2871            e, string
2872        )
2873    });
2874    if let Err(e) = pr.validate() {
2875        panic!(
2876            "Invalid PreemptionRecord when loading from path {}. Error:\n {}",
2877            path.display(),
2878            e
2879        );
2880    }
2881    pr
2882}
2883
2884/// Read only the virtual-time epoch stored in a preemption record file.
2885///
2886/// Unlike [`PreemptionReader::new`], this reports an unreadable or malformed
2887/// file as an error instead of panicking, because the CLI consults it before
2888/// any guest starts. `Ok(None)` means the record predates stored epochs.
2889pub fn read_recorded_epoch(path: &Path) -> Result<Option<DateTime<Utc>>, String> {
2890    #[derive(Deserialize)]
2891    struct EpochOnly {
2892        #[serde(default)]
2893        epoch: Option<DateTime<Utc>>,
2894    }
2895    Ok(boundary_recording::read_settings::<EpochOnly>(path)?.epoch)
2896}
2897
2898/// Read only the `--rng-seed` stored in a preemption record file, as
2899/// [`read_recorded_epoch`] reads the epoch. `Ok(None)` means the record predates
2900/// the field.
2901pub fn read_recorded_rng_seed(path: &Path) -> Result<Option<u64>, String> {
2902    #[derive(Deserialize)]
2903    struct RngSeedOnly {
2904        #[serde(default)]
2905        rng_seed: Option<u64>,
2906    }
2907    Ok(boundary_recording::read_settings::<RngSeedOnly>(path)?.rng_seed)
2908}
2909
2910/// Read only the `--pipe-wake-latency` stored in a preemption record file, as
2911/// [`read_recorded_epoch`] reads the epoch. `Ok(None)` means the record was
2912/// made with the option off, or before the field existed.
2913pub fn read_recorded_pipe_wake_latency(path: &Path) -> Result<Option<u64>, String> {
2914    #[derive(Deserialize)]
2915    struct LatencyOnly {
2916        #[serde(default)]
2917        pipe_wake_latency: Option<u64>,
2918    }
2919    Ok(boundary_recording::read_settings::<LatencyOnly>(path)?.pipe_wake_latency)
2920}
2921
2922/// The refusal of a replay whose `--pipe-wake-latency` differs from the one a
2923/// record stores, naming the record by `source` (an option and a path), or
2924/// `None` when they agree. Absent and off are the same; `Some(0)` is not off.
2925pub fn pipe_wake_latency_mismatch(
2926    source: &str,
2927    recorded: Option<u64>,
2928    replay: Option<u64>,
2929) -> Option<String> {
2930    let show = |latency: Option<u64>| match latency {
2931        Some(ns) => format!("--pipe-wake-latency={ns}"),
2932        None => "no --pipe-wake-latency".to_owned(),
2933    };
2934    (recorded != replay).then(|| {
2935        format!(
2936            "{source} was recorded with {} and would be replayed with {}. A hand-off moves \
2937             turns and the time they charge, so the replay cannot reproduce the run. Replay \
2938             with {}.",
2939            show(recorded),
2940            show(replay),
2941            show(recorded),
2942        )
2943    })
2944}
2945
2946/// Read only whether the run that wrote a preemption record file capped
2947/// preemption charges, as [`read_recorded_epoch`] reads the epoch. A record
2948/// without the field was made with the option off.
2949pub fn read_recorded_cap_preemption_charge(path: &Path) -> Result<bool, String> {
2950    #[derive(Deserialize)]
2951    struct CapOnly {
2952        #[serde(default)]
2953        cap_preemption_charge: bool,
2954    }
2955    Ok(boundary_recording::read_settings::<CapOnly>(path)?.cap_preemption_charge)
2956}
2957
2958/// The refusal of a replay whose `--cap-preemption-charge` differs from the
2959/// one a record stores, naming the record by `source` (an option and a
2960/// path), or `None` when they agree.
2961pub fn cap_preemption_charge_mismatch(
2962    source: &str,
2963    recorded: bool,
2964    replay: bool,
2965) -> Option<String> {
2966    let show = |on: bool| {
2967        if on {
2968            "--cap-preemption-charge"
2969        } else {
2970            "no --cap-preemption-charge"
2971        }
2972    };
2973    (recorded != replay).then(|| {
2974        format!(
2975            "{source} was recorded with {} and would be replayed with {}. The option changes \
2976             what each preemption charges the clock, so timers and timeouts move and the replay \
2977             cannot reproduce the run. Replay with {}.",
2978            show(recorded),
2979            show(replay),
2980            show(recorded),
2981        )
2982    })
2983}
2984
2985/// Read only whether the signal-state copy was on in the run that wrote a
2986/// preemption record file, as [`read_recorded_epoch`] reads the epoch.
2987/// `Ok(None)` for a record written before the field existed.
2988pub fn read_recorded_signal_state_copy(path: &Path) -> Result<Option<bool>, String> {
2989    #[derive(Deserialize)]
2990    struct CopyOnly {
2991        #[serde(default)]
2992        signal_state_copy: Option<bool>,
2993    }
2994    Ok(boundary_recording::read_settings::<CopyOnly>(path)?.signal_state_copy)
2995}
2996
2997/// Read only whether a preemption record file carries callback checkpoints
2998/// (`Config::annotated_priority_replay`), as [`read_recorded_epoch`] reads
2999/// the epoch. A record without the field, older or made without chaos
3000/// recording, carries none. Detcore validates the whole record, checkpoints
3001/// included, when it loads it for the run.
3002pub fn read_recorded_callback_checkpoints(path: &Path) -> Result<bool, String> {
3003    #[derive(Deserialize)]
3004    struct CheckpointsOnly {
3005        #[serde(default)]
3006        callback_checkpoints: BTreeMap<String, Vec<serde_json::Value>>,
3007    }
3008    Ok(boundary_recording::read_settings::<CheckpointsOnly>(path)?
3009        .callback_checkpoints
3010        .values()
3011        .any(|points| !points.is_empty()))
3012}
3013
3014/// Detcore's own refusal of a run prepared to replay a callback-annotated
3015/// priority history (`Config::annotated_priority_replay`) on an effective
3016/// configuration that cannot honor its row ends
3017/// (`Config::annotated_priority_replay_unqualified`), checked before the
3018/// scheduler or any history exists. Hosts refuse earlier, by name; this
3019/// backstop holds for every configuration that reaches Detcore, whoever
3020/// prepared it. `None` for an unannotated run or a qualified one.
3021pub fn annotated_replay_mode_refusal(cfg: &crate::Config) -> Option<String> {
3022    if !cfg.annotated_priority_replay {
3023        return None;
3024    }
3025    cfg.annotated_priority_replay_unqualified().map(|setting| {
3026        format!(
3027            "HERMIT_CALLBACK_CHECKPOINT_UNQUALIFIED_MODE: the run was prepared to replay a \
3028             callback-annotated priority history, but its configuration {setting}; no thread \
3029             has started"
3030        )
3031    })
3032}
3033
3034/// Read only whether the run that wrote a preemption record file charged a
3035/// run of expired timed waits one turn, as [`read_recorded_epoch`] reads the
3036/// epoch. `Ok(false)` for a record that does not say: one written before the
3037/// field existed, or by a run under the `each` rule (by default race search),
3038/// both of which charged every wait.
3039pub fn read_recorded_expired_wait_runs_charged_once(path: &Path) -> Result<bool, String> {
3040    #[derive(Deserialize)]
3041    struct ChargeOnly {
3042        #[serde(default)]
3043        expired_wait_runs_charged_once: bool,
3044    }
3045    Ok(boundary_recording::read_settings::<ChargeOnly>(path)?.expired_wait_runs_charged_once)
3046}
3047
3048/// The refusal of a replay whose explicit `--expired-wait-charge` differs from
3049/// the rule a record states (`recorded_once`, as
3050/// [`read_recorded_expired_wait_runs_charged_once`] reads it), naming the
3051/// record by `source` (an option and a path), or `None` when they agree or the
3052/// replay leaves the option unset, and so takes the record's rule. For a
3053/// search (`hermit analyze`, `hermit bisect`), which runs every trial under
3054/// one rule, the remedy names the recordings instead.
3055pub fn expired_wait_charge_mismatch(
3056    source: &str,
3057    recorded_once: bool,
3058    replay: Option<detcore_model::config::ExpiredWaitCharge>,
3059    search: bool,
3060) -> Option<String> {
3061    let replay = replay?;
3062    let recorded = detcore_model::config::ExpiredWaitCharge::from_charged_once(recorded_once);
3063    (recorded != replay).then(|| {
3064        let remedy = if search {
3065            format!(
3066                "A search replays every trial under one rule, its target's unless its run \
3067                 arguments give one. Re-record this one under that rule \
3068                 (`hermit run --expired-wait-charge={} --record-preemptions-to=...`), or, if \
3069                 it is the target, pass --expired-wait-charge={} in the run arguments.",
3070                replay.as_str(),
3071                recorded.as_str()
3072            )
3073        } else {
3074            format!(
3075                "Replay with --expired-wait-charge={}, or leave the option out to take the \
3076                 record's.",
3077                recorded.as_str()
3078            )
3079        };
3080        format!(
3081            "{source} was recorded with --expired-wait-charge={} and would be replayed with \
3082             --expired-wait-charge={}. The rule decides how much virtual time expired timed \
3083             waits cost, and so which thread runs next, so the replay cannot reproduce the run. \
3084             {remedy}",
3085            recorded.as_str(),
3086            replay.as_str(),
3087        )
3088    })
3089}
3090
3091/// Read only the timeslice a preemption record file states its run drew under
3092/// `--chaos-timeslice-draw=run` (`PreemptionRecord::chaos_timeslice`), as
3093/// [`read_recorded_epoch`] reads the epoch. `Ok(None)` for a record without
3094/// one.
3095pub fn read_recorded_chaos_timeslice(path: &Path) -> Result<Option<u64>, String> {
3096    #[derive(Deserialize)]
3097    struct TimesliceOnly {
3098        #[serde(default)]
3099        chaos_timeslice: Option<u64>,
3100    }
3101    Ok(boundary_recording::read_settings::<TimesliceOnly>(path)?.chaos_timeslice)
3102}
3103
3104/// The `--max-timeslice` a chaos run with `--chaos-timeslice-draw=run` draws:
3105/// log-uniform over `range`, both bounds included, as a pure function of the
3106/// scheduler seed. A stream of its own (`CHAOS_TIMESLICE_SALT`) keeps it
3107/// independent of the seed's other draws, so adding the option changes no
3108/// other random choice of the run.
3109pub fn draw_chaos_timeslice(
3110    range: detcore_model::config::TimesliceRange,
3111    sched_seed: u64,
3112) -> std::num::NonZeroU64 {
3113    use rand::RngExt as _;
3114    use rand::SeedableRng as _;
3115    const CHAOS_TIMESLICE_SALT: u64 = 0x7469_6d65_736c_6963; // "timeslic"
3116    let mut prng = rand_pcg::Pcg64Mcg::seed_from_u64(sched_seed ^ CHAOS_TIMESLICE_SALT);
3117    let drawn = range.log_uniform(prng.random::<f64>()).round() as u64;
3118    std::num::NonZeroU64::new(drawn.clamp(range.min.get(), range.max.get()))
3119        .expect("the range starts at one RCB")
3120}
3121
3122/// The refusal of a replay whose signal-state copy setting differs from the
3123/// one a record stores, naming the record by `source` (an option and a path),
3124/// or `None` when they agree or the record does not say.
3125pub fn signal_state_copy_mismatch(
3126    source: &str,
3127    recorded: Option<bool>,
3128    replay: bool,
3129) -> Option<String> {
3130    let show = |on: bool| {
3131        if on {
3132            "the signal-state copy on"
3133        } else {
3134            "the signal-state copy off (--no-signal-state-copy)"
3135        }
3136    };
3137    let recorded = recorded?;
3138    (recorded != replay).then(|| {
3139        format!(
3140            "{source} was recorded with {} and would be replayed with {}. The copy decides \
3141             some signal questions and adds a stop at each handler return, which can move a \
3142             preemption, so the replay cannot be relied on to reproduce the run. Replay with {}.",
3143            show(recorded),
3144            show(replay),
3145            show(recorded),
3146        )
3147    })
3148}
3149
3150/// How the run that wrote the record at `path` numbered its guest, as the
3151/// record stores it: `GuestIds::Namespace` for a record that does not say,
3152/// which only a run its PID namespace numbered, or one before the field
3153/// existed, writes.
3154pub fn read_recorded_guest_ids(path: &Path) -> Result<detcore_model::pid::GuestIds, String> {
3155    #[derive(Deserialize)]
3156    struct IdsOnly {
3157        #[serde(default)]
3158        guest_ids: Option<detcore_model::pid::GuestIds>,
3159    }
3160    Ok(boundary_recording::read_settings::<IdsOnly>(path)?
3161        .guest_ids
3162        .unwrap_or_default())
3163}
3164
3165/// The refusal of a replay whose guest would be numbered otherwise than the
3166/// run that a record (named by `source`, an option and a path) came from,
3167/// or `None` when they number it alike (`GuestIds::numbers_like`).
3168pub fn guest_ids_mismatch(
3169    source: &str,
3170    recorded: detcore_model::pid::GuestIds,
3171    replay: detcore_model::pid::GuestIds,
3172) -> Option<String> {
3173    (!recorded.numbers_like(&replay)).then(|| {
3174        format!(
3175            "{source} was recorded with guest process IDs {recorded}, and this replay's would be \
3176             {replay}. The record names every thread by its ID, so the replay cannot follow it."
3177        )
3178    })
3179}
3180
3181// TODO: eventually this should do streaming IO, and abstract it behind this interface.
3182impl PreemptionReader {
3183    /// Fallible legacy access for editors. A boundary recording cannot be
3184    /// flattened into a legacy event vector, even if it has no markers.
3185    pub fn try_new(path: &Path) -> anyhow::Result<Self> {
3186        let inner =
3187            boundary_recording::Recording::read(path)?.into_legacy("legacy preemption reader")?;
3188        Ok(Self { inner })
3189    }
3190
3191    /// Access the stored `PreemptionRecord`, and eagerly or lazily load its data from disk.
3192    pub fn new(path: &Path) -> Self {
3193        let pr = read_preemption_record(path);
3194        PreemptionReader { inner: pr }
3195    }
3196
3197    /// Gets the inner `PreemptionRecord`.
3198    pub fn into_inner(self) -> PreemptionRecord {
3199        self.inner
3200    }
3201
3202    /// Extract a copy of all the entries for a particular thread.  Note that this returns None if
3203    /// there is NO record for the thread (not registered).  Otherwise, it returns a copy of the
3204    /// `ThreadHistory`, which may be read into memory or not.
3205    pub fn extract_thread_record(&self, tid: &DetTid) -> Option<ThreadHistory> {
3206        self.inner.per_thread.get(tid).cloned()
3207    }
3208
3209    /// Return the initial priority for a thread, if it is recorded in the record.
3210    pub fn thread_initial_priority(&self, tid: &DetTid) -> Option<Priority> {
3211        self.inner.per_thread.get(tid).map(|x| x.initial_priority())
3212    }
3213
3214    /// Return all the threads in this record.
3215    pub fn all_threads(&self) -> Vec<DetTid> {
3216        // TODO(T110956298): have this return an iterator.
3217        self.inner.per_thread.keys().copied().collect()
3218    }
3219
3220    /// Load the full record of preemptions into memory.
3221    pub fn load_all(&self) -> PreemptionRecord {
3222        self.inner.clone()
3223    }
3224
3225    /// Size in number of preemptions, across all threads.
3226    pub fn size(&self) -> usize {
3227        let mut sum = 0;
3228        for th in self.inner.per_thread.values() {
3229            sum += th.prio_changes.len()
3230        }
3231        sum
3232    }
3233}
3234
3235/// Take a file containing a PreemptionRecord and strip the times from all the events.
3236/// Optionally take a destination path, otherwise create a destination file in the same directory
3237/// with our own naming convention.
3238pub fn strip_times_from_events_file(
3239    sched_path: &Path,
3240    dest: Option<PathBuf>,
3241) -> anyhow::Result<PathBuf> {
3242    let new_path = dest.unwrap_or_else(|| sched_path.with_extension("notimes"));
3243    let mut recording = boundary_recording::Recording::read(sched_path)?;
3244    recording.strip_times();
3245    recording.write(&new_path)?;
3246    Ok(new_path)
3247}