1#![expect(clippy::borrowed_box)]
2use std::{
19 borrow::Borrow,
20 collections::VecDeque,
21 fmt::{Debug, Display, Formatter},
22 sync::Arc,
23 time::Duration,
24};
25
26use cbor4ii::serde::from_slice;
27use parking_lot::Mutex;
28
29use crate::{
30 Effect, ExternalEffect, Instant, Name, ScheduleId, SendData,
31 effect::{StageEffect, StageResponse},
32 serde::to_cbor,
33};
34
35#[derive(Default)]
43pub struct TraceBuffer {
44 messages: VecDeque<Vec<u8>>,
45 min_entries: usize,
46 max_size: usize,
47 used_size: usize,
48 dropped_messages: usize,
49 fetch_replay: Option<std::vec::IntoIter<TraceEntry>>,
50}
51
52#[derive(PartialEq, serde::Serialize, serde::Deserialize)]
58pub enum TraceEntry {
59 Suspend(Effect),
60 Resume {
61 stage: Name,
62 response: StageResponse,
63 },
64 Clock(Instant),
65 Input {
66 stage: Name,
67 #[serde(with = "crate::serde::serialize_send_data")]
68 input: Box<dyn SendData>,
69 },
70 State {
71 stage: Name,
72 #[serde(with = "crate::serde::serialize_send_data")]
73 state: Box<dyn SendData>,
74 },
75 Terminated {
76 stage: Name,
77 reason: TerminationReason,
78 },
79}
80
81#[derive(Debug, PartialEq, serde::Serialize, serde::Deserialize)]
82pub enum TerminationReason {
83 Voluntary,
84 Supervision(Name),
85 Aborted,
86}
87
88impl TraceEntry {
89 pub fn to_json(&self) -> serde_json::Value {
90 match &self {
91 TraceEntry::Suspend(effect) => serde_json::json!({
92 "type": "suspend",
93 "effect": effect.to_json(),
94 }),
95 TraceEntry::Resume { stage, response } => serde_json::json!({
96 "type": "resume",
97 "stage": stage,
98 "response": response,
99 }),
100 TraceEntry::Clock(instant) => serde_json::json!({
101 "type": "clock",
102 "instant": instant,
103 }),
104 TraceEntry::Input { stage, input } => serde_json::json!({
105 "type": "input",
106 "stage": stage,
107 "input": input.to_string(),
108 }),
109 TraceEntry::State { stage, state } => serde_json::json!({
110 "type": "state",
111 "stage": stage,
112 "state": state.to_string(),
113 }),
114 TraceEntry::Terminated { stage, reason } => serde_json::json!({
115 "type": "terminated",
116 "stage": stage,
117 "reason": reason,
118 }),
119 }
120 }
121}
122
123impl Debug for TraceEntry {
124 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
127 match self {
128 TraceEntry::Suspend(effect) => f.debug_tuple("Suspend").field(&effect).finish(),
129 TraceEntry::Resume { stage, response, .. } => {
130 f.debug_struct("Resume").field("stage", stage).field("response", response).finish()
131 }
132 TraceEntry::Clock(instant) => f.debug_tuple("Clock").field(instant).finish(),
133 TraceEntry::Input { stage, input } => {
134 f.debug_struct("Input").field("stage", stage).field("input", input).finish()
135 }
136 TraceEntry::State { stage, state } => {
137 f.debug_struct("State").field("stage", stage).field("state", state).finish()
138 }
139 TraceEntry::Terminated { stage, reason } => {
140 f.debug_struct("Terminated").field("stage", stage).field("reason", reason).finish()
141 }
142 }
143 }
144}
145
146impl Display for TraceEntry {
147 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148 match self {
149 TraceEntry::Suspend(effect) => write!(f, "suspend {stage}", stage = effect.at_stage()),
150 TraceEntry::Resume { stage, response } => {
151 write!(
152 f,
153 "resume {stage}{response}",
154 stage = stage.as_str(),
155 response = match response {
156 StageResponse::Unit => "".to_string(),
157 other @ StageResponse::ClockResponse(_)
158 | other @ StageResponse::WaitResponse(_)
159 | other @ StageResponse::CallResponse(_)
160 | other @ StageResponse::CancelScheduleResponse(_)
161 | other @ StageResponse::ExternalResponse(_)
162 | other @ StageResponse::AddStageResponse(_)
163 | other @ StageResponse::ContramapResponse(_) => format!(" -> {other}"),
164 },
165 )
166 }
167 TraceEntry::Clock(instant) => write!(f, "clock {instant}"),
168 TraceEntry::Input { stage, input } => {
169 write!(
170 f,
171 "input {stage} -> {input}",
172 stage = stage.as_str(),
173 input = input.as_send_data_value().borrow()
174 )
175 }
176 TraceEntry::State { stage, state } => {
177 write!(
178 f,
179 "state {stage} -> {state}",
180 stage = stage.as_str(),
181 state = state.as_send_data_value().borrow()
182 )
183 }
184 TraceEntry::Terminated { stage, reason } => {
185 write!(f, "terminated {stage} {reason:?}")
186 }
187 }
188 }
189}
190
191impl TraceEntry {
192 pub fn suspend(effect: Effect) -> Self {
194 Self::Suspend(effect)
195 }
196
197 pub fn resume(stage: impl AsRef<str>, response: StageResponse) -> Self {
199 Self::Resume { stage: Name::from(stage.as_ref()), response }
200 }
201
202 pub fn clock(instant: Instant) -> Self {
204 Self::Clock(instant)
205 }
206
207 pub fn input(stage: impl AsRef<str>, input: Box<dyn SendData>) -> Self {
209 Self::Input { stage: Name::from(stage.as_ref()), input }
210 }
211
212 pub fn state(stage: impl AsRef<str>, state: Box<dyn SendData>) -> Self {
214 Self::State { stage: Name::from(stage.as_ref()), state }
215 }
216
217 pub fn terminated(stage: impl AsRef<str>, reason: TerminationReason) -> Self {
219 Self::Terminated { stage: Name::from(stage.as_ref()), reason }
220 }
221
222 pub fn at_stage<'a, 'b: 'a>(&'b self) -> Option<&'a Name> {
223 match self {
224 TraceEntry::Suspend(effect) => Some(effect.at_stage()),
225 TraceEntry::Resume { stage, .. } => Some(stage),
226 TraceEntry::Clock(..) => None,
227 TraceEntry::Input { stage, .. } => Some(stage),
228 TraceEntry::State { stage, .. } => Some(stage),
229 TraceEntry::Terminated { stage, .. } => Some(stage),
230 }
231 }
232}
233
234#[derive(Debug, PartialEq, serde::Serialize)]
238enum TraceEntryRef<'a> {
239 Suspend(&'a Effect),
240 Resume { stage: &'a Name, response: &'a StageResponse },
241 Clock(Instant),
242 Input { stage: &'a Name, input: &'a Box<dyn SendData> },
243 State { stage: &'a Name, state: &'a Box<dyn SendData> },
244 Terminated { stage: &'a Name, reason: TerminationReasonRef<'a> },
245}
246
247#[derive(Debug, PartialEq, serde::Serialize)]
248enum TerminationReasonRef<'a> {
249 Voluntary,
250 Supervision(&'a Name),
251 Aborted,
252}
253
254impl<'a> From<&'a TerminationReason> for TerminationReasonRef<'a> {
255 fn from(reason: &'a TerminationReason) -> Self {
256 match reason {
257 TerminationReason::Voluntary => TerminationReasonRef::Voluntary,
258 TerminationReason::Supervision(child) => TerminationReasonRef::Supervision(child),
259 TerminationReason::Aborted => TerminationReasonRef::Aborted,
260 }
261 }
262}
263
264#[derive(serde::Serialize)]
266enum TraceEntryRefRef<'a> {
267 Suspend(EffectRef<'a>),
268 Resume { stage: &'a Name, response: StageResponseRef<'a> },
269}
270
271#[derive(serde::Serialize)]
273enum EffectRef<'a> {
274 Receive { at_stage: &'a Name },
275 Send { from: &'a Name, to: &'a Name, msg: &'a dyn SendData },
276 Call { from: &'a Name, to: &'a Name, duration: Duration, msg: &'a dyn SendData },
277 Clock { at_stage: &'a Name },
278 Wait { at_stage: &'a Name, duration: Duration },
279 Schedule { at_stage: &'a Name, msg: &'a dyn SendData, id: ScheduleId },
280 CancelSchedule { at_stage: &'a Name, id: ScheduleId },
281 External { at_stage: &'a Name, effect: &'a dyn crate::ExternalEffect },
282 Terminate { at_stage: &'a Name },
283 AddStage { at_stage: &'a Name, name: &'a Name },
284 WireStage { at_stage: &'a Name, name: &'a Name, initial_state: &'a dyn SendData, tombstone: &'a dyn SendData },
285 Contramap { at_stage: &'a Name, original: &'a Name, new_name: &'a Name },
286}
287
288impl<'a> EffectRef<'a> {
289 fn from(at_stage: &'a Name, effect: &'a StageEffect<Box<dyn SendData>>) -> Option<Self> {
290 Some(match effect {
291 StageEffect::Receive => EffectRef::Receive { at_stage },
292 StageEffect::Send(to, _call, msg) => EffectRef::Send { from: at_stage, to, msg: &**msg },
293 StageEffect::Call(..) => return None,
294 StageEffect::Clock => EffectRef::Clock { at_stage },
295 StageEffect::Wait(duration) => EffectRef::Wait { at_stage, duration: *duration },
296 StageEffect::Schedule(msg, id) => EffectRef::Schedule { at_stage, msg: &**msg, id: *id },
297 StageEffect::CancelSchedule(id) => EffectRef::CancelSchedule { at_stage, id: *id },
298 StageEffect::External(effect) => EffectRef::External { at_stage, effect: &**effect },
299 StageEffect::Terminate => EffectRef::Terminate { at_stage },
300 StageEffect::AddStage(name) => EffectRef::AddStage { at_stage, name },
301 StageEffect::WireStage(name, _transition, initial_state, tombstone) => {
302 EffectRef::WireStage { at_stage, name, initial_state: &**initial_state, tombstone: &**tombstone }
303 }
304 StageEffect::Contramap { original, new_name, transform: _ } => {
305 EffectRef::Contramap { at_stage, original, new_name }
306 }
307 })
308 }
309}
310
311#[derive(serde::Serialize)]
313enum StageResponseRef<'a> {
314 ExternalResponse(&'a dyn SendData),
315}
316
317impl TraceBuffer {
318 pub fn new_shared(min_entries: usize, max_size: usize) -> Arc<Mutex<Self>> {
322 Arc::new(Mutex::new(Self::new(min_entries, max_size)))
323 }
324
325 pub fn new(min_entries: usize, max_size: usize) -> Self {
330 Self { messages: VecDeque::new(), min_entries, max_size, used_size: 0, dropped_messages: 0, fetch_replay: None }
331 }
332
333 pub fn push_suspend(&mut self, effect: &Effect) {
335 self.push(TraceEntryRef::Suspend(effect));
336 }
337
338 pub fn push_suspend_external(&mut self, at_stage: &Name, effect: &dyn crate::ExternalEffect) {
339 self.push(TraceEntryRefRef::Suspend(EffectRef::External { at_stage, effect }));
340 }
341
342 pub fn push_suspend_call(&mut self, at_stage: &Name, to: &Name, duration: Duration, msg: &dyn SendData) {
343 self.push(TraceEntryRefRef::Suspend(EffectRef::Call { from: at_stage, to, duration, msg }));
344 }
345
346 pub fn push_suspend_ref(&mut self, at_stage: &Name, effect: &StageEffect<Box<dyn SendData>>) {
347 if let Some(effect) = EffectRef::from(at_stage, effect) {
348 self.push(TraceEntryRefRef::Suspend(effect));
349 }
350 }
351
352 pub fn push_resume(&mut self, stage: &Name, response: &StageResponse) {
354 self.push(TraceEntryRef::Resume { stage, response });
355 }
356
357 pub fn push_resume_external(&mut self, stage: &Name, response: &dyn SendData) {
358 self.push(TraceEntryRefRef::Resume { stage, response: StageResponseRef::ExternalResponse(response) });
359 }
360
361 pub fn push_clock(&mut self, instant: Instant) {
363 self.push(TraceEntryRef::Clock(instant));
364 }
365
366 pub fn push_input(&mut self, stage: &Name, input: &Box<dyn SendData>) {
368 self.push(TraceEntryRef::Input { stage, input });
369 }
370
371 pub fn push_state(&mut self, stage: &Name, state: &Box<dyn SendData>) {
376 self.push(TraceEntryRef::State { stage, state });
377 }
378
379 pub fn push_terminated(&mut self, stage: &Name, reason: TerminationReason) {
380 self.push(TraceEntryRef::Terminated { stage, reason: (&reason).into() });
381 }
382
383 pub fn push_terminated_voluntary(&mut self, stage: &Name) {
384 self.push(TraceEntryRef::Terminated { stage, reason: TerminationReasonRef::Voluntary });
385 }
386
387 pub fn push_terminated_supervision(&mut self, stage: &Name, child: &Name) {
388 self.push(TraceEntryRef::Terminated { stage, reason: TerminationReasonRef::Supervision(child) });
389 }
390
391 pub fn push_terminated_aborted(&mut self, stage: &Name) {
392 self.push(TraceEntryRef::Terminated { stage, reason: TerminationReasonRef::Aborted });
393 }
394
395 fn push<T: serde::Serialize>(&mut self, msg: T) {
396 let msg = to_cbor(&(Instant::now(), msg));
397
398 if self.max_size == 0 {
399 return;
400 }
401
402 self.used_size += msg.len();
403
404 #[expect(clippy::expect_used)]
405 while self.used_size > self.max_size && self.messages.len() > self.min_entries {
406 self.used_size -= self.messages.pop_front().expect("messages is definitely not empty").len();
407 self.dropped_messages += 1;
408 }
409
410 let one_more = self.messages.front().map(|m| m.len()).unwrap_or(0);
413 if self.used_size > self.max_size && self.used_size - one_more <= self.max_size {
414 self.used_size -= one_more;
415 self.messages.pop_front();
416 }
417
418 if self.used_size > self.max_size {
419 self.used_size -= msg.len();
420 self.dropped_messages += 1;
421 return;
422 }
423
424 self.messages.push_back(msg);
425 }
426
427 pub fn iter(&self) -> impl Iterator<Item = &[u8]> {
429 self.messages.iter().map(|m| m.as_slice())
430 }
431
432 pub fn iter_entries(&self) -> impl Iterator<Item = (Instant, TraceEntry)> {
434 #[expect(clippy::expect_used)]
435 self.messages.iter().map(|m| from_slice(m).expect("trace buffer is not supposed to contain invalid CBOR"))
436 }
437
438 pub fn take(&mut self) -> Vec<Vec<u8>> {
440 std::mem::take(&mut self.messages).into()
441 }
442
443 #[expect(clippy::expect_used)]
445 pub fn hydrate(&self) -> Vec<(Instant, TraceEntry)> {
446 self.messages
447 .iter()
448 .map(|m| from_slice(m).expect("trace buffer is not supposed to contain invalid CBOR"))
449 .collect()
450 }
451
452 #[expect(clippy::expect_used)]
454 pub fn hydrate_without_timestamps(&self) -> Vec<TraceEntry> {
455 self.messages
456 .iter()
457 .map(|m| {
458 from_slice::<(Instant, TraceEntry)>(m).expect("trace buffer is not supposed to contain invalid CBOR").1
459 })
460 .collect()
461 }
462
463 pub fn clear(&mut self) {
465 self.messages.clear();
466 self.used_size = 0;
467 self.dropped_messages = 0;
468 }
469
470 pub fn len(&self) -> usize {
472 self.messages.len()
473 }
474
475 pub fn is_empty(&self) -> bool {
477 self.messages.is_empty()
478 }
479
480 pub fn used_size(&self) -> usize {
482 self.used_size
483 }
484
485 pub fn dropped_messages(&self) -> usize {
487 self.dropped_messages
488 }
489
490 pub fn set_fetch_replay(&mut self, replay: std::vec::IntoIter<TraceEntry>) {
491 self.fetch_replay = Some(replay);
492 }
493
494 pub fn take_fetch_replay(&mut self) -> Option<std::vec::IntoIter<TraceEntry>> {
495 self.fetch_replay.take()
496 }
497
498 pub fn fetch_replay_mut(&mut self) -> Option<&mut std::vec::IntoIter<TraceEntry>> {
499 self.fetch_replay.as_mut()
500 }
501
502 pub fn drop_guard(this: &Arc<Mutex<Self>>) -> DropGuard {
503 DropGuard { buffer: this.clone(), active: true }
504 }
505}
506
507pub struct DropGuard {
508 buffer: Arc<Mutex<TraceBuffer>>,
509 active: bool,
510}
511
512impl DropGuard {
513 pub fn defuse(mut self) {
514 self.active = false;
515 }
516}
517
518impl Drop for DropGuard {
519 fn drop(&mut self) {
520 if !self.active {
521 return;
522 }
523 eprintln!("Dropping trace buffer");
524 for entry in self.buffer.lock().iter() {
525 match from_slice::<(Instant, TraceEntry)>(entry) {
526 Ok((instant, entry)) => eprintln!("{instant} {entry:?}"),
527 Err(error) => eprintln!("error deserializing trace entry: {error:?}"),
528 };
529 }
530 eprintln!("Dropped trace buffer");
531 }
532}
533
534#[allow(clippy::wildcard_enum_match_arm, clippy::panic)]
535pub fn find_next_external_suspend(
536 fetch_replay: &mut std::vec::IntoIter<TraceEntry>,
537 at_stage: &Name,
538) -> Option<Box<dyn ExternalEffect>> {
539 find_next(
540 fetch_replay,
541 |entry| entry.at_stage() == Some(at_stage),
542 |entry| match entry {
543 TraceEntry::Suspend(Effect::External { effect, .. }) => effect,
544 entry => panic!("unexpected trace entry when finding next external suspend: {:?}", entry),
545 },
546 )
547}
548
549#[allow(clippy::wildcard_enum_match_arm, clippy::panic)]
550pub fn find_next_external_resume(
551 fetch_replay: &mut std::vec::IntoIter<TraceEntry>,
552 at_stage: &Name,
553) -> Option<Box<dyn SendData>> {
554 find_next(
555 fetch_replay,
556 |entry| entry.at_stage() == Some(at_stage),
557 |entry| match entry {
558 TraceEntry::Resume { response: StageResponse::ExternalResponse(response), .. } => response,
559 entry => panic!("unexpected trace entry when finding next external resume: {:?}", entry),
560 },
561 )
562}
563
564fn find_next<T>(
565 fetch_replay: &mut std::vec::IntoIter<TraceEntry>,
566 predicate: impl Fn(&TraceEntry) -> bool,
567 extract: impl FnOnce(TraceEntry) -> T,
568) -> Option<T> {
569 let log = fetch_replay.as_mut_slice();
570 let idx = log.iter().position(predicate)?;
571 log[..=idx].rotate_right(1);
572 fetch_replay.next().map(extract)
573}
574
575#[cfg(test)]
576mod tests {
577 use tokio::sync::mpsc;
578
579 use super::*;
580 use crate::OutputEffect;
581
582 #[test]
583 fn test_serialization() {
584 let effect = OutputEffect::new(Name::from("test"), 42u32, mpsc::channel(1).0);
585 let trr = TraceEntryRefRef::Suspend(EffectRef::External { at_stage: &Name::from("test"), effect: &effect });
586 let rr = to_cbor(&trr);
587 let json = serde_json::to_string(&trr).unwrap();
588 let r = to_cbor(&TraceEntryRef::Suspend(&Effect::External {
589 at_stage: Name::from("test"),
590 effect: Box::new(effect),
591 }));
592 let effect = OutputEffect::new(Name::from("test"), 42u32, mpsc::channel(1).0);
593 let t =
594 to_cbor(&TraceEntry::suspend(Effect::External { at_stage: Name::from("test"), effect: Box::new(effect) }));
595 assert_eq!(rr, r);
596 assert_eq!(rr, t);
597
598 assert_eq!(
599 json,
600 r#"{"Suspend":{"External":{"at_stage":"test","effect":{"typetag":"pure_stage::output::OutputEffect<u32>","value":{"name":"test","msg":42,"sender":{}}}}}}"#
601 );
602 }
603}