telltale_machine/
trace.rs1use std::collections::BTreeMap;
7
8use serde::{Deserialize, Serialize};
9
10use crate::engine::ObsEvent;
11use crate::session::SessionId;
12
13fn default_trace_schema_version() -> u32 {
14 1
15}
16
17pub const TRACE_NORMALIZATION_SCHEMA_VERSION: u32 = 1;
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct NormalizedTraceV1 {
23 #[serde(default = "default_trace_schema_version")]
25 pub schema_version: u32,
26 pub events: Vec<ObsEvent>,
28}
29
30#[must_use]
32pub fn obs_session(ev: &ObsEvent) -> Option<SessionId> {
33 match ev {
34 ObsEvent::Sent { session, .. }
35 | ObsEvent::Received { session, .. }
36 | ObsEvent::Opened { session, .. }
37 | ObsEvent::Closed { session, .. }
38 | ObsEvent::SessionTerminal { session, .. }
39 | ObsEvent::Acquired { session, .. }
40 | ObsEvent::Released { session, .. }
41 | ObsEvent::Transferred { session, .. }
42 | ObsEvent::Forked { session, .. }
43 | ObsEvent::Joined { session, .. }
44 | ObsEvent::Aborted { session, .. }
45 | ObsEvent::CancellationRequested { session, .. }
46 | ObsEvent::Cancelled { session, .. }
47 | ObsEvent::Tagged { session, .. }
48 | ObsEvent::Checked { session, .. } => Some(*session),
49 ObsEvent::Offered { edge, .. } | ObsEvent::Chose { edge, .. } => Some(edge.sid),
50 ObsEvent::EpochAdvanced { sid, .. } => Some(*sid),
51 ObsEvent::Halted { .. }
52 | ObsEvent::Invoked { .. }
53 | ObsEvent::TimeoutIssued { .. }
54 | ObsEvent::FailureBranchEntered { .. }
55 | ObsEvent::Faulted { .. }
56 | ObsEvent::OutputConditionChecked { .. } => None,
57 }
58}
59
60#[must_use]
62pub fn with_tick(ev: &ObsEvent, tick: u64) -> ObsEvent {
63 let mut out = ev.clone();
64 set_obs_event_tick(&mut out, tick);
65 out
66}
67
68#[allow(clippy::too_many_lines)]
69fn set_obs_event_tick(out: &mut ObsEvent, tick: u64) {
70 match out {
71 ObsEvent::Sent {
72 tick: event_tick, ..
73 }
74 | ObsEvent::Received {
75 tick: event_tick, ..
76 }
77 | ObsEvent::Offered {
78 tick: event_tick, ..
79 }
80 | ObsEvent::Chose {
81 tick: event_tick, ..
82 }
83 | ObsEvent::Opened {
84 tick: event_tick, ..
85 }
86 | ObsEvent::Closed {
87 tick: event_tick, ..
88 }
89 | ObsEvent::SessionTerminal {
90 tick: event_tick, ..
91 }
92 | ObsEvent::EpochAdvanced {
93 tick: event_tick, ..
94 }
95 | ObsEvent::Halted {
96 tick: event_tick, ..
97 }
98 | ObsEvent::Invoked {
99 tick: event_tick, ..
100 }
101 | ObsEvent::Acquired {
102 tick: event_tick, ..
103 }
104 | ObsEvent::Released {
105 tick: event_tick, ..
106 }
107 | ObsEvent::Transferred {
108 tick: event_tick, ..
109 }
110 | ObsEvent::Forked {
111 tick: event_tick, ..
112 }
113 | ObsEvent::Joined {
114 tick: event_tick, ..
115 }
116 | ObsEvent::Aborted {
117 tick: event_tick, ..
118 }
119 | ObsEvent::CancellationRequested {
120 tick: event_tick, ..
121 }
122 | ObsEvent::Cancelled {
123 tick: event_tick, ..
124 }
125 | ObsEvent::Tagged {
126 tick: event_tick, ..
127 }
128 | ObsEvent::Checked {
129 tick: event_tick, ..
130 }
131 | ObsEvent::FailureBranchEntered {
132 tick: event_tick, ..
133 }
134 | ObsEvent::TimeoutIssued {
135 tick: event_tick, ..
136 }
137 | ObsEvent::Faulted {
138 tick: event_tick, ..
139 }
140 | ObsEvent::OutputConditionChecked {
141 tick: event_tick, ..
142 } => *event_tick = tick,
143 }
144}
145
146#[must_use]
148pub fn normalize_trace(trace: &[ObsEvent]) -> Vec<ObsEvent> {
149 let mut counters: BTreeMap<SessionId, u64> = BTreeMap::new();
150 let mut out = Vec::with_capacity(trace.len());
151 for ev in trace {
152 if let Some(session) = obs_session(ev) {
153 let counter = counters.entry(session).or_insert(0);
154 let local_tick = *counter;
155 *counter += 1;
156 out.push(with_tick(ev, local_tick));
157 } else {
158 out.push(ev.clone());
159 }
160 }
161 out
162}
163
164#[must_use]
166pub fn strict_trace(trace: &[ObsEvent]) -> Vec<ObsEvent> {
167 trace.to_vec()
168}
169
170#[must_use]
172pub fn normalize_trace_v1(trace: &[ObsEvent]) -> NormalizedTraceV1 {
173 NormalizedTraceV1 {
174 schema_version: TRACE_NORMALIZATION_SCHEMA_VERSION,
175 events: normalize_trace(trace),
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::session::Edge;
183
184 #[test]
185 #[allow(clippy::as_conversions)]
186 fn normalize_trace_memory_is_bounded_at_10k_events() {
187 let mut trace = Vec::with_capacity(10_000);
188 for i in 0..10_000usize {
189 let sid = i % 32;
190 let tick = i as u64;
191 if i % 2 == 0 {
192 trace.push(ObsEvent::Sent {
193 tick,
194 edge: Edge::new(sid, "A", "B"),
195 session: sid,
196 from: "A".to_string(),
197 to: "B".to_string(),
198 label: "m".to_string(),
199 });
200 } else {
201 trace.push(ObsEvent::Received {
202 tick,
203 edge: Edge::new(sid, "B", "A"),
204 session: sid,
205 from: "B".to_string(),
206 to: "A".to_string(),
207 label: "m".to_string(),
208 });
209 }
210 }
211
212 let normalized = normalize_trace(&trace);
213 assert_eq!(normalized.len(), trace.len());
214 assert!(
215 normalized.capacity() <= trace.len() + 1,
216 "normalize_trace should allocate O(n) space without capacity blow-up"
217 );
218 }
219}