use crate::types::{RegionId, Severity, TaskId, Time};
use serde::{Deserialize, Serialize};
use std::io;
pub const REPLAY_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TraceMetadata {
pub version: u32,
pub seed: u64,
pub recorded_at: u64,
pub config_hash: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
impl TraceMetadata {
#[must_use]
pub fn new(seed: u64) -> Self {
Self {
version: REPLAY_SCHEMA_VERSION,
seed,
recorded_at: 0,
config_hash: 0,
description: None,
}
}
#[must_use]
pub const fn with_config_hash(mut self, hash: u64) -> Self {
self.config_hash = hash;
self
}
#[must_use]
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
#[must_use]
pub fn is_compatible(&self) -> bool {
self.version == REPLAY_SCHEMA_VERSION
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(transparent)]
pub struct CompactTaskId(pub u64);
impl From<TaskId> for CompactTaskId {
fn from(id: TaskId) -> Self {
let idx = id.arena_index();
let packed = (u64::from(idx.index()) << 32) | u64::from(idx.generation());
Self(packed)
}
}
impl CompactTaskId {
#[must_use]
pub const fn unpack(self) -> (u32, u32) {
let index = (self.0 >> 32) as u32;
let generation = self.0 as u32;
(index, generation)
}
#[cfg(any(test, feature = "test-internals"))]
#[must_use]
pub fn to_task_id(self) -> TaskId {
let (index, generation) = self.unpack();
TaskId::new_for_test(index, generation)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[repr(transparent)]
pub struct CompactRegionId(pub u64);
impl From<RegionId> for CompactRegionId {
fn from(id: RegionId) -> Self {
let idx = id.arena_index();
let packed = (u64::from(idx.index()) << 32) | u64::from(idx.generation());
Self(packed)
}
}
impl CompactRegionId {
#[must_use]
pub const fn unpack(self) -> (u32, u32) {
let index = (self.0 >> 32) as u32;
let generation = self.0 as u32;
(index, generation)
}
#[cfg(any(test, feature = "test-internals"))]
#[must_use]
pub fn to_region_id(self) -> RegionId {
let (index, generation) = self.unpack();
RegionId::new_for_test(index, generation)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type")]
pub enum ReplayEvent {
TaskScheduled {
task: CompactTaskId,
at_tick: u64,
},
TaskYielded {
task: CompactTaskId,
},
TaskCompleted {
task: CompactTaskId,
outcome: u8,
},
TaskSpawned {
task: CompactTaskId,
region: CompactRegionId,
at_tick: u64,
},
TimeAdvanced {
from_nanos: u64,
to_nanos: u64,
},
TimerCreated {
timer_id: u64,
deadline_nanos: u64,
},
TimerFired {
timer_id: u64,
},
TimerCancelled {
timer_id: u64,
},
IoReady {
token: u64,
readiness: u8,
},
IoResult {
token: u64,
bytes: i64,
},
IoError {
token: u64,
kind: u8,
},
RngSeed {
seed: u64,
},
RngValue {
value: u64,
},
ChaosInjection {
kind: u8,
task: Option<CompactTaskId>,
data: u64,
},
RegionCreated {
region: CompactRegionId,
parent: Option<CompactRegionId>,
at_tick: u64,
},
RegionClosed {
region: CompactRegionId,
outcome: u8,
},
RegionCancelled {
region: CompactRegionId,
cancel_kind: u8,
},
WakerWake {
task: CompactTaskId,
},
WakerBatchWake {
count: u32,
},
Checkpoint {
sequence: u64,
time_nanos: u64,
active_tasks: u32,
active_regions: u32,
},
}
impl ReplayEvent {
#[must_use]
pub const fn estimated_size(&self) -> usize {
match self {
Self::TaskYielded { .. }
| Self::TimerFired { .. }
| Self::TimerCancelled { .. }
| Self::RngSeed { .. }
| Self::RngValue { .. }
| Self::WakerWake { .. } => 9, Self::TaskCompleted { .. }
| Self::IoReady { .. }
| Self::IoError { .. }
| Self::RegionClosed { .. }
| Self::RegionCancelled { .. } => 10, Self::TaskScheduled { .. }
| Self::TimeAdvanced { .. }
| Self::TimerCreated { .. }
| Self::IoResult { .. }
| Self::RegionCreated { parent: None, .. } => 17, Self::TaskSpawned { .. }
| Self::RegionCreated {
parent: Some(_), ..
}
| Self::Checkpoint { .. } => 25, Self::ChaosInjection { task: None, .. } => 11, Self::ChaosInjection { task: Some(_), .. } => 19, Self::WakerBatchWake { .. } => 5, }
}
#[must_use]
pub fn task_scheduled(task: impl Into<CompactTaskId>, at_tick: u64) -> Self {
Self::TaskScheduled {
task: task.into(),
at_tick,
}
}
#[must_use]
pub fn task_completed(task: impl Into<CompactTaskId>, severity: Severity) -> Self {
Self::TaskCompleted {
task: task.into(),
outcome: severity.as_u8(),
}
}
#[must_use]
pub fn time_advanced(from: Time, to: Time) -> Self {
Self::TimeAdvanced {
from_nanos: from.as_nanos(),
to_nanos: to.as_nanos(),
}
}
#[must_use]
#[allow(clippy::fn_params_excessive_bools)]
pub fn io_ready(token: u64, readable: bool, writable: bool, error: bool, hangup: bool) -> Self {
let mut readiness = 0u8;
if readable {
readiness |= 1;
}
if writable {
readiness |= 2;
}
if error {
readiness |= 4;
}
if hangup {
readiness |= 8;
}
Self::IoReady { token, readiness }
}
#[must_use]
pub fn io_error(token: u64, kind: io::ErrorKind) -> Self {
Self::IoError {
token,
kind: error_kind_to_u8(kind),
}
}
#[must_use]
pub fn region_created(
region: impl Into<CompactRegionId>,
parent: Option<impl Into<CompactRegionId>>,
at_tick: u64,
) -> Self {
Self::RegionCreated {
region: region.into(),
parent: parent.map(Into::into),
at_tick,
}
}
#[must_use]
pub fn region_closed(region: impl Into<CompactRegionId>, severity: Severity) -> Self {
Self::RegionClosed {
region: region.into(),
outcome: severity.as_u8(),
}
}
#[must_use]
pub fn region_cancelled(region: impl Into<CompactRegionId>, cancel_kind: u8) -> Self {
Self::RegionCancelled {
region: region.into(),
cancel_kind,
}
}
#[must_use]
pub fn checkpoint(
sequence: u64,
time_nanos: u64,
active_tasks: u32,
active_regions: u32,
) -> Self {
Self::Checkpoint {
sequence,
time_nanos,
active_tasks,
active_regions,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReplayTrace {
pub metadata: TraceMetadata,
pub events: Vec<ReplayEvent>,
#[serde(skip)]
pub cursor: usize,
}
impl ReplayTrace {
#[must_use]
pub fn new(metadata: TraceMetadata) -> Self {
Self {
metadata,
events: Vec::new(),
cursor: 0,
}
}
#[must_use]
pub fn with_capacity(metadata: TraceMetadata, capacity: usize) -> Self {
Self {
metadata,
events: Vec::with_capacity(capacity),
cursor: 0,
}
}
pub fn push(&mut self, event: ReplayEvent) {
self.events.push(event);
}
#[must_use]
pub fn len(&self) -> usize {
self.events.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub fn to_bytes(&self) -> Result<Vec<u8>, rmp_serde::encode::Error> {
rmp_serde::to_vec(self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ReplayTraceError> {
let trace: Self = rmp_serde::from_slice(bytes)?;
if !trace.metadata.is_compatible() {
return Err(ReplayTraceError::IncompatibleVersion {
expected: REPLAY_SCHEMA_VERSION,
found: trace.metadata.version,
});
}
Ok(trace)
}
pub fn iter(&self) -> impl Iterator<Item = &ReplayEvent> {
self.events.iter()
}
#[must_use]
pub fn estimated_size(&self) -> usize {
50 + self
.events
.iter()
.map(ReplayEvent::estimated_size)
.sum::<usize>()
}
}
#[derive(Debug, thiserror::Error)]
pub enum ReplayTraceError {
#[error("serialization error: {0}")]
Serde(#[from] rmp_serde::decode::Error),
#[error("incompatible trace version: expected {expected}, found {found}")]
IncompatibleVersion {
expected: u32,
found: u32,
},
}
#[must_use]
fn error_kind_to_u8(kind: io::ErrorKind) -> u8 {
use io::ErrorKind::{
AddrInUse, AddrNotAvailable, AlreadyExists, BrokenPipe, ConnectionAborted,
ConnectionRefused, ConnectionReset, Interrupted, InvalidData, InvalidInput, NotConnected,
NotFound, OutOfMemory, PermissionDenied, TimedOut, UnexpectedEof, WouldBlock, WriteZero,
};
match kind {
NotFound => 1,
PermissionDenied => 2,
ConnectionRefused => 3,
ConnectionReset => 4,
ConnectionAborted => 5,
NotConnected => 6,
AddrInUse => 7,
AddrNotAvailable => 8,
BrokenPipe => 9,
AlreadyExists => 10,
WouldBlock => 11,
InvalidInput => 12,
InvalidData => 13,
TimedOut => 14,
WriteZero => 15,
Interrupted => 16,
UnexpectedEof => 17,
OutOfMemory => 18,
_ => 255, }
}
#[must_use]
pub fn u8_to_error_kind(value: u8) -> io::ErrorKind {
use io::ErrorKind::{
AddrInUse, AddrNotAvailable, AlreadyExists, BrokenPipe, ConnectionAborted,
ConnectionRefused, ConnectionReset, Interrupted, InvalidData, InvalidInput, NotConnected,
NotFound, Other, OutOfMemory, PermissionDenied, TimedOut, UnexpectedEof, WouldBlock,
WriteZero,
};
match value {
1 => NotFound,
2 => PermissionDenied,
3 => ConnectionRefused,
4 => ConnectionReset,
5 => ConnectionAborted,
6 => NotConnected,
7 => AddrInUse,
8 => AddrNotAvailable,
9 => BrokenPipe,
10 => AlreadyExists,
11 => WouldBlock,
12 => InvalidInput,
13 => InvalidData,
14 => TimedOut,
15 => WriteZero,
16 => Interrupted,
17 => UnexpectedEof,
18 => OutOfMemory,
_ => Other,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProjectionError {
Empty,
MissingSpawn {
task: CompactTaskId,
seq: u64,
kind: crate::trace::event::TraceEventKind,
},
DuplicateSpawn {
task: CompactTaskId,
seq: u64,
},
}
impl core::fmt::Display for ProjectionError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Empty => write!(f, "production trace carries no schedule events"),
Self::MissingSpawn { task, seq, kind } => write!(
f,
"event {kind:?} at seq {seq} names task {task:?} whose spawn is outside the trace window"
),
Self::DuplicateSpawn { task, seq } => {
write!(f, "task {task:?} spawned twice (second at seq {seq})")
}
}
}
}
impl std::error::Error for ProjectionError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct ProjectionOptions {
pub allow_orphans: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ProjectionSummary {
pub spawned: usize,
pub steps: usize,
pub yielded: usize,
pub completed: usize,
pub time_advances: usize,
pub timers: usize,
pub orphans: Vec<CompactTaskId>,
pub skipped: usize,
pub first_seq: u64,
pub last_seq: u64,
pub steps_from_schedule_events: bool,
}
#[derive(Debug, Clone)]
pub struct ProductionSchedule {
trace: ReplayTrace,
spawn_order: Vec<CompactTaskId>,
summary: ProjectionSummary,
}
impl ProductionSchedule {
pub fn from_runtime_trace(
events: &[crate::trace::event::TraceEvent],
) -> Result<Self, ProjectionError> {
Self::from_runtime_trace_with(events, ProjectionOptions::default())
}
pub fn from_runtime_trace_with(
events: &[crate::trace::event::TraceEvent],
options: ProjectionOptions,
) -> Result<Self, ProjectionError> {
use crate::trace::event::{TraceData, TraceEventKind};
let (Some(first), Some(last)) = (events.first(), events.last()) else {
return Err(ProjectionError::Empty);
};
let use_polls = events
.iter()
.any(|event| matches!(event.kind, TraceEventKind::Poll));
let mut trace = ReplayTrace::new(
TraceMetadata::new(0).with_description("production schedule projection"),
);
let mut spawn_order: Vec<CompactTaskId> = Vec::new();
let mut ordinal: std::collections::BTreeMap<u64, usize> = std::collections::BTreeMap::new();
let mut summary = ProjectionSummary {
first_seq: first.seq,
last_seq: last.seq,
steps_from_schedule_events: !use_polls,
..ProjectionSummary::default()
};
fn admit(
ordinal: &mut std::collections::BTreeMap<u64, usize>,
spawn_order: &mut Vec<CompactTaskId>,
summary: &mut ProjectionSummary,
options: ProjectionOptions,
task: CompactTaskId,
seq: u64,
kind: TraceEventKind,
) -> Result<(), ProjectionError> {
if ordinal.contains_key(&task.0) {
return Ok(());
}
if options.allow_orphans {
ordinal.insert(task.0, spawn_order.len());
spawn_order.push(task);
summary.orphans.push(task);
Ok(())
} else {
Err(ProjectionError::MissingSpawn { task, seq, kind })
}
}
for event in events {
match (&event.kind, &event.data) {
(TraceEventKind::Spawn, TraceData::Task { task, region }) => {
let task = CompactTaskId::from(*task);
if ordinal.contains_key(&task.0) {
return Err(ProjectionError::DuplicateSpawn {
task,
seq: event.seq,
});
}
ordinal.insert(task.0, spawn_order.len());
spawn_order.push(task);
trace.push(ReplayEvent::TaskSpawned {
task,
region: CompactRegionId::from(*region),
at_tick: event.seq,
});
summary.spawned += 1;
}
(
kind @ (TraceEventKind::Poll | TraceEventKind::Schedule),
TraceData::Task { task, .. },
) => {
let is_step = if use_polls {
matches!(kind, TraceEventKind::Poll)
} else {
matches!(kind, TraceEventKind::Schedule)
};
if !is_step {
summary.skipped += 1;
continue;
}
let task = CompactTaskId::from(*task);
admit(
&mut ordinal,
&mut spawn_order,
&mut summary,
options,
task,
event.seq,
*kind,
)?;
trace.push(ReplayEvent::TaskScheduled {
task,
at_tick: event.seq,
});
summary.steps += 1;
}
(TraceEventKind::Yield, TraceData::Task { task, .. }) => {
let task = CompactTaskId::from(*task);
admit(
&mut ordinal,
&mut spawn_order,
&mut summary,
options,
task,
event.seq,
event.kind,
)?;
trace.push(ReplayEvent::TaskYielded { task });
summary.yielded += 1;
}
(TraceEventKind::Complete, TraceData::Task { task, .. }) => {
let task = CompactTaskId::from(*task);
admit(
&mut ordinal,
&mut spawn_order,
&mut summary,
options,
task,
event.seq,
event.kind,
)?;
trace.push(ReplayEvent::TaskCompleted { task, outcome: 0 });
summary.completed += 1;
}
(TraceEventKind::TimeAdvance, TraceData::Time { old, new }) => {
trace.push(ReplayEvent::TimeAdvanced {
from_nanos: old.as_nanos(),
to_nanos: new.as_nanos(),
});
summary.time_advances += 1;
}
(TraceEventKind::TimerScheduled, TraceData::Timer { timer_id, deadline }) => {
trace.push(ReplayEvent::TimerCreated {
timer_id: *timer_id,
deadline_nanos: deadline.map_or(0, |deadline| deadline.as_nanos()),
});
summary.timers += 1;
}
(TraceEventKind::TimerFired, TraceData::Timer { timer_id, .. }) => {
trace.push(ReplayEvent::TimerFired {
timer_id: *timer_id,
});
summary.timers += 1;
}
(TraceEventKind::TimerCancelled, TraceData::Timer { timer_id, .. }) => {
trace.push(ReplayEvent::TimerCancelled {
timer_id: *timer_id,
});
summary.timers += 1;
}
_ => summary.skipped += 1,
}
}
if summary.spawned == 0 && summary.steps == 0 {
return Err(ProjectionError::Empty);
}
Ok(Self {
trace,
spawn_order,
summary,
})
}
#[must_use]
pub fn trace(&self) -> &ReplayTrace {
&self.trace
}
#[must_use]
pub fn into_trace(self) -> ReplayTrace {
self.trace
}
#[must_use]
pub fn spawn_order(&self) -> &[CompactTaskId] {
&self.spawn_order
}
#[must_use]
pub fn spawn_ordinal(&self, task: CompactTaskId) -> Option<usize> {
self.spawn_order
.iter()
.position(|candidate| *candidate == task)
}
pub fn steps(&self) -> impl Iterator<Item = (usize, u64)> + '_ {
self.trace.events.iter().filter_map(|event| {
if let ReplayEvent::TaskScheduled { task, at_tick } = event {
Some((self.spawn_ordinal(*task)?, *at_tick))
} else {
None
}
})
}
#[must_use]
pub fn summary(&self) -> &ProjectionSummary {
&self.summary
}
}
#[cfg(test)]
mod tests {
#![allow(
clippy::pedantic,
clippy::nursery,
clippy::expect_fun_call,
clippy::map_unwrap_or,
clippy::cast_possible_wrap,
clippy::future_not_send
)]
use super::*;
#[test]
fn metadata_creation() {
let meta = TraceMetadata::new(42);
assert_eq!(meta.version, REPLAY_SCHEMA_VERSION);
assert_eq!(meta.seed, 42);
assert_eq!(meta.recorded_at, 0);
assert!(meta.is_compatible());
}
#[test]
fn metadata_creation_is_deterministic_for_same_seed() {
let first = TraceMetadata::new(42);
let second = TraceMetadata::new(42);
assert_eq!(first, second);
assert_eq!(first.recorded_at, 0);
}
#[test]
fn metadata_builder() {
let meta = TraceMetadata::new(42)
.with_config_hash(0xDEAD_BEEF)
.with_description("test trace");
assert_eq!(meta.config_hash, 0xDEAD_BEEF);
assert_eq!(meta.description, Some("test trace".to_string()));
}
#[test]
fn compact_task_id_roundtrip() {
let task = TaskId::new_for_test(123, 456);
let compact = CompactTaskId::from(task);
let (index, generation) = compact.unpack();
assert_eq!(index, 123);
assert_eq!(generation, 456);
assert_eq!(compact.to_task_id(), task);
}
#[test]
fn replay_event_sizes() {
let events = [
ReplayEvent::TaskScheduled {
task: CompactTaskId(0),
at_tick: 0,
},
ReplayEvent::TaskYielded {
task: CompactTaskId(0),
},
ReplayEvent::TaskCompleted {
task: CompactTaskId(0),
outcome: 0,
},
ReplayEvent::TimeAdvanced {
from_nanos: 0,
to_nanos: 0,
},
ReplayEvent::TimerFired { timer_id: 0 },
ReplayEvent::IoReady {
token: 0,
readiness: 0,
},
ReplayEvent::RngSeed { seed: 0 },
ReplayEvent::WakerWake {
task: CompactTaskId(0),
},
];
for event in &events {
let size = event.estimated_size();
assert!(size < 64, "Event {event:?} exceeds 64 bytes: {size} bytes");
}
}
#[test]
fn trace_serialization_roundtrip() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
trace.push(ReplayEvent::RngSeed { seed: 42 });
trace.push(ReplayEvent::TaskScheduled {
task: CompactTaskId(1),
at_tick: 0,
});
trace.push(ReplayEvent::TimeAdvanced {
from_nanos: 0,
to_nanos: 1_000_000,
});
trace.push(ReplayEvent::TaskCompleted {
task: CompactTaskId(1),
outcome: 0,
});
let bytes = trace.to_bytes().expect("serialize");
let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
assert_eq!(loaded.metadata.seed, 42);
assert_eq!(loaded.events.len(), 4);
assert_eq!(loaded.events[0], ReplayEvent::RngSeed { seed: 42 });
}
#[test]
fn trace_actual_serialized_size() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
for i in 0..100 {
trace.push(ReplayEvent::TaskScheduled {
task: CompactTaskId(i),
at_tick: i,
});
}
let bytes = trace.to_bytes().expect("serialize");
let avg_size = bytes.len() / 100;
assert!(
avg_size < 32,
"Average serialized event size {avg_size} bytes exceeds expected"
);
}
#[test]
fn error_kind_roundtrip() {
use io::ErrorKind::*;
let kinds = [
NotFound,
PermissionDenied,
ConnectionRefused,
ConnectionReset,
BrokenPipe,
WouldBlock,
TimedOut,
];
for kind in kinds {
let encoded = error_kind_to_u8(kind);
let decoded = u8_to_error_kind(encoded);
assert_eq!(kind, decoded, "Failed roundtrip for {kind:?}");
}
}
#[test]
fn version_compatibility_check() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
trace.push(ReplayEvent::RngSeed { seed: 42 });
let bytes = trace.to_bytes().expect("serialize");
let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
assert!(loaded.metadata.is_compatible());
}
#[test]
fn io_ready_flags() {
let event = ReplayEvent::io_ready(123, true, false, false, false);
if let ReplayEvent::IoReady { token, readiness } = event {
assert_eq!(token, 123);
assert_eq!(readiness & 1, 1); assert_eq!(readiness & 2, 0); } else {
panic!("Expected IoReady");
}
let event = ReplayEvent::io_ready(456, true, true, true, true);
if let ReplayEvent::IoReady { readiness, .. } = event {
assert_eq!(readiness, 0b1111); } else {
panic!("Expected IoReady");
}
}
#[test]
fn chaos_injection_variants() {
let event_no_task = ReplayEvent::ChaosInjection {
kind: 1, task: None,
data: 1_000_000, };
assert!(event_no_task.estimated_size() < 64);
let event_with_task = ReplayEvent::ChaosInjection {
kind: 0, task: Some(CompactTaskId(42)),
data: 0,
};
assert!(event_with_task.estimated_size() < 64);
}
#[test]
fn region_created_event() {
let event = ReplayEvent::region_created(CompactRegionId(1), Some(CompactRegionId(0)), 100);
if let ReplayEvent::RegionCreated {
region,
parent,
at_tick,
} = event
{
assert_eq!(region.0, 1);
assert_eq!(parent.map(|p| p.0), Some(0));
assert_eq!(at_tick, 100);
} else {
panic!("Expected RegionCreated");
}
let root = ReplayEvent::region_created(CompactRegionId(0), None::<CompactRegionId>, 0);
if let ReplayEvent::RegionCreated { parent, .. } = root {
assert!(parent.is_none());
} else {
panic!("Expected RegionCreated");
}
}
#[test]
fn region_closed_event() {
let event = ReplayEvent::region_closed(CompactRegionId(5), Severity::Ok);
if let ReplayEvent::RegionClosed { region, outcome } = event {
assert_eq!(region.0, 5);
assert_eq!(outcome, Severity::Ok.as_u8());
} else {
panic!("Expected RegionClosed");
}
}
#[test]
fn region_cancelled_event() {
let event = ReplayEvent::region_cancelled(CompactRegionId(3), 1);
if let ReplayEvent::RegionCancelled {
region,
cancel_kind,
} = event
{
assert_eq!(region.0, 3);
assert_eq!(cancel_kind, 1);
} else {
panic!("Expected RegionCancelled");
}
}
#[test]
fn checkpoint_event() {
let event = ReplayEvent::checkpoint(42, 1_000_000_000, 5, 2);
if let ReplayEvent::Checkpoint {
sequence,
time_nanos,
active_tasks,
active_regions,
} = event
{
assert_eq!(sequence, 42);
assert_eq!(time_nanos, 1_000_000_000);
assert_eq!(active_tasks, 5);
assert_eq!(active_regions, 2);
} else {
panic!("Expected Checkpoint");
}
}
#[test]
fn region_events_size() {
let events = [
ReplayEvent::RegionCreated {
region: CompactRegionId(0),
parent: None,
at_tick: 0,
},
ReplayEvent::RegionCreated {
region: CompactRegionId(0),
parent: Some(CompactRegionId(1)),
at_tick: 0,
},
ReplayEvent::RegionClosed {
region: CompactRegionId(0),
outcome: 0,
},
ReplayEvent::RegionCancelled {
region: CompactRegionId(0),
cancel_kind: 0,
},
ReplayEvent::Checkpoint {
sequence: 0,
time_nanos: 0,
active_tasks: 0,
active_regions: 0,
},
];
for event in &events {
let size = event.estimated_size();
assert!(size < 64, "Event {event:?} exceeds 64 bytes: {size} bytes");
}
}
#[test]
fn empty_trace_serialization_roundtrip() {
let trace = ReplayTrace::new(TraceMetadata::new(0));
assert!(trace.is_empty());
assert_eq!(trace.len(), 0);
let bytes = trace.to_bytes().expect("serialize empty");
let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize empty");
assert_eq!(loaded.metadata.seed, 0);
assert!(loaded.is_empty());
}
#[test]
fn incompatible_version_rejected() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
trace.push(ReplayEvent::RngSeed { seed: 42 });
let _bytes = trace.to_bytes().expect("serialize");
let meta = TraceMetadata {
version: 999,
seed: 42,
recorded_at: 0,
config_hash: 0,
description: None,
};
let bad_trace = ReplayTrace {
metadata: meta,
events: vec![ReplayEvent::RngSeed { seed: 42 }],
cursor: 0,
};
let bad_bytes = bad_trace.to_bytes().expect("serialize bad version");
let err = ReplayTrace::from_bytes(&bad_bytes).unwrap_err();
assert!(matches!(
err,
ReplayTraceError::IncompatibleVersion {
expected: REPLAY_SCHEMA_VERSION,
found: 999
}
));
}
#[test]
fn trace_with_capacity_preallocates() {
let trace = ReplayTrace::with_capacity(TraceMetadata::new(1), 100);
assert!(trace.is_empty());
assert_eq!(trace.len(), 0);
}
#[test]
fn estimated_size_increases_with_events() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
let base_size = trace.estimated_size();
trace.push(ReplayEvent::RngSeed { seed: 42 });
let one_event_size = trace.estimated_size();
assert!(one_event_size > base_size);
trace.push(ReplayEvent::TaskScheduled {
task: CompactTaskId(1),
at_tick: 0,
});
let two_event_size = trace.estimated_size();
assert!(two_event_size > one_event_size);
}
#[test]
fn compact_region_id_roundtrip() {
let region = RegionId::new_for_test(456, 789);
let compact = CompactRegionId::from(region);
let (index, generation) = compact.unpack();
assert_eq!(index, 456);
assert_eq!(generation, 789);
assert_eq!(compact.to_region_id(), region);
}
#[test]
fn metadata_compatibility_flag() {
let meta = TraceMetadata::new(42);
assert!(meta.is_compatible());
let old_meta = TraceMetadata {
version: 0,
seed: 42,
recorded_at: 0,
config_hash: 0,
description: None,
};
assert!(!old_meta.is_compatible());
}
#[test]
fn io_error_roundtrip_all_known_kinds() {
use io::ErrorKind::*;
let all_known = [
NotFound,
PermissionDenied,
ConnectionRefused,
ConnectionReset,
ConnectionAborted,
NotConnected,
AddrInUse,
AddrNotAvailable,
BrokenPipe,
AlreadyExists,
WouldBlock,
InvalidInput,
InvalidData,
TimedOut,
WriteZero,
Interrupted,
UnexpectedEof,
OutOfMemory,
];
for kind in all_known {
let encoded = error_kind_to_u8(kind);
let decoded = u8_to_error_kind(encoded);
assert_eq!(kind, decoded, "Roundtrip failed for {kind:?}");
}
}
#[test]
fn unknown_error_kind_maps_to_other() {
let decoded = u8_to_error_kind(255);
assert_eq!(decoded, io::ErrorKind::Other);
let decoded = u8_to_error_kind(200);
assert_eq!(decoded, io::ErrorKind::Other);
}
#[test]
fn trace_iter_yields_all_events() {
let mut trace = ReplayTrace::new(TraceMetadata::new(42));
trace.push(ReplayEvent::RngSeed { seed: 1 });
trace.push(ReplayEvent::RngSeed { seed: 2 });
trace.push(ReplayEvent::RngSeed { seed: 3 });
assert_eq!(trace.iter().count(), 3);
}
#[test]
fn region_events_serialization_roundtrip() {
let mut trace = ReplayTrace::new(TraceMetadata::new(123));
trace.push(ReplayEvent::RegionCreated {
region: CompactRegionId(0),
parent: None,
at_tick: 0,
});
trace.push(ReplayEvent::RegionCreated {
region: CompactRegionId(1),
parent: Some(CompactRegionId(0)),
at_tick: 10,
});
trace.push(ReplayEvent::RegionCancelled {
region: CompactRegionId(1),
cancel_kind: 2,
});
trace.push(ReplayEvent::RegionClosed {
region: CompactRegionId(1),
outcome: 2, });
trace.push(ReplayEvent::RegionClosed {
region: CompactRegionId(0),
outcome: 0, });
trace.push(ReplayEvent::Checkpoint {
sequence: 1,
time_nanos: 1_000_000,
active_tasks: 0,
active_regions: 0,
});
let bytes = trace.to_bytes().expect("serialize");
let loaded = ReplayTrace::from_bytes(&bytes).expect("deserialize");
assert_eq!(loaded.events.len(), 6);
match &loaded.events[0] {
ReplayEvent::RegionCreated {
region,
parent,
at_tick,
} => {
assert_eq!(region.0, 0);
assert!(parent.is_none());
assert_eq!(*at_tick, 0);
}
_ => panic!("Expected RegionCreated"),
}
match &loaded.events[5] {
ReplayEvent::Checkpoint {
sequence,
time_nanos,
active_tasks,
active_regions,
} => {
assert_eq!(*sequence, 1);
assert_eq!(*time_nanos, 1_000_000);
assert_eq!(*active_tasks, 0);
assert_eq!(*active_regions, 0);
}
_ => panic!("Expected Checkpoint"),
}
}
#[test]
fn trace_metadata_debug_clone_eq() {
let m = TraceMetadata {
version: REPLAY_SCHEMA_VERSION,
seed: 42,
recorded_at: 0,
config_hash: 0xABC,
description: Some("test".into()),
};
let m2 = m.clone();
assert_eq!(m, m2);
let dbg = format!("{m:?}");
assert!(dbg.contains("TraceMetadata"));
}
#[test]
fn compact_task_id_debug_clone_copy_eq() {
let id = CompactTaskId(42);
let id2 = id; let id3 = id;
assert_eq!(id, id2);
assert_eq!(id, id3);
assert_ne!(id, CompactTaskId(99));
let dbg = format!("{id:?}");
assert!(dbg.contains("42"));
}
#[test]
fn compact_region_id_debug_clone_copy_eq() {
let id = CompactRegionId(7);
let id2 = id; let id3 = id;
assert_eq!(id, id2);
assert_eq!(id, id3);
assert_ne!(id, CompactRegionId(99));
let dbg = format!("{id:?}");
assert!(dbg.contains('7'));
}
#[test]
fn replay_event_debug_clone_eq() {
let e = ReplayEvent::TaskScheduled {
task: CompactTaskId(1),
at_tick: 100,
};
let e2 = e.clone();
assert_eq!(e, e2);
assert_ne!(
e,
ReplayEvent::TaskYielded {
task: CompactTaskId(1),
}
);
let dbg = format!("{e:?}");
assert!(dbg.contains("TaskScheduled"));
}
}
#[cfg(test)]
mod production_schedule_tests {
use super::{
CompactTaskId, ProductionSchedule, ProjectionError, ProjectionOptions, ReplayEvent,
};
use crate::trace::event::{TraceEvent, TraceEventKind};
use crate::types::{RegionId, TaskId, Time};
fn task(n: u32) -> TaskId {
TaskId::new_for_test(n, 1)
}
fn region() -> RegionId {
RegionId::new_for_test(1, 1)
}
fn at(seq: u64) -> Time {
Time::from_nanos(seq * 10)
}
#[test]
fn interleaved_polls_project_in_order() {
let events = vec![
TraceEvent::spawn(1, at(1), task(1), region()),
TraceEvent::spawn(2, at(2), task(2), region()),
TraceEvent::schedule(3, at(3), task(1), region()),
TraceEvent::poll(4, at(4), task(1), region()),
TraceEvent::poll(5, at(5), task(2), region()),
TraceEvent::poll(6, at(6), task(1), region()),
TraceEvent::complete(7, at(7), task(1), region()),
TraceEvent::complete(8, at(8), task(2), region()),
];
let schedule = ProductionSchedule::from_runtime_trace(&events).expect("projects");
assert_eq!(
schedule.steps().collect::<Vec<_>>(),
vec![(0, 4), (1, 5), (0, 6)],
"steps are the Poll events in order, keyed by spawn ordinal"
);
let summary = schedule.summary();
assert_eq!(summary.spawned, 2);
assert_eq!(summary.steps, 3);
assert_eq!(summary.completed, 2);
assert_eq!(
summary.skipped, 1,
"the Schedule event is dropped when Polls exist"
);
assert!(!summary.steps_from_schedule_events);
assert_eq!((summary.first_seq, summary.last_seq), (1, 8));
assert_eq!(
schedule.spawn_order(),
&[CompactTaskId::from(task(1)), CompactTaskId::from(task(2))]
);
assert!(matches!(
schedule.trace().events.first(),
Some(ReplayEvent::TaskSpawned { at_tick: 1, .. })
));
}
#[test]
fn poll_before_spawn_is_refused_strictly_and_tolerated_as_orphan() {
let events = vec![
TraceEvent::spawn(1, at(1), task(1), region()),
TraceEvent::poll(2, at(2), task(2), region()),
];
assert_eq!(
ProductionSchedule::from_runtime_trace(&events).err(),
Some(ProjectionError::MissingSpawn {
task: CompactTaskId::from(task(2)),
seq: 2,
kind: TraceEventKind::Poll,
})
);
let tolerant = ProductionSchedule::from_runtime_trace_with(
&events,
ProjectionOptions {
allow_orphans: true,
},
)
.expect("orphans admitted");
assert_eq!(
tolerant.summary().orphans,
vec![CompactTaskId::from(task(2))]
);
assert_eq!(
tolerant.spawn_ordinal(CompactTaskId::from(task(2))),
Some(1)
);
assert_eq!(tolerant.steps().collect::<Vec<_>>(), vec![(1, 2)]);
}
#[test]
fn schedule_events_are_steps_only_when_no_polls_exist() {
let events = vec![
TraceEvent::spawn(1, at(1), task(1), region()),
TraceEvent::schedule(2, at(2), task(1), region()),
TraceEvent::schedule(3, at(3), task(1), region()),
];
let schedule = ProductionSchedule::from_runtime_trace(&events).expect("projects");
assert_eq!(schedule.steps().collect::<Vec<_>>(), vec![(0, 2), (0, 3)]);
assert!(schedule.summary().steps_from_schedule_events);
assert_eq!(schedule.summary().skipped, 0);
}
#[test]
fn empty_and_duplicate_spawn_are_errors() {
assert_eq!(
ProductionSchedule::from_runtime_trace(&[]).err(),
Some(ProjectionError::Empty)
);
let events = vec![
TraceEvent::spawn(1, at(1), task(1), region()),
TraceEvent::spawn(2, at(2), task(1), region()),
];
assert_eq!(
ProductionSchedule::from_runtime_trace(&events).err(),
Some(ProjectionError::DuplicateSpawn {
task: CompactTaskId::from(task(1)),
seq: 2,
})
);
let only_region = vec![TraceEvent::time_advance(
1,
Time::ZERO,
Time::from_nanos(0),
Time::from_nanos(100),
)];
assert_eq!(
ProductionSchedule::from_runtime_trace(&only_region).err(),
Some(ProjectionError::Empty)
);
}
#[test]
fn time_and_timer_hints_are_carried_and_others_counted() {
let events = vec![
TraceEvent::spawn(1, at(1), task(1), region()),
TraceEvent::time_advance(2, at(2), Time::from_nanos(0), Time::from_nanos(100)),
TraceEvent::timer_scheduled(3, at(3), 7, Time::from_nanos(500)),
TraceEvent::timer_fired(4, at(4), 7),
TraceEvent::wake(5, at(5), task(1), region()),
TraceEvent::poll(6, at(6), task(1), region()),
TraceEvent::yield_task(7, at(7), task(1), region()),
];
let schedule = ProductionSchedule::from_runtime_trace(&events).expect("projects");
let summary = schedule.summary();
assert_eq!(summary.time_advances, 1);
assert_eq!(summary.timers, 2);
assert_eq!(summary.yielded, 1);
assert_eq!(
summary.skipped, 1,
"Wake is not a decision the lab re-drives"
);
assert!(schedule.trace().events.iter().any(|event| matches!(
event,
ReplayEvent::TimeAdvanced {
from_nanos: 0,
to_nanos: 100
}
)));
assert!(schedule.trace().events.iter().any(|event| matches!(
event,
ReplayEvent::TimerCreated {
timer_id: 7,
deadline_nanos: 500
}
)));
assert_eq!(schedule.into_trace().len(), 6);
}
}