1pub 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#[derive(PartialEq, Default, Debug, Eq, Clone, Hash)]
47pub struct PreemptionRecord {
48 per_thread: BTreeMap<DetTid, ThreadHistory>,
51 global: Vec<SchedEvent>,
52 epoch: Option<DateTime<Utc>>,
60 pipe_wake_latency: Option<u64>,
67 signal_state_copy: Option<bool>,
75 cap_preemption_charge: bool,
81 chaos_timeslice: Option<u64>,
88 guest_ids: Option<detcore_model::pid::GuestIds>,
95 replay_departed_at: Option<u64>,
108 reserved_input: ReservedInputMetadata,
112 callback_checkpoints: BTreeMap<DetTid, Vec<callback_history::Checkpoint>>,
115 rng_seed: Option<u64>,
123 expired_wait_runs_charged_once: bool,
134 host_inputs: Option<HostInputSet>,
141 store_exclusive_failures: HostObservedCount,
148 format_version: Option<u32>,
150 time_skips: Option<Vec<TimeSkip>>,
160}
161
162#[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
187pub const PREEMPTION_RECORD_FORMAT: u32 = SWITCH_IN_CHARGE_FORMAT;
201
202pub const TIME_SKIPS_FORMAT: u32 = 2;
204
205pub const SWITCH_IN_CHARGE_FORMAT: u32 = 3;
218
219#[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
233fn lists_no_time_skips(skips: &Option<Vec<TimeSkip>>) -> bool {
235 skips.as_ref().is_none_or(Vec::is_empty)
236}
237
238#[derive(PartialEq, Debug, Eq, Clone, Copy, Hash, Serialize, Deserialize)]
240pub enum TimeSkipReason {
241 FifoOpenAlone,
244 IdleGuestPollers,
247}
248
249#[derive(PartialEq, Debug, Eq, Clone, Copy, Hash, Serialize, Deserialize)]
255pub struct TimeSkip {
256 pub event: u64,
262 pub target: LogicalTime,
264 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 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 pub fn expired_wait_runs_charged_once(&self) -> bool {
303 self.expired_wait_runs_charged_once
304 }
305
306 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 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 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 store_exclusive_failures: HostObservedCount::default(),
414 replay_departed_at: *replay_departed_at,
416 format_version: *format_version,
417 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 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 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 other_metadata.reserved_input = record.reserved_input.clone();
513 }
514 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 pub fn chaos_timeslice(&self) -> Option<u64> {
532 self.chaos_timeslice
533 }
534
535 pub fn with_chaos_timeslice(mut self, timeslice: Option<u64>) -> Self {
537 self.chaos_timeslice = timeslice;
538 self
539 }
540
541 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 pub fn with_current_format(self) -> Self {
555 self.with_format(Some(PREEMPTION_RECORD_FORMAT))
556 }
557
558 pub fn with_format(mut self, format: Option<u32>) -> Self {
562 self.format_version = format;
563 self
564 }
565
566 pub fn format_version(&self) -> Option<u32> {
568 self.format_version
569 }
570
571 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 pub fn lists_its_time_skips(&self) -> bool {
582 self.format_version
583 .is_some_and(|format| format >= TIME_SKIPS_FORMAT)
584 }
585
586 pub fn with_pipe_wake_latency(mut self, latency: Option<u64>) -> Self {
590 self.pipe_wake_latency = latency;
591 self
592 }
593
594 pub fn pipe_wake_latency(&self) -> Option<u64> {
597 self.pipe_wake_latency
598 }
599
600 pub fn epoch(&self) -> Option<DateTime<Utc>> {
602 self.epoch
603 }
604
605 pub fn signal_state_copy(&self) -> Option<bool> {
608 self.signal_state_copy
609 }
610
611 pub fn host_inputs(&self) -> Option<&HostInputSet> {
613 self.host_inputs.as_ref()
614 }
615
616 pub fn store_exclusive_failures(&self) -> u64 {
619 self.store_exclusive_failures.0
620 }
621
622 pub fn replay_departed_at(&self) -> Option<u64> {
625 self.replay_departed_at
626 }
627
628 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 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 pub fn with_signal_state_copy(mut self, on: Option<bool>) -> Self {
659 self.signal_state_copy = on;
660 self
661 }
662
663 pub fn with_cap_preemption_charge(mut self, on: bool) -> Self {
666 self.cap_preemption_charge = on;
667 self
668 }
669
670 pub fn cap_preemption_charge(&self) -> bool {
672 self.cap_preemption_charge
673 }
674
675 pub fn guest_ids(&self) -> Option<detcore_model::pid::GuestIds> {
678 self.guest_ids
679 }
680
681 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 pub fn extract_all(&self) -> BTreeMap<DetTid, ThreadHistory> {
690 self.per_thread.clone()
691 }
692
693 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 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 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 pub fn schedevents_iter_mut(&mut self) -> std::slice::IterMut<'_, SchedEvent> {
736 self.callback_checkpoints.clear();
737 self.global.iter_mut()
738 }
739
740 pub fn preemptions_only(&mut self) {
742 self.callback_checkpoints.clear();
743 self.global.clear();
744 }
745
746 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 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 store_exclusive_failures: HostObservedCount::default(),
766 format_version: None,
767 time_skips: None,
768 }
769 }
770
771 pub fn schedevents(&self) -> &Vec<SchedEvent> {
773 &self.global
774 }
775
776 pub fn contains_schedevents(&self) -> bool {
778 !self.global.is_empty()
779 }
780
781 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 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 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 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 pub(crate) fn strip_event_times(&mut self) {
856 for event in &mut self.global {
857 event.end_time = None;
858 }
859 }
860
861 pub fn into_global(self) -> Vec<SchedEvent> {
863 self.global
864 }
865
866 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 pub fn validate(&self) -> Result<(), String> {
888 self.validate_contents()?;
889 self.validate_format()
890 }
891
892 pub fn validate_contents(&self) -> Result<(), String> {
896 self.validate_callback_checkpoints()?;
897 self.validate_histories()
898 }
899
900 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 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 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 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 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() .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 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 clone.per_thread.pop_last();
1156 }
1157 }
1158 clone
1159 }
1160}
1161
1162#[derive(
1164 PartialEq, Debug,
1166 Eq,
1167 Clone,
1168 Hash,
1169 Serialize,
1170 Deserialize,
1171)]
1172pub struct ThreadHistory {
1173 pub final_prio: Priority,
1179
1180 prio_changes: Vec<(LogicalTime, Priority)>,
1184
1185 #[serde(default)]
1191 preemption_rcbs: Vec<u64>,
1192
1193 #[serde(default)]
1200 chaos_epochs: Vec<ChaosEpochTransition>,
1201}
1202
1203impl ThreadHistory {
1204 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 #[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 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 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#[derive(PartialEq, Eq, Clone, Serialize, Deserialize)]
1279pub struct ThreadHistoryIterator {
1280 full_history: ThreadHistory,
1282 ix: usize,
1284 #[serde(default)]
1287 chaos_epoch_ix: usize,
1288}
1289
1290impl ThreadHistoryIterator {
1291 pub fn initial_priority(&self) -> Priority {
1293 self.full_history.initial_priority()
1294 }
1295
1296 pub fn final_priority(&self) -> Priority {
1298 self.full_history.final_prio
1299 }
1300
1301 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 pub fn has_chaos_epochs(&self) -> bool {
1321 !self.full_history.chaos_epochs.is_empty()
1322 }
1323
1324 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 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 #[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 #[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 #[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 #[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 #[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 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 #[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 #[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 #[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 #[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(¤t, &encoded).unwrap();
1805 assert_eq!(read_recorded_epoch(¤t), 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 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 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 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 #[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 #[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 #[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 #[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 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 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 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 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 assert_eq!(kept(5), record.callback_checkpoints);
2221 assert_eq!(
2222 record.schedule_prefix(9).callback_checkpoints,
2223 record.callback_checkpoints
2224 );
2225
2226 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]); assert_eq!(original, pr.global);
2362
2363 pr.split_map(|e, _| match &e {
2364 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![]); assert_eq!(pr.global, Vec::new());
2381 }
2382}
2383
2384#[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 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 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 pub fn with_format(mut self, format: Option<u32>) -> Self {
2448 self.inner.format_version = format;
2449 self
2450 }
2451
2452 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 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 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 pub fn records_slice_boundaries(&self) -> bool {
2488 self.boundary_records.is_some()
2489 }
2490
2491 pub(crate) fn next_boundary_index(&self) -> Option<usize> {
2494 self.boundary_records.as_ref().map(Vec::len)
2495 }
2496
2497 pub fn with_epoch(mut self, epoch: DateTime<Utc>) -> Self {
2500 self.inner.epoch = Some(epoch);
2501 self
2502 }
2503
2504 pub fn with_pipe_wake_latency(mut self, latency: Option<u64>) -> Self {
2507 self.inner.pipe_wake_latency = latency;
2508 self
2509 }
2510
2511 pub fn with_signal_state_copy(mut self, on: bool) -> Self {
2514 self.inner.signal_state_copy = Some(on);
2515 self
2516 }
2517
2518 pub fn with_rng_seed(mut self, rng_seed: u64) -> Self {
2521 self.inner.rng_seed = Some(rng_seed);
2522 self
2523 }
2524
2525 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 pub fn with_cap_preemption_charge(mut self, on: bool) -> Self {
2537 self.inner.cap_preemption_charge = on;
2538 self
2539 }
2540
2541 pub fn set_host_inputs(&mut self, inputs: HostInputSet) {
2544 self.inner.host_inputs = Some(inputs);
2545 }
2546
2547 pub fn record_store_exclusive_failures(&mut self, failures: u64) {
2550 self.inner.store_exclusive_failures = HostObservedCount(failures);
2551 }
2552
2553 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 pub fn with_guest_ids(mut self, ids: detcore_model::pid::GuestIds) -> Self {
2564 self.set_guest_ids(ids);
2565 self
2566 }
2567
2568 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 pub fn is_empty(&self) -> bool {
2576 self.inner.per_thread.is_empty()
2577 }
2578
2579 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 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 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 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 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 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 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 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 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 } else {
2733 trace!("NOT recording scheduled event with zero count!");
2734 }
2735 }
2736
2737 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 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 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 pub fn into_string(mut self) -> String {
2784 self.flushed = true;
2785 self.serialized_string()
2786 .expect("invalid preemption recording")
2787 }
2788
2789 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 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
2840pub 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#[derive(Debug)]
2853pub struct PreemptionReader {
2854 inner: PreemptionRecord,
2855}
2856
2857pub fn read_trace(path: &Path) -> Vec<SchedEvent> {
2860 let pr = read_preemption_record(path);
2861 pr.global
2862}
2863
2864fn 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
2884pub 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
2898pub 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
2910pub 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
2922pub 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
2946pub 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
2958pub 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
2985pub 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
2997pub 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
3014pub 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
3034pub 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
3048pub 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
3091pub 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
3104pub 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; 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
3122pub 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
3150pub 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
3165pub 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
3181impl PreemptionReader {
3183 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 pub fn new(path: &Path) -> Self {
3193 let pr = read_preemption_record(path);
3194 PreemptionReader { inner: pr }
3195 }
3196
3197 pub fn into_inner(self) -> PreemptionRecord {
3199 self.inner
3200 }
3201
3202 pub fn extract_thread_record(&self, tid: &DetTid) -> Option<ThreadHistory> {
3206 self.inner.per_thread.get(tid).cloned()
3207 }
3208
3209 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 pub fn all_threads(&self) -> Vec<DetTid> {
3216 self.inner.per_thread.keys().copied().collect()
3218 }
3219
3220 pub fn load_all(&self) -> PreemptionRecord {
3222 self.inner.clone()
3223 }
3224
3225 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
3235pub 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}