1use crate::monitor::DownReason;
7use crate::record::{ObligationAbortReason, ObligationKind, ObligationState};
8use crate::trace::distributed::LogicalTime;
9use crate::types::{CancelReason, ObligationId, PanicPayload, RegionId, TaskId, Time};
10use core::fmt;
11use serde::{Deserialize, Serialize};
12use std::collections::{BTreeMap, BTreeSet};
13
14pub const TRACE_EVENT_SCHEMA_VERSION: u32 = 1;
16pub const BROWSER_TRACE_SCHEMA_VERSION: &str = "browser-trace-schema-v1";
18const MAX_BROWSER_TRACE_ATTRIBUTE_BYTES: usize = 128;
19
20#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[serde(rename_all = "snake_case")]
23pub enum BrowserTraceCategory {
24 Scheduler,
26 Timer,
28 HostCallback,
30 CapabilityInvocation,
32 CancellationTransition,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
38pub struct BrowserTraceEventSpec {
39 pub event_kind: String,
41 pub category: BrowserTraceCategory,
43 pub required_fields: Vec<String>,
45 pub redacted_fields: Vec<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51pub struct BrowserTraceCompatibility {
52 pub minimum_reader_version: String,
54 pub supported_reader_versions: Vec<String>,
56 pub backward_decode_aliases: Vec<String>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub struct BrowserTraceSchema {
63 pub schema_version: String,
65 pub required_envelope_fields: Vec<String>,
67 pub ordering_semantics: Vec<String>,
69 pub structured_log_required_fields: Vec<String>,
71 pub validation_failure_categories: Vec<String>,
73 pub event_specs: Vec<BrowserTraceEventSpec>,
75 pub compatibility: BrowserTraceCompatibility,
77}
78
79#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
81#[serde(rename_all = "snake_case")]
82pub enum BrowserCaptureSource {
83 Runtime,
85 Time,
87 Event,
89 HostInput,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
95pub struct BrowserCaptureMetadata {
96 pub host_turn_seq: u64,
98 pub source: BrowserCaptureSource,
100 pub source_seq: u64,
102 pub host_time_ns: u64,
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum TraceEventKind {
110 Spawn,
112 Schedule,
114 Yield,
116 Wake,
118 Poll,
120 Complete,
122 CancelRequest,
124 CancelAck,
126 WorkerCancelRequested,
128 WorkerCancelAcknowledged,
130 WorkerDrainStarted,
132 WorkerDrainCompleted,
134 WorkerFinalizeCompleted,
136 RegionCloseBegin,
138 RegionCloseComplete,
140 RegionCreated,
142 RegionCancelled,
144 ObligationReserve,
146 ObligationCommit,
148 ObligationAbort,
150 ObligationLeak,
152 TimeAdvance,
154 TimerScheduled,
156 TimerFired,
158 TimerCancelled,
160 IoRequested,
162 IoReady,
164 IoResult,
166 IoError,
168 RngSeed,
170 RngValue,
172 Checkpoint,
174 FuturelockDetected,
176 ChaosInjection,
178 UserTrace,
180 MonitorCreated,
182 MonitorDropped,
184 DownDelivered,
186 LinkCreated,
188 LinkDropped,
190 ExitDelivered,
192 TaskSpawnEnqueued,
194 TaskAdmitted,
196 BudgetInstalled,
199 BudgetConsumed,
202}
203
204impl TraceEventKind {
205 pub const ALL: [Self; 45] = [
210 Self::Spawn,
211 Self::Schedule,
212 Self::Yield,
213 Self::Wake,
214 Self::Poll,
215 Self::Complete,
216 Self::CancelRequest,
217 Self::CancelAck,
218 Self::WorkerCancelRequested,
219 Self::WorkerCancelAcknowledged,
220 Self::WorkerDrainStarted,
221 Self::WorkerDrainCompleted,
222 Self::WorkerFinalizeCompleted,
223 Self::RegionCloseBegin,
224 Self::RegionCloseComplete,
225 Self::RegionCreated,
226 Self::RegionCancelled,
227 Self::ObligationReserve,
228 Self::ObligationCommit,
229 Self::ObligationAbort,
230 Self::ObligationLeak,
231 Self::TimeAdvance,
232 Self::TimerScheduled,
233 Self::TimerFired,
234 Self::TimerCancelled,
235 Self::IoRequested,
236 Self::IoReady,
237 Self::IoResult,
238 Self::IoError,
239 Self::RngSeed,
240 Self::RngValue,
241 Self::Checkpoint,
242 Self::FuturelockDetected,
243 Self::ChaosInjection,
244 Self::UserTrace,
245 Self::MonitorCreated,
246 Self::MonitorDropped,
247 Self::DownDelivered,
248 Self::LinkCreated,
249 Self::LinkDropped,
250 Self::ExitDelivered,
251 Self::TaskSpawnEnqueued,
252 Self::TaskAdmitted,
253 Self::BudgetInstalled,
254 Self::BudgetConsumed,
255 ];
256
257 #[must_use]
259 pub const fn stable_name(self) -> &'static str {
260 match self {
261 Self::Spawn => "spawn",
262 Self::Schedule => "schedule",
263 Self::Yield => "yield",
264 Self::Wake => "wake",
265 Self::Poll => "poll",
266 Self::Complete => "complete",
267 Self::CancelRequest => "cancel_request",
268 Self::CancelAck => "cancel_ack",
269 Self::WorkerCancelRequested => "worker_cancel_requested",
270 Self::WorkerCancelAcknowledged => "worker_cancel_acknowledged",
271 Self::WorkerDrainStarted => "worker_drain_started",
272 Self::WorkerDrainCompleted => "worker_drain_completed",
273 Self::WorkerFinalizeCompleted => "worker_finalize_completed",
274 Self::RegionCloseBegin => "region_close_begin",
275 Self::RegionCloseComplete => "region_close_complete",
276 Self::RegionCreated => "region_created",
277 Self::RegionCancelled => "region_cancelled",
278 Self::ObligationReserve => "obligation_reserve",
279 Self::ObligationCommit => "obligation_commit",
280 Self::ObligationAbort => "obligation_abort",
281 Self::ObligationLeak => "obligation_leak",
282 Self::TimeAdvance => "time_advance",
283 Self::TimerScheduled => "timer_scheduled",
284 Self::TimerFired => "timer_fired",
285 Self::TimerCancelled => "timer_cancelled",
286 Self::IoRequested => "io_requested",
287 Self::IoReady => "io_ready",
288 Self::IoResult => "io_result",
289 Self::IoError => "io_error",
290 Self::RngSeed => "rng_seed",
291 Self::RngValue => "rng_value",
292 Self::Checkpoint => "checkpoint",
293 Self::FuturelockDetected => "futurelock_detected",
294 Self::ChaosInjection => "chaos_injection",
295 Self::UserTrace => "user_trace",
296 Self::MonitorCreated => "monitor_created",
297 Self::MonitorDropped => "monitor_dropped",
298 Self::DownDelivered => "down_delivered",
299 Self::LinkCreated => "link_created",
300 Self::LinkDropped => "link_dropped",
301 Self::ExitDelivered => "exit_delivered",
302 Self::TaskSpawnEnqueued => "task_spawn_enqueued",
303 Self::TaskAdmitted => "task_admitted",
304 Self::BudgetInstalled => "budget_installed",
305 Self::BudgetConsumed => "budget_consumed",
306 }
307 }
308
309 #[must_use]
311 pub const fn required_fields(self) -> &'static str {
312 match self {
313 Self::Spawn
314 | Self::Schedule
315 | Self::Yield
316 | Self::Wake
317 | Self::Poll
318 | Self::Complete
319 | Self::TaskSpawnEnqueued
320 | Self::TaskAdmitted => "task, region",
321 Self::CancelRequest | Self::CancelAck => "task, region, reason",
322 Self::WorkerCancelRequested
323 | Self::WorkerCancelAcknowledged
324 | Self::WorkerDrainStarted
325 | Self::WorkerDrainCompleted
326 | Self::WorkerFinalizeCompleted => {
327 "decision_seq, job_id, obligation, region, replay_hash, task, worker_id"
328 }
329 Self::RegionCloseBegin | Self::RegionCloseComplete | Self::RegionCreated => {
330 "region, parent"
331 }
332 Self::RegionCancelled => "region, reason",
333 Self::ObligationReserve => "obligation, task, region, kind, state",
334 Self::ObligationCommit | Self::ObligationLeak => {
335 "obligation, task, region, kind, state, duration_ns"
336 }
337 Self::ObligationAbort => {
338 "obligation, task, region, kind, state, duration_ns, abort_reason"
339 }
340 Self::TimeAdvance => "old, new",
341 Self::TimerScheduled => "timer_id, deadline",
342 Self::TimerFired | Self::TimerCancelled => "timer_id",
343 Self::IoRequested => "token, interest",
344 Self::IoReady => "token, readiness",
345 Self::IoResult => "token, bytes",
346 Self::IoError => "token, kind",
347 Self::RngSeed => "seed",
348 Self::RngValue => "value",
349 Self::Checkpoint => "sequence, active_tasks, active_regions",
350 Self::FuturelockDetected => "task, region, idle_steps, held",
351 Self::ChaosInjection => "kind, task, detail",
352 Self::UserTrace => "message",
353 Self::MonitorCreated | Self::MonitorDropped => {
354 "monitor_ref, watcher, watcher_region, monitored"
355 }
356 Self::DownDelivered => "monitor_ref, watcher, monitored, completion_vt, reason",
357 Self::LinkCreated | Self::LinkDropped => "link_ref, task_a, region_a, task_b, region_b",
358 Self::ExitDelivered => "link_ref, from, to, failure_vt, reason",
359 Self::BudgetInstalled => {
360 "task, region, protocol, deadline_ns, poll_quota, cost_quota, priority, source"
361 }
362 Self::BudgetConsumed => "task, region, protocol, deadline_ns, elapsed_ns, outcome",
363 }
364 }
365}
366
367#[must_use]
369pub const fn browser_trace_category_for_kind(kind: TraceEventKind) -> BrowserTraceCategory {
370 match kind {
371 TraceEventKind::Spawn
372 | TraceEventKind::Schedule
373 | TraceEventKind::Yield
374 | TraceEventKind::Wake
375 | TraceEventKind::Poll
376 | TraceEventKind::Complete
377 | TraceEventKind::Checkpoint
378 | TraceEventKind::FuturelockDetected
379 | TraceEventKind::TaskSpawnEnqueued
380 | TraceEventKind::TaskAdmitted
381 | TraceEventKind::BudgetInstalled
382 | TraceEventKind::BudgetConsumed => BrowserTraceCategory::Scheduler,
383 TraceEventKind::TimeAdvance
384 | TraceEventKind::TimerScheduled
385 | TraceEventKind::TimerFired
386 | TraceEventKind::TimerCancelled => BrowserTraceCategory::Timer,
387 TraceEventKind::IoRequested
388 | TraceEventKind::IoReady
389 | TraceEventKind::IoResult
390 | TraceEventKind::IoError
391 | TraceEventKind::RngSeed
392 | TraceEventKind::RngValue
393 | TraceEventKind::UserTrace
394 | TraceEventKind::ChaosInjection => BrowserTraceCategory::HostCallback,
395 TraceEventKind::ObligationReserve
396 | TraceEventKind::ObligationCommit
397 | TraceEventKind::ObligationAbort
398 | TraceEventKind::ObligationLeak
399 | TraceEventKind::RegionCreated
400 | TraceEventKind::MonitorCreated
401 | TraceEventKind::MonitorDropped
402 | TraceEventKind::DownDelivered
403 | TraceEventKind::LinkCreated
404 | TraceEventKind::LinkDropped
405 | TraceEventKind::ExitDelivered => BrowserTraceCategory::CapabilityInvocation,
406 TraceEventKind::CancelRequest
407 | TraceEventKind::CancelAck
408 | TraceEventKind::WorkerCancelRequested
409 | TraceEventKind::WorkerCancelAcknowledged
410 | TraceEventKind::WorkerDrainStarted
411 | TraceEventKind::WorkerDrainCompleted
412 | TraceEventKind::WorkerFinalizeCompleted
413 | TraceEventKind::RegionCloseBegin
414 | TraceEventKind::RegionCloseComplete
415 | TraceEventKind::RegionCancelled => BrowserTraceCategory::CancellationTransition,
416 }
417}
418
419#[must_use]
421pub const fn browser_trace_category_name(category: BrowserTraceCategory) -> &'static str {
422 match category {
423 BrowserTraceCategory::Scheduler => "scheduler",
424 BrowserTraceCategory::Timer => "timer",
425 BrowserTraceCategory::HostCallback => "host_callback",
426 BrowserTraceCategory::CapabilityInvocation => "capability_invocation",
427 BrowserTraceCategory::CancellationTransition => "cancellation_transition",
428 }
429}
430
431fn redacted_fields_for_kind(kind: TraceEventKind) -> Vec<String> {
432 match kind {
433 TraceEventKind::UserTrace => vec!["message".to_string()],
434 TraceEventKind::ChaosInjection => vec!["detail".to_string()],
435 _ => Vec::new(),
436 }
437}
438
439fn split_required_fields_csv(csv: &str) -> Vec<String> {
440 let mut fields = csv
441 .split(',')
442 .map(str::trim)
443 .filter(|value| !value.is_empty())
444 .map(ToString::to_string)
445 .collect::<Vec<_>>();
446 sort_and_dedup_strings(&mut fields);
447 fields
448}
449
450fn sort_and_dedup_strings(values: &mut Vec<String>) {
451 values.sort();
452 values.dedup();
453}
454
455fn trace_event_kind_from_stable_name(name: &str) -> Option<TraceEventKind> {
456 TraceEventKind::ALL
457 .iter()
458 .copied()
459 .find(|kind| kind.stable_name() == name)
460}
461
462fn validate_lexical_string_set(values: &[String], field: &str) -> Result<(), String> {
463 if values.is_empty() {
464 return Err(format!("{field} must be non-empty"));
465 }
466 for value in values {
467 if value.trim().is_empty() {
468 return Err(format!("{field} must not contain empty values"));
469 }
470 }
471 for window in values.windows(2) {
472 if window[0] >= window[1] {
473 return Err(format!("{field} must be lexically sorted and unique"));
474 }
475 }
476 Ok(())
477}
478
479#[must_use]
481pub fn browser_trace_schema_v1() -> BrowserTraceSchema {
482 let mut event_specs = TraceEventKind::ALL
483 .iter()
484 .map(|kind| {
485 let mut redacted_fields = redacted_fields_for_kind(*kind);
486 sort_and_dedup_strings(&mut redacted_fields);
487 BrowserTraceEventSpec {
488 event_kind: kind.stable_name().to_string(),
489 category: browser_trace_category_for_kind(*kind),
490 required_fields: split_required_fields_csv(kind.required_fields()),
491 redacted_fields,
492 }
493 })
494 .collect::<Vec<_>>();
495 event_specs.sort_by(|left, right| left.event_kind.cmp(&right.event_kind));
496
497 BrowserTraceSchema {
498 schema_version: BROWSER_TRACE_SCHEMA_VERSION.to_string(),
499 required_envelope_fields: vec![
500 "event_kind".to_string(),
501 "schema_version".to_string(),
502 "seq".to_string(),
503 "time_ns".to_string(),
504 "trace_id".to_string(),
505 ],
506 ordering_semantics: vec![
507 "events must be strictly ordered by seq ascending".to_string(),
508 "logical_time must be monotonic for comparable causal domains".to_string(),
509 "trace streams must be deterministic for identical seed/config/replay inputs"
510 .to_string(),
511 ],
512 structured_log_required_fields: vec![
513 "capture_host_time_ns".to_string(),
514 "capture_host_turn_seq".to_string(),
515 "capture_replay_key".to_string(),
516 "capture_source".to_string(),
517 "capture_source_seq".to_string(),
518 "event_kind".to_string(),
519 "schema_version".to_string(),
520 "seq".to_string(),
521 "sequence_group".to_string(),
522 "time_ns".to_string(),
523 "trace_id".to_string(),
524 "validation_failure_category".to_string(),
525 "validation_status".to_string(),
526 ],
527 validation_failure_categories: vec![
528 "invalid_event_payload".to_string(),
529 "missing_required_field".to_string(),
530 "schema_version_mismatch".to_string(),
531 "sequence_regression".to_string(),
532 ],
533 event_specs,
534 compatibility: BrowserTraceCompatibility {
535 minimum_reader_version: "browser-trace-schema-v0".to_string(),
536 supported_reader_versions: vec![
537 "browser-trace-schema-v0".to_string(),
538 BROWSER_TRACE_SCHEMA_VERSION.to_string(),
539 ],
540 backward_decode_aliases: vec!["browser-trace-schema-v0".to_string()],
541 },
542 }
543}
544
545#[allow(clippy::too_many_lines)]
552pub fn validate_browser_trace_schema(schema: &BrowserTraceSchema) -> Result<(), String> {
553 if schema.schema_version != BROWSER_TRACE_SCHEMA_VERSION {
554 return Err(format!(
555 "unsupported browser trace schema version {}",
556 schema.schema_version
557 ));
558 }
559
560 validate_lexical_string_set(&schema.required_envelope_fields, "required_envelope_fields")?;
561 validate_lexical_string_set(&schema.ordering_semantics, "ordering_semantics")?;
562 validate_lexical_string_set(
563 &schema.structured_log_required_fields,
564 "structured_log_required_fields",
565 )?;
566 validate_lexical_string_set(
567 &schema.validation_failure_categories,
568 "validation_failure_categories",
569 )?;
570
571 for required in [
572 "capture_host_time_ns",
573 "capture_host_turn_seq",
574 "capture_replay_key",
575 "capture_source",
576 "capture_source_seq",
577 "trace_id",
578 "time_ns",
579 "seq",
580 "sequence_group",
581 "event_kind",
582 "schema_version",
583 "validation_failure_category",
584 "validation_status",
585 ] {
586 if !schema
587 .structured_log_required_fields
588 .iter()
589 .any(|field| field == required)
590 {
591 return Err(format!("structured_log_required_fields missing {required}"));
592 }
593 }
594
595 if schema.event_specs.is_empty() {
596 return Err("event_specs must be non-empty".to_string());
597 }
598 let event_kinds = schema
599 .event_specs
600 .iter()
601 .map(|entry| entry.event_kind.clone())
602 .collect::<Vec<_>>();
603 validate_lexical_string_set(&event_kinds, "event_specs.event_kind")?;
604
605 let expected = TraceEventKind::ALL
606 .iter()
607 .map(|kind| kind.stable_name().to_string())
608 .collect::<BTreeSet<_>>();
609 let observed = event_kinds.into_iter().collect::<BTreeSet<_>>();
610 if expected != observed {
611 return Err("event_specs must include exactly all TraceEventKind stable names".to_string());
612 }
613
614 for entry in &schema.event_specs {
615 validate_lexical_string_set(
616 &entry.required_fields,
617 &format!("event_specs[{}].required_fields", entry.event_kind),
618 )?;
619 if !entry.redacted_fields.is_empty() {
620 validate_lexical_string_set(
621 &entry.redacted_fields,
622 &format!("event_specs[{}].redacted_fields", entry.event_kind),
623 )?;
624 for field in &entry.redacted_fields {
625 if !entry
626 .required_fields
627 .iter()
628 .any(|required| required == field)
629 {
630 return Err(format!(
631 "event_specs[{}].redacted_fields contains unknown field {}",
632 entry.event_kind, field
633 ));
634 }
635 }
636 }
637 }
638
639 if schema
640 .compatibility
641 .minimum_reader_version
642 .trim()
643 .is_empty()
644 {
645 return Err("compatibility.minimum_reader_version must be non-empty".to_string());
646 }
647 validate_lexical_string_set(
648 &schema.compatibility.supported_reader_versions,
649 "compatibility.supported_reader_versions",
650 )?;
651 if !schema
652 .compatibility
653 .supported_reader_versions
654 .iter()
655 .any(|version| version == &schema.compatibility.minimum_reader_version)
656 {
657 return Err("minimum_reader_version missing from supported_reader_versions".to_string());
658 }
659 if !schema
660 .compatibility
661 .supported_reader_versions
662 .iter()
663 .any(|version| version == BROWSER_TRACE_SCHEMA_VERSION)
664 {
665 return Err("supported_reader_versions must include browser-trace-schema-v1".to_string());
666 }
667 validate_lexical_string_set(
668 &schema.compatibility.backward_decode_aliases,
669 "compatibility.backward_decode_aliases",
670 )?;
671
672 Ok(())
673}
674
675#[derive(Debug, Deserialize)]
676struct BrowserTraceSchemaLegacyV0 {
677 schema_version: String,
678 required_envelope_fields: Vec<String>,
679 ordering_semantics: Vec<String>,
680 event_specs: Vec<BrowserTraceEventSpecLegacyV0>,
681}
682
683#[derive(Debug, Deserialize)]
684struct BrowserTraceEventSpecLegacyV0 {
685 event_kind: String,
686 category: Option<BrowserTraceCategory>,
687 required_fields: Option<Vec<String>>,
688 redacted_fields: Option<Vec<String>>,
689}
690
691fn upgrade_legacy_event_specs(
692 legacy_specs: Vec<BrowserTraceEventSpecLegacyV0>,
693) -> Result<Vec<BrowserTraceEventSpec>, String> {
694 let mut event_specs = Vec::with_capacity(legacy_specs.len());
695 for legacy in legacy_specs {
696 let kind = trace_event_kind_from_stable_name(legacy.event_kind.as_str())
697 .ok_or_else(|| format!("unknown legacy event kind {}", legacy.event_kind))?;
698
699 let mut required_fields = legacy
700 .required_fields
701 .unwrap_or_else(|| split_required_fields_csv(kind.required_fields()));
702 sort_and_dedup_strings(&mut required_fields);
703
704 let mut redacted_fields = legacy
705 .redacted_fields
706 .unwrap_or_else(|| redacted_fields_for_kind(kind));
707 sort_and_dedup_strings(&mut redacted_fields);
708
709 event_specs.push(BrowserTraceEventSpec {
710 event_kind: kind.stable_name().to_string(),
711 category: legacy
712 .category
713 .unwrap_or_else(|| browser_trace_category_for_kind(kind)),
714 required_fields,
715 redacted_fields,
716 });
717 }
718 event_specs.sort_by(|left, right| left.event_kind.cmp(&right.event_kind));
719 Ok(event_specs)
720}
721
722pub fn decode_browser_trace_schema(payload: &str) -> Result<BrowserTraceSchema, String> {
728 let value: serde_json::Value =
729 serde_json::from_str(payload).map_err(|err| format!("invalid schema JSON: {err}"))?;
730 let version = value
731 .get("schema_version")
732 .and_then(serde_json::Value::as_str)
733 .ok_or_else(|| "schema_version must be a string".to_string())?;
734
735 let schema = match version {
736 BROWSER_TRACE_SCHEMA_VERSION => serde_json::from_value::<BrowserTraceSchema>(value)
737 .map_err(|err| format!("invalid browser-trace-schema-v1 payload: {err}"))?,
738 "browser-trace-schema-v0" => {
739 let legacy = serde_json::from_value::<BrowserTraceSchemaLegacyV0>(value)
740 .map_err(|err| format!("invalid browser-trace-schema-v0 payload: {err}"))?;
741 if legacy.schema_version != "browser-trace-schema-v0" {
742 return Err(format!(
743 "invalid legacy schema version {}",
744 legacy.schema_version
745 ));
746 }
747 let mut schema = browser_trace_schema_v1();
748 schema.required_envelope_fields = legacy.required_envelope_fields;
749 schema.ordering_semantics = legacy.ordering_semantics;
750 schema.event_specs = upgrade_legacy_event_specs(legacy.event_specs)?;
751 schema.compatibility.backward_decode_aliases =
752 vec!["browser-trace-schema-v0".to_string()];
753 schema.compatibility.minimum_reader_version = "browser-trace-schema-v0".to_string();
754 schema
755 }
756 other => {
757 return Err(format!("unsupported browser trace schema version {other}"));
758 }
759 };
760
761 validate_browser_trace_schema(&schema)?;
762 Ok(schema)
763}
764
765#[must_use]
788pub fn redact_browser_trace_event(event: &TraceEvent) -> TraceEvent {
789 let mut redacted = event.clone();
790 redacted.data = redact_browser_trace_data(&event.data);
791 redacted
792}
793
794fn redact_browser_trace_data(data: &TraceData) -> TraceData {
808 match data {
809 TraceData::None => TraceData::None,
810 TraceData::Task { task, region } => TraceData::Task {
811 task: *task,
812 region: *region,
813 },
814 TraceData::Region { region, parent } => TraceData::Region {
815 region: *region,
816 parent: *parent,
817 },
818 TraceData::Obligation {
819 obligation,
820 task,
821 region,
822 kind,
823 state,
824 duration_ns,
825 abort_reason,
826 } => TraceData::Obligation {
827 obligation: *obligation,
828 task: *task,
829 region: *region,
830 kind: *kind,
831 state: *state,
832 duration_ns: *duration_ns,
833 abort_reason: *abort_reason,
834 },
835 TraceData::Cancel {
836 task,
837 region,
838 reason,
839 } => TraceData::Cancel {
840 task: *task,
841 region: *region,
842 reason: redact_cancel_reason(reason),
843 },
844 TraceData::Worker {
845 worker_id: _,
846 job_id,
847 decision_seq,
848 replay_hash,
849 task,
850 region,
851 obligation,
852 } => TraceData::Worker {
853 worker_id: "<redacted>".to_string(),
854 job_id: *job_id,
855 decision_seq: *decision_seq,
856 replay_hash: *replay_hash,
857 task: *task,
858 region: *region,
859 obligation: *obligation,
860 },
861 TraceData::RegionCancel { region, reason } => TraceData::RegionCancel {
862 region: *region,
863 reason: redact_cancel_reason(reason),
864 },
865 TraceData::Time { old, new } => TraceData::Time {
866 old: *old,
867 new: *new,
868 },
869 TraceData::Timer { timer_id, deadline } => TraceData::Timer {
870 timer_id: *timer_id,
871 deadline: *deadline,
872 },
873 TraceData::IoRequested { token, interest } => TraceData::IoRequested {
874 token: *token,
875 interest: *interest,
876 },
877 TraceData::IoReady { token, readiness } => TraceData::IoReady {
878 token: *token,
879 readiness: *readiness,
880 },
881 TraceData::IoResult { token, bytes } => TraceData::IoResult {
882 token: *token,
883 bytes: *bytes,
884 },
885 TraceData::IoError { token, kind } => TraceData::IoError {
886 token: *token,
887 kind: *kind,
888 },
889 TraceData::RngSeed { seed } => TraceData::RngSeed { seed: *seed },
890 TraceData::RngValue { value } => TraceData::RngValue { value: *value },
891 TraceData::Checkpoint {
892 sequence,
893 active_tasks,
894 active_regions,
895 } => TraceData::Checkpoint {
896 sequence: *sequence,
897 active_tasks: *active_tasks,
898 active_regions: *active_regions,
899 },
900 TraceData::Futurelock {
901 task,
902 region,
903 idle_steps,
904 held,
905 } => TraceData::Futurelock {
906 task: *task,
907 region: *region,
908 idle_steps: *idle_steps,
909 held: held.clone(),
910 },
911 TraceData::Monitor {
912 monitor_ref,
913 watcher,
914 watcher_region,
915 monitored,
916 } => TraceData::Monitor {
917 monitor_ref: *monitor_ref,
918 watcher: *watcher,
919 watcher_region: *watcher_region,
920 monitored: *monitored,
921 },
922 TraceData::Down {
923 monitor_ref,
924 watcher,
925 monitored,
926 completion_vt,
927 reason,
928 } => TraceData::Down {
929 monitor_ref: *monitor_ref,
930 watcher: *watcher,
931 monitored: *monitored,
932 completion_vt: *completion_vt,
933 reason: redact_down_reason(reason),
934 },
935 TraceData::Link {
936 link_ref,
937 task_a,
938 region_a,
939 task_b,
940 region_b,
941 } => TraceData::Link {
942 link_ref: *link_ref,
943 task_a: *task_a,
944 region_a: *region_a,
945 task_b: *task_b,
946 region_b: *region_b,
947 },
948 TraceData::Exit {
949 link_ref,
950 from,
951 to,
952 failure_vt,
953 reason,
954 } => TraceData::Exit {
955 link_ref: *link_ref,
956 from: *from,
957 to: *to,
958 failure_vt: *failure_vt,
959 reason: redact_down_reason(reason),
960 },
961 TraceData::Message(_) => TraceData::Message("<redacted>".to_string()),
962 TraceData::Chaos {
963 kind,
964 task,
965 detail: _,
966 } => TraceData::Chaos {
967 kind: kind.clone(),
968 task: *task,
969 detail: "<redacted>".to_string(),
970 },
971 TraceData::Budget {
974 task,
975 region,
976 protocol,
977 deadline_ns,
978 poll_quota,
979 cost_quota,
980 priority,
981 source,
982 elapsed_ns,
983 outcome,
984 } => TraceData::Budget {
985 task: *task,
986 region: *region,
987 protocol: protocol.clone(),
988 deadline_ns: *deadline_ns,
989 poll_quota: *poll_quota,
990 cost_quota: *cost_quota,
991 priority: *priority,
992 source: source.clone(),
993 elapsed_ns: *elapsed_ns,
994 outcome: outcome.clone(),
995 },
996 }
997}
998
999fn redact_cancel_reason(reason: &CancelReason) -> CancelReason {
1002 let mut redacted = reason.clone();
1003 if redacted.message.is_some() {
1004 redacted.message = Some("<redacted>".to_string());
1005 }
1006 redacted
1007}
1008
1009fn redact_down_reason(reason: &DownReason) -> DownReason {
1013 match reason {
1014 DownReason::Normal => DownReason::Normal,
1015 DownReason::Error(_) => DownReason::Error("<redacted>".to_string()),
1016 DownReason::Cancelled(cr) => DownReason::Cancelled(redact_cancel_reason(cr)),
1017 DownReason::Panicked(payload) => DownReason::Panicked(redact_panic_payload(payload)),
1018 }
1019}
1020
1021fn redact_panic_payload(_payload: &PanicPayload) -> PanicPayload {
1025 PanicPayload::new("<redacted>")
1026}
1027
1028fn default_browser_capture_metadata(event: &TraceEvent) -> BrowserCaptureMetadata {
1029 BrowserCaptureMetadata {
1030 host_turn_seq: event.seq,
1031 source: BrowserCaptureSource::Runtime,
1032 source_seq: event.seq,
1033 host_time_ns: event.time.as_nanos(),
1034 }
1035}
1036
1037fn stable_browser_trace_hash(bytes: &[u8]) -> u64 {
1038 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
1039 for &byte in bytes {
1040 hash ^= u64::from(byte);
1041 hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
1042 }
1043 hash
1044}
1045
1046fn cap_browser_trace_attribute(value: &str) -> String {
1047 if value.len() <= MAX_BROWSER_TRACE_ATTRIBUTE_BYTES {
1048 return value.to_string();
1049 }
1050
1051 let suffix = format!("#{:016x}", stable_browser_trace_hash(value.as_bytes()));
1052 let mut cut = MAX_BROWSER_TRACE_ATTRIBUTE_BYTES.saturating_sub(suffix.len());
1053 while cut > 0 && !value.is_char_boundary(cut) {
1054 cut -= 1;
1055 }
1056
1057 let mut capped = value[..cut].to_string();
1058 capped.push_str(&suffix);
1059 capped
1060}
1061
1062fn obligation_state_name(state: ObligationState) -> &'static str {
1063 match state {
1064 ObligationState::Reserved => "reserved",
1065 ObligationState::Committed => "committed",
1066 ObligationState::Aborted => "aborted",
1067 ObligationState::Leaked => "leaked",
1068 }
1069}
1070
1071fn optional_time_field(value: Option<Time>) -> String {
1072 value.map_or_else(|| "none".to_string(), |time| time.as_nanos().to_string())
1073}
1074
1075fn optional_display_field<T: fmt::Display>(value: Option<T>) -> String {
1076 value.map_or_else(|| "none".to_string(), |value| value.to_string())
1077}
1078
1079fn futurelock_held_field(held: &[(ObligationId, ObligationKind)]) -> String {
1080 let held = held
1081 .iter()
1082 .map(|(obligation, kind)| format!("{obligation}:{kind}"))
1083 .collect::<Vec<_>>();
1084 serde_json::to_string(&held).expect("futurelock held obligations serialize")
1085}
1086
1087fn insert_browser_trace_payload_fields(fields: &mut BTreeMap<String, String>, event: &TraceEvent) {
1088 match &event.data {
1089 TraceData::None => {}
1090 TraceData::Task { task, region } => {
1091 fields.insert("task".to_string(), task.to_string());
1092 fields.insert("region".to_string(), region.to_string());
1093 }
1094 TraceData::Budget {
1095 task,
1096 region,
1097 protocol,
1098 deadline_ns,
1099 poll_quota,
1100 cost_quota,
1101 priority,
1102 source,
1103 elapsed_ns,
1104 outcome,
1105 } => {
1106 fields.insert("task".to_string(), task.to_string());
1107 fields.insert("region".to_string(), region.to_string());
1108 fields.insert("protocol".to_string(), protocol.clone());
1109 fields.insert(
1110 "deadline_ns".to_string(),
1111 optional_display_field(*deadline_ns),
1112 );
1113 fields.insert("poll_quota".to_string(), poll_quota.to_string());
1114 fields.insert(
1115 "cost_quota".to_string(),
1116 optional_display_field(*cost_quota),
1117 );
1118 fields.insert("priority".to_string(), priority.to_string());
1119 if let Some(source) = source {
1120 fields.insert("source".to_string(), source.clone());
1121 }
1122 if let Some(elapsed_ns) = elapsed_ns {
1123 fields.insert("elapsed_ns".to_string(), elapsed_ns.to_string());
1124 }
1125 if let Some(outcome) = outcome {
1126 fields.insert("outcome".to_string(), outcome.clone());
1127 }
1128 }
1129 TraceData::Region { region, parent } => {
1130 fields.insert("region".to_string(), region.to_string());
1131 fields.insert("parent".to_string(), optional_display_field(*parent));
1132 }
1133 TraceData::Obligation {
1134 obligation,
1135 task,
1136 region,
1137 kind,
1138 state,
1139 duration_ns,
1140 abort_reason,
1141 } => {
1142 fields.insert("obligation".to_string(), obligation.to_string());
1143 fields.insert("task".to_string(), task.to_string());
1144 fields.insert("region".to_string(), region.to_string());
1145 fields.insert("kind".to_string(), kind.to_string());
1146 fields.insert(
1147 "state".to_string(),
1148 obligation_state_name(*state).to_string(),
1149 );
1150
1151 if matches!(
1152 event.kind,
1153 TraceEventKind::ObligationCommit
1154 | TraceEventKind::ObligationAbort
1155 | TraceEventKind::ObligationLeak
1156 ) {
1157 fields.insert(
1158 "duration_ns".to_string(),
1159 duration_ns.map_or_else(|| "none".to_string(), |value| value.to_string()),
1160 );
1161 }
1162
1163 if matches!(event.kind, TraceEventKind::ObligationAbort) {
1164 fields.insert(
1165 "abort_reason".to_string(),
1166 abort_reason.map_or_else(|| "none".to_string(), |reason| reason.to_string()),
1167 );
1168 }
1169 }
1170 TraceData::Cancel {
1171 task,
1172 region,
1173 reason,
1174 } => {
1175 fields.insert("task".to_string(), task.to_string());
1176 fields.insert("region".to_string(), region.to_string());
1177 fields.insert("reason".to_string(), reason.to_string());
1178 }
1179 TraceData::Worker {
1180 worker_id,
1181 job_id,
1182 decision_seq,
1183 replay_hash,
1184 task,
1185 region,
1186 obligation,
1187 } => {
1188 fields.insert("decision_seq".to_string(), decision_seq.to_string());
1189 fields.insert("job_id".to_string(), job_id.to_string());
1190 fields.insert("obligation".to_string(), obligation.to_string());
1191 fields.insert("region".to_string(), region.to_string());
1192 fields.insert("replay_hash".to_string(), replay_hash.to_string());
1193 fields.insert("task".to_string(), task.to_string());
1194 fields.insert(
1195 "worker_id".to_string(),
1196 cap_browser_trace_attribute(worker_id),
1197 );
1198 }
1199 TraceData::RegionCancel { region, reason } => {
1200 fields.insert("region".to_string(), region.to_string());
1201 fields.insert("reason".to_string(), reason.to_string());
1202 }
1203 TraceData::Time { old, new } => {
1204 fields.insert("old".to_string(), old.as_nanos().to_string());
1205 fields.insert("new".to_string(), new.as_nanos().to_string());
1206 }
1207 TraceData::Timer { timer_id, deadline } => {
1208 fields.insert("timer_id".to_string(), timer_id.to_string());
1209 if matches!(event.kind, TraceEventKind::TimerScheduled) || deadline.is_some() {
1210 fields.insert("deadline".to_string(), optional_time_field(*deadline));
1211 }
1212 }
1213 TraceData::IoRequested { token, interest } => {
1214 fields.insert("token".to_string(), token.to_string());
1215 fields.insert("interest".to_string(), interest.to_string());
1216 }
1217 TraceData::IoReady { token, readiness } => {
1218 fields.insert("token".to_string(), token.to_string());
1219 fields.insert("readiness".to_string(), readiness.to_string());
1220 }
1221 TraceData::IoResult { token, bytes } => {
1222 fields.insert("token".to_string(), token.to_string());
1223 fields.insert("bytes".to_string(), bytes.to_string());
1224 }
1225 TraceData::IoError { token, kind } => {
1226 fields.insert("token".to_string(), token.to_string());
1227 fields.insert("kind".to_string(), kind.to_string());
1228 }
1229 TraceData::RngSeed { seed } => {
1230 fields.insert("seed".to_string(), seed.to_string());
1231 }
1232 TraceData::RngValue { value } => {
1233 fields.insert("value".to_string(), value.to_string());
1234 }
1235 TraceData::Checkpoint {
1236 sequence,
1237 active_tasks,
1238 active_regions,
1239 } => {
1240 fields.insert("sequence".to_string(), sequence.to_string());
1241 fields.insert("active_tasks".to_string(), active_tasks.to_string());
1242 fields.insert("active_regions".to_string(), active_regions.to_string());
1243 }
1244 TraceData::Futurelock {
1245 task,
1246 region,
1247 idle_steps,
1248 held,
1249 } => {
1250 fields.insert("task".to_string(), task.to_string());
1251 fields.insert("region".to_string(), region.to_string());
1252 fields.insert("idle_steps".to_string(), idle_steps.to_string());
1253 fields.insert("held".to_string(), futurelock_held_field(held));
1254 }
1255 TraceData::Monitor {
1256 monitor_ref,
1257 watcher,
1258 watcher_region,
1259 monitored,
1260 } => {
1261 fields.insert("monitor_ref".to_string(), monitor_ref.to_string());
1262 fields.insert("watcher".to_string(), watcher.to_string());
1263 fields.insert("watcher_region".to_string(), watcher_region.to_string());
1264 fields.insert("monitored".to_string(), monitored.to_string());
1265 }
1266 TraceData::Down {
1267 monitor_ref,
1268 watcher,
1269 monitored,
1270 completion_vt,
1271 reason,
1272 } => {
1273 fields.insert("monitor_ref".to_string(), monitor_ref.to_string());
1274 fields.insert("watcher".to_string(), watcher.to_string());
1275 fields.insert("monitored".to_string(), monitored.to_string());
1276 fields.insert(
1277 "completion_vt".to_string(),
1278 completion_vt.as_nanos().to_string(),
1279 );
1280 fields.insert("reason".to_string(), reason.to_string());
1281 }
1282 TraceData::Link {
1283 link_ref,
1284 task_a,
1285 region_a,
1286 task_b,
1287 region_b,
1288 } => {
1289 fields.insert("link_ref".to_string(), link_ref.to_string());
1290 fields.insert("task_a".to_string(), task_a.to_string());
1291 fields.insert("region_a".to_string(), region_a.to_string());
1292 fields.insert("task_b".to_string(), task_b.to_string());
1293 fields.insert("region_b".to_string(), region_b.to_string());
1294 }
1295 TraceData::Exit {
1296 link_ref,
1297 from,
1298 to,
1299 failure_vt,
1300 reason,
1301 } => {
1302 fields.insert("link_ref".to_string(), link_ref.to_string());
1303 fields.insert("from".to_string(), from.to_string());
1304 fields.insert("to".to_string(), to.to_string());
1305 fields.insert("failure_vt".to_string(), failure_vt.as_nanos().to_string());
1306 fields.insert("reason".to_string(), reason.to_string());
1307 }
1308 TraceData::Message(message) => {
1309 fields.insert("message".to_string(), message.clone());
1310 }
1311 TraceData::Chaos { kind, task, detail } => {
1312 fields.insert("kind".to_string(), kind.clone());
1313 fields.insert("task".to_string(), optional_display_field(*task));
1314 fields.insert("detail".to_string(), detail.clone());
1315 }
1316 }
1317}
1318
1319fn browser_trace_sequence_group(event: &TraceEvent) -> String {
1320 let raw = match &event.data {
1324 TraceData::Task { task, .. }
1325 | TraceData::Cancel { task, .. }
1326 | TraceData::Futurelock { task, .. }
1327 | TraceData::Budget { task, .. } => format!("task:{task}"),
1328 TraceData::Region { region, .. } | TraceData::RegionCancel { region, .. } => {
1329 format!("region:{region}")
1330 }
1331 TraceData::Obligation { obligation, .. } => format!("obligation:{obligation}"),
1332 TraceData::Worker {
1333 worker_id, job_id, ..
1334 } => format!("worker_job:{job_id}:{worker_id}"),
1335 TraceData::Time { .. } => "time".to_string(),
1336 TraceData::Timer { timer_id, .. } => format!("timer:{timer_id}"),
1337 TraceData::IoRequested { token, .. }
1338 | TraceData::IoReady { token, .. }
1339 | TraceData::IoResult { token, .. }
1340 | TraceData::IoError { token, .. } => format!("io:{token}"),
1341 TraceData::RngSeed { .. } | TraceData::RngValue { .. } => "rng".to_string(),
1342 TraceData::Checkpoint { sequence, .. } => format!("checkpoint:{sequence}"),
1343 TraceData::Monitor { monitor_ref, .. } | TraceData::Down { monitor_ref, .. } => {
1344 format!("monitor:{monitor_ref}")
1345 }
1346 TraceData::Link { link_ref, .. } | TraceData::Exit { link_ref, .. } => {
1347 format!("link:{link_ref}")
1348 }
1349 TraceData::Message(_) => "user_trace".to_string(),
1350 TraceData::Chaos {
1351 task: Some(task), ..
1352 } => format!("task:{task}"),
1353 TraceData::Chaos { task: None, .. } => "chaos".to_string(),
1354 TraceData::None => format!("kind:{}", event.kind.stable_name()),
1355 };
1356 cap_browser_trace_attribute(&raw)
1357}
1358
1359fn browser_capture_replay_key(metadata: &BrowserCaptureMetadata) -> String {
1360 format!(
1361 "{}:{}:{}:{}",
1362 match metadata.source {
1363 BrowserCaptureSource::Runtime => "runtime",
1364 BrowserCaptureSource::Time => "time",
1365 BrowserCaptureSource::Event => "event",
1366 BrowserCaptureSource::HostInput => "host_input",
1367 },
1368 metadata.host_turn_seq,
1369 metadata.source_seq,
1370 metadata.host_time_ns
1371 )
1372}
1373
1374#[must_use]
1379pub fn browser_trace_log_fields_with_capture(
1380 event: &TraceEvent,
1381 trace_id: &str,
1382 validation_failure_category: Option<&str>,
1383 capture_metadata: Option<&BrowserCaptureMetadata>,
1384) -> BTreeMap<String, String> {
1385 let event = &redact_browser_trace_event(event);
1395 let capture = capture_metadata
1396 .cloned()
1397 .unwrap_or_else(|| default_browser_capture_metadata(event));
1398 let mut fields = BTreeMap::new();
1399 fields.insert(
1400 "capture_host_time_ns".to_string(),
1401 capture.host_time_ns.to_string(),
1402 );
1403 fields.insert(
1404 "capture_host_turn_seq".to_string(),
1405 capture.host_turn_seq.to_string(),
1406 );
1407 fields.insert(
1408 "capture_replay_key".to_string(),
1409 browser_capture_replay_key(&capture),
1410 );
1411 fields.insert(
1412 "capture_source".to_string(),
1413 match capture.source {
1414 BrowserCaptureSource::Runtime => "runtime".to_string(),
1415 BrowserCaptureSource::Time => "time".to_string(),
1416 BrowserCaptureSource::Event => "event".to_string(),
1417 BrowserCaptureSource::HostInput => "host_input".to_string(),
1418 },
1419 );
1420 fields.insert(
1421 "capture_source_seq".to_string(),
1422 capture.source_seq.to_string(),
1423 );
1424 fields.insert(
1425 "event_kind".to_string(),
1426 event.kind.stable_name().to_string(),
1427 );
1428 fields.insert(
1429 "schema_version".to_string(),
1430 BROWSER_TRACE_SCHEMA_VERSION.to_string(),
1431 );
1432 fields.insert("seq".to_string(), event.seq.to_string());
1433 fields.insert("time_ns".to_string(), event.time.as_nanos().to_string());
1434 fields.insert("trace_id".to_string(), trace_id.to_string());
1435 fields.insert(
1436 "sequence_group".to_string(),
1437 browser_trace_sequence_group(event),
1438 );
1439 let failure_category = validation_failure_category
1440 .filter(|category| !category.trim().is_empty())
1441 .unwrap_or("none");
1442 fields.insert(
1443 "validation_failure_category".to_string(),
1444 failure_category.to_string(),
1445 );
1446 fields.insert(
1447 "validation_status".to_string(),
1448 if failure_category == "none" {
1449 "valid".to_string()
1450 } else {
1451 "invalid".to_string()
1452 },
1453 );
1454 insert_browser_trace_payload_fields(&mut fields, event);
1455 fields
1456}
1457
1458#[must_use]
1460pub fn browser_trace_log_fields(
1461 event: &TraceEvent,
1462 trace_id: &str,
1463 validation_failure_category: Option<&str>,
1464) -> BTreeMap<String, String> {
1465 browser_trace_log_fields_with_capture(event, trace_id, validation_failure_category, None)
1466}
1467
1468impl fmt::Display for TraceEventKind {
1469 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1470 f.write_str(self.stable_name())
1471 }
1472}
1473
1474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1476pub enum TraceData {
1477 None,
1479 Task {
1481 task: TaskId,
1483 region: RegionId,
1485 },
1486 Region {
1488 region: RegionId,
1490 parent: Option<RegionId>,
1492 },
1493 Obligation {
1495 obligation: ObligationId,
1497 task: TaskId,
1499 region: RegionId,
1501 kind: ObligationKind,
1503 state: ObligationState,
1505 duration_ns: Option<u64>,
1507 abort_reason: Option<ObligationAbortReason>,
1509 },
1510 Cancel {
1512 task: TaskId,
1514 region: RegionId,
1516 reason: CancelReason,
1518 },
1519 Worker {
1521 worker_id: String,
1523 job_id: u64,
1525 decision_seq: u64,
1527 replay_hash: u64,
1529 task: TaskId,
1531 region: RegionId,
1533 obligation: ObligationId,
1535 },
1536 RegionCancel {
1538 region: RegionId,
1540 reason: CancelReason,
1542 },
1543 Time {
1545 old: Time,
1547 new: Time,
1549 },
1550 Timer {
1552 timer_id: u64,
1554 deadline: Option<Time>,
1556 },
1557 IoRequested {
1559 token: u64,
1561 interest: u8,
1563 },
1564 IoReady {
1566 token: u64,
1568 readiness: u8,
1570 },
1571 IoResult {
1573 token: u64,
1575 bytes: i64,
1577 },
1578 IoError {
1580 token: u64,
1582 kind: u8,
1584 },
1585 RngSeed {
1587 seed: u64,
1589 },
1590 RngValue {
1592 value: u64,
1594 },
1595 Checkpoint {
1597 sequence: u64,
1599 active_tasks: u32,
1601 active_regions: u32,
1603 },
1604 Futurelock {
1606 task: TaskId,
1608 region: RegionId,
1610 idle_steps: u64,
1612 held: Vec<(ObligationId, ObligationKind)>,
1614 },
1615 Monitor {
1617 monitor_ref: u64,
1619 watcher: TaskId,
1621 watcher_region: RegionId,
1623 monitored: TaskId,
1625 },
1626 Down {
1630 monitor_ref: u64,
1632 watcher: TaskId,
1634 monitored: TaskId,
1636 completion_vt: Time,
1638 reason: DownReason,
1640 },
1641 Link {
1643 link_ref: u64,
1645 task_a: TaskId,
1647 region_a: RegionId,
1649 task_b: TaskId,
1651 region_b: RegionId,
1653 },
1654 Exit {
1658 link_ref: u64,
1660 from: TaskId,
1662 to: TaskId,
1664 failure_vt: Time,
1666 reason: DownReason,
1668 },
1669 Message(String),
1671 Chaos {
1673 kind: String,
1675 task: Option<TaskId>,
1677 detail: String,
1679 },
1680 Budget {
1683 task: TaskId,
1685 region: RegionId,
1687 protocol: String,
1689 deadline_ns: Option<u64>,
1691 poll_quota: u64,
1693 cost_quota: Option<u64>,
1695 priority: u8,
1697 source: Option<String>,
1699 elapsed_ns: Option<u64>,
1701 outcome: Option<String>,
1705 },
1706}
1707
1708#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1710pub struct TraceEvent {
1711 pub version: u32,
1713 pub seq: u64,
1715 pub time: Time,
1717 pub logical_time: Option<LogicalTime>,
1723 pub kind: TraceEventKind,
1725 pub data: TraceData,
1727}
1728
1729macro_rules! trace_event_constructors {
1730 ($(
1731 $(#[$meta:meta])*
1732 $name:ident($($arg:ident: $ty:ty),* $(,)?) => $kind:ident, $data:expr;
1733 )*) => {
1734 $(
1735 $(#[$meta])*
1736 #[must_use]
1737 pub fn $name(seq: u64, time: Time, $($arg: $ty),*) -> Self {
1738 Self::new(seq, time, TraceEventKind::$kind, $data)
1739 }
1740 )*
1741 };
1742}
1743
1744macro_rules! worker_lifecycle_constructors {
1745 ($(
1746 $(#[$meta:meta])*
1747 $name:ident => $kind:ident;
1748 )*) => {
1749 $(
1750 $(#[$meta])*
1751 #[allow(clippy::too_many_arguments)]
1752 #[must_use]
1753 pub fn $name(
1754 seq: u64,
1755 time: Time,
1756 worker_id: impl Into<String>,
1757 job_id: u64,
1758 decision_seq: u64,
1759 replay_hash: u64,
1760 task: TaskId,
1761 region: RegionId,
1762 obligation: ObligationId,
1763 ) -> Self {
1764 Self::worker_lifecycle(
1765 seq,
1766 time,
1767 TraceEventKind::$kind,
1768 worker_id,
1769 job_id,
1770 decision_seq,
1771 replay_hash,
1772 task,
1773 region,
1774 obligation,
1775 )
1776 }
1777 )*
1778 };
1779}
1780
1781impl TraceEvent {
1782 #[must_use]
1784 #[inline]
1785 pub fn new(seq: u64, time: Time, kind: TraceEventKind, data: TraceData) -> Self {
1786 Self {
1787 version: TRACE_EVENT_SCHEMA_VERSION,
1788 seq,
1789 time,
1790 logical_time: None,
1791 kind,
1792 data,
1793 }
1794 }
1795
1796 #[inline]
1798 #[must_use]
1799 pub fn with_logical_time(mut self, logical_time: LogicalTime) -> Self {
1800 self.logical_time = Some(logical_time);
1801 self
1802 }
1803
1804 trace_event_constructors! {
1805 spawn(task: TaskId, region: RegionId) => Spawn, TraceData::Task { task, region };
1807 schedule(task: TaskId, region: RegionId) => Schedule, TraceData::Task { task, region };
1809 yield_task(task: TaskId, region: RegionId) => Yield, TraceData::Task { task, region };
1811 wake(task: TaskId, region: RegionId) => Wake, TraceData::Task { task, region };
1813 poll(task: TaskId, region: RegionId) => Poll, TraceData::Task { task, region };
1815 complete(task: TaskId, region: RegionId) => Complete, TraceData::Task { task, region };
1817 task_spawn_enqueued(task: TaskId, region: RegionId) => TaskSpawnEnqueued,
1819 TraceData::Task { task, region };
1820 task_admitted(task: TaskId, region: RegionId) => TaskAdmitted,
1823 TraceData::Task { task, region };
1824 cancel_request(task: TaskId, region: RegionId, reason: CancelReason) => CancelRequest,
1826 TraceData::Cancel { task, region, reason };
1827 }
1828
1829 #[allow(clippy::too_many_arguments)]
1832 #[must_use]
1833 pub fn budget_installed(
1834 seq: u64,
1835 time: Time,
1836 task: TaskId,
1837 region: RegionId,
1838 protocol: impl Into<String>,
1839 deadline_ns: Option<u64>,
1840 poll_quota: u64,
1841 cost_quota: Option<u64>,
1842 priority: u8,
1843 source: impl Into<String>,
1844 ) -> Self {
1845 Self::new(
1846 seq,
1847 time,
1848 TraceEventKind::BudgetInstalled,
1849 TraceData::Budget {
1850 task,
1851 region,
1852 protocol: protocol.into(),
1853 deadline_ns,
1854 poll_quota,
1855 cost_quota,
1856 priority,
1857 source: Some(source.into()),
1858 elapsed_ns: None,
1859 outcome: None,
1860 },
1861 )
1862 }
1863
1864 #[allow(clippy::too_many_arguments)]
1867 #[must_use]
1868 pub fn budget_consumed(
1869 seq: u64,
1870 time: Time,
1871 task: TaskId,
1872 region: RegionId,
1873 protocol: impl Into<String>,
1874 deadline_ns: Option<u64>,
1875 poll_quota: u64,
1876 cost_quota: Option<u64>,
1877 priority: u8,
1878 elapsed_ns: Option<u64>,
1879 outcome: impl Into<String>,
1880 ) -> Self {
1881 Self::new(
1882 seq,
1883 time,
1884 TraceEventKind::BudgetConsumed,
1885 TraceData::Budget {
1886 task,
1887 region,
1888 protocol: protocol.into(),
1889 deadline_ns,
1890 poll_quota,
1891 cost_quota,
1892 priority,
1893 source: None,
1894 elapsed_ns,
1895 outcome: Some(outcome.into()),
1896 },
1897 )
1898 }
1899
1900 #[allow(clippy::too_many_arguments)]
1901 fn worker_lifecycle(
1902 seq: u64,
1903 time: Time,
1904 kind: TraceEventKind,
1905 worker_id: impl Into<String>,
1906 job_id: u64,
1907 decision_seq: u64,
1908 replay_hash: u64,
1909 task: TaskId,
1910 region: RegionId,
1911 obligation: ObligationId,
1912 ) -> Self {
1913 Self::new(
1914 seq,
1915 time,
1916 kind,
1917 TraceData::Worker {
1918 worker_id: worker_id.into(),
1919 job_id,
1920 decision_seq,
1921 replay_hash,
1922 task,
1923 region,
1924 obligation,
1925 },
1926 )
1927 }
1928
1929 worker_lifecycle_constructors! {
1930 worker_cancel_requested => WorkerCancelRequested;
1932 worker_cancel_acknowledged => WorkerCancelAcknowledged;
1934 worker_drain_started => WorkerDrainStarted;
1936 worker_drain_completed => WorkerDrainCompleted;
1938 worker_finalize_completed => WorkerFinalizeCompleted;
1940 }
1941
1942 trace_event_constructors! {
1943 region_created(region: RegionId, parent: Option<RegionId>) => RegionCreated,
1945 TraceData::Region { region, parent };
1946 region_cancelled(region: RegionId, reason: CancelReason) => RegionCancelled,
1948 TraceData::RegionCancel { region, reason };
1949 time_advance(old: Time, new: Time) => TimeAdvance, TraceData::Time { old, new };
1951 timer_scheduled(timer_id: u64, deadline: Time) => TimerScheduled,
1953 TraceData::Timer { timer_id, deadline: Some(deadline) };
1954 timer_fired(timer_id: u64) => TimerFired, TraceData::Timer { timer_id, deadline: None };
1956 timer_cancelled(timer_id: u64) => TimerCancelled,
1958 TraceData::Timer { timer_id, deadline: None };
1959 io_requested(token: u64, interest: u8) => IoRequested,
1961 TraceData::IoRequested { token, interest };
1962 io_ready(token: u64, readiness: u8) => IoReady, TraceData::IoReady { token, readiness };
1964 io_result(token: u64, bytes: i64) => IoResult, TraceData::IoResult { token, bytes };
1966 io_error(token: u64, kind: u8) => IoError, TraceData::IoError { token, kind };
1968 rng_seed(seed: u64) => RngSeed, TraceData::RngSeed { seed };
1970 rng_value(value: u64) => RngValue, TraceData::RngValue { value };
1972 checkpoint(sequence: u64, active_tasks: u32, active_regions: u32) => Checkpoint,
1974 TraceData::Checkpoint { sequence, active_tasks, active_regions };
1975 obligation_reserve(
1977 obligation: ObligationId,
1978 task: TaskId,
1979 region: RegionId,
1980 kind: ObligationKind,
1981 ) => ObligationReserve,
1982 TraceData::Obligation {
1983 obligation,
1984 task,
1985 region,
1986 kind,
1987 state: ObligationState::Reserved,
1988 duration_ns: None,
1989 abort_reason: None,
1990 };
1991 obligation_commit(
1993 obligation: ObligationId,
1994 task: TaskId,
1995 region: RegionId,
1996 kind: ObligationKind,
1997 duration_ns: u64,
1998 ) => ObligationCommit,
1999 TraceData::Obligation {
2000 obligation,
2001 task,
2002 region,
2003 kind,
2004 state: ObligationState::Committed,
2005 duration_ns: Some(duration_ns),
2006 abort_reason: None,
2007 };
2008 #[allow(clippy::too_many_arguments)]
2010 obligation_abort(
2011 obligation: ObligationId,
2012 task: TaskId,
2013 region: RegionId,
2014 kind: ObligationKind,
2015 duration_ns: u64,
2016 reason: ObligationAbortReason,
2017 ) => ObligationAbort,
2018 TraceData::Obligation {
2019 obligation,
2020 task,
2021 region,
2022 kind,
2023 state: ObligationState::Aborted,
2024 duration_ns: Some(duration_ns),
2025 abort_reason: Some(reason),
2026 };
2027 obligation_leak(
2029 obligation: ObligationId,
2030 task: TaskId,
2031 region: RegionId,
2032 kind: ObligationKind,
2033 duration_ns: u64,
2034 ) => ObligationLeak,
2035 TraceData::Obligation {
2036 obligation,
2037 task,
2038 region,
2039 kind,
2040 state: ObligationState::Leaked,
2041 duration_ns: Some(duration_ns),
2042 abort_reason: None,
2043 };
2044 monitor_created(
2046 monitor_ref: u64,
2047 watcher: TaskId,
2048 watcher_region: RegionId,
2049 monitored: TaskId,
2050 ) => MonitorCreated,
2051 TraceData::Monitor { monitor_ref, watcher, watcher_region, monitored };
2052 monitor_dropped(
2054 monitor_ref: u64,
2055 watcher: TaskId,
2056 watcher_region: RegionId,
2057 monitored: TaskId,
2058 ) => MonitorDropped,
2059 TraceData::Monitor { monitor_ref, watcher, watcher_region, monitored };
2060 down_delivered(
2062 monitor_ref: u64,
2063 watcher: TaskId,
2064 monitored: TaskId,
2065 completion_vt: Time,
2066 reason: DownReason,
2067 ) => DownDelivered,
2068 TraceData::Down { monitor_ref, watcher, monitored, completion_vt, reason };
2069 link_created(
2071 link_ref: u64,
2072 task_a: TaskId,
2073 region_a: RegionId,
2074 task_b: TaskId,
2075 region_b: RegionId,
2076 ) => LinkCreated,
2077 TraceData::Link { link_ref, task_a, region_a, task_b, region_b };
2078 link_dropped(
2080 link_ref: u64,
2081 task_a: TaskId,
2082 region_a: RegionId,
2083 task_b: TaskId,
2084 region_b: RegionId,
2085 ) => LinkDropped,
2086 TraceData::Link { link_ref, task_a, region_a, task_b, region_b };
2087 exit_delivered(link_ref: u64, from: TaskId, to: TaskId, failure_vt: Time, reason: DownReason)
2089 => ExitDelivered, TraceData::Exit { link_ref, from, to, failure_vt, reason };
2090 user_trace(message: impl Into<String>) => UserTrace, TraceData::Message(message.into());
2092 }
2093}
2094
2095impl fmt::Display for TraceEvent {
2096 #[allow(clippy::too_many_lines)]
2097 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2098 write!(f, "[{:06}] {} {}", self.seq, self.time, self.kind)?;
2099 if let Some(ref lt) = self.logical_time {
2100 write!(f, " @{lt:?}")?;
2101 }
2102 match &self.data {
2103 TraceData::None => {}
2104 TraceData::Task { task, region } => write!(f, " {task} in {region}")?,
2105 TraceData::Budget {
2106 task,
2107 region,
2108 protocol,
2109 outcome,
2110 ..
2111 } => {
2112 write!(f, " {task} in {region} [{protocol}]")?;
2113 if let Some(outcome) = outcome {
2114 write!(f, " outcome={outcome}")?;
2115 }
2116 }
2117 TraceData::Region { region, parent } => {
2118 write!(f, " {region}")?;
2119 if let Some(p) = parent {
2120 write!(f, " (parent: {p})")?;
2121 }
2122 }
2123 TraceData::Obligation {
2124 obligation,
2125 task,
2126 region,
2127 kind,
2128 state,
2129 duration_ns,
2130 abort_reason,
2131 } => {
2132 write!(
2133 f,
2134 " {obligation} {kind:?} {state:?} holder={task} region={region}"
2135 )?;
2136 if let Some(duration) = duration_ns {
2137 write!(f, " duration={duration}ns")?;
2138 }
2139 if let Some(reason) = abort_reason {
2140 write!(f, " abort_reason={reason}")?;
2141 }
2142 }
2143 TraceData::Cancel {
2144 task,
2145 region,
2146 reason,
2147 } => write!(f, " {task} in {region} reason={reason}")?,
2148 TraceData::Worker {
2149 worker_id,
2150 job_id,
2151 decision_seq,
2152 replay_hash,
2153 task,
2154 region,
2155 obligation,
2156 } => write!(
2157 f,
2158 " worker={worker_id} job_id={job_id} {task} in {region} obligation={obligation} decision_seq={decision_seq} replay_hash={replay_hash}"
2159 )?,
2160 TraceData::RegionCancel { region, reason } => {
2161 write!(f, " {region} reason={reason}")?;
2162 }
2163 TraceData::Time { old, new } => write!(f, " {old} -> {new}")?,
2164 TraceData::Timer { timer_id, deadline } => {
2165 write!(f, " timer={timer_id}")?;
2166 if let Some(dl) = deadline {
2167 write!(f, " deadline={dl}")?;
2168 }
2169 }
2170 TraceData::IoRequested { token, interest } => {
2171 write!(f, " io_requested token={token} interest={interest}")?;
2172 }
2173 TraceData::IoReady { token, readiness } => {
2174 write!(f, " io_ready token={token} readiness={readiness}")?;
2175 }
2176 TraceData::IoResult { token, bytes } => {
2177 write!(f, " io_result token={token} bytes={bytes}")?;
2178 }
2179 TraceData::IoError { token, kind } => {
2180 write!(f, " io_error token={token} kind={kind}")?;
2181 }
2182 TraceData::RngSeed { seed } => write!(f, " rng_seed={seed}")?,
2183 TraceData::RngValue { value } => write!(f, " rng_value={value}")?,
2184 TraceData::Checkpoint {
2185 sequence,
2186 active_tasks,
2187 active_regions,
2188 } => write!(
2189 f,
2190 " checkpoint seq={sequence} tasks={active_tasks} regions={active_regions}"
2191 )?,
2192 TraceData::Futurelock {
2193 task,
2194 region,
2195 idle_steps,
2196 held,
2197 } => {
2198 write!(f, " futurelock: {task} in {region} idle={idle_steps}")?;
2199 write!(f, " held=[")?;
2200 for (i, (oid, kind)) in held.iter().enumerate() {
2201 if i > 0 {
2202 write!(f, ", ")?;
2203 }
2204 write!(f, "{oid}:{kind:?}")?;
2205 }
2206 write!(f, "]")?;
2207 }
2208 TraceData::Monitor {
2209 monitor_ref,
2210 watcher,
2211 watcher_region,
2212 monitored,
2213 } => write!(
2214 f,
2215 " monitor_ref={monitor_ref} watcher={watcher} watcher_region={watcher_region} monitored={monitored}"
2216 )?,
2217 TraceData::Down {
2218 monitor_ref,
2219 watcher,
2220 monitored,
2221 completion_vt,
2222 reason,
2223 } => write!(
2224 f,
2225 " down monitor_ref={monitor_ref} watcher={watcher} monitored={monitored} completion_vt={completion_vt} reason={reason}"
2226 )?,
2227 TraceData::Link {
2228 link_ref,
2229 task_a,
2230 region_a,
2231 task_b,
2232 region_b,
2233 } => write!(
2234 f,
2235 " link_ref={link_ref} a={task_a} region_a={region_a} b={task_b} region_b={region_b}"
2236 )?,
2237 TraceData::Exit {
2238 link_ref,
2239 from,
2240 to,
2241 failure_vt,
2242 reason,
2243 } => write!(
2244 f,
2245 " exit link_ref={link_ref} from={from} to={to} failure_vt={failure_vt} reason={reason}"
2246 )?,
2247 TraceData::Message(msg) => write!(f, " \"{msg}\"")?,
2248 TraceData::Chaos { kind, task, detail } => {
2249 write!(f, " chaos:{kind}")?;
2250 if let Some(t) = task {
2251 write!(f, " task={t}")?;
2252 }
2253 write!(f, " {detail}")?;
2254 }
2255 }
2256 Ok(())
2257 }
2258}
2259
2260#[cfg(test)]
2261mod tests {
2262 #![allow(
2263 clippy::pedantic,
2264 clippy::nursery,
2265 clippy::expect_fun_call,
2266 clippy::map_unwrap_or,
2267 clippy::cast_possible_wrap,
2268 clippy::future_not_send
2269 )]
2270 use super::*;
2271 use crate::monitor::DownReason;
2272 use crate::record::{ObligationAbortReason, ObligationKind, ObligationState};
2273 use crate::trace::distributed::LamportTime;
2274 use crate::types::CancelReason;
2275 use serde_json::Value;
2276 use std::collections::BTreeSet;
2277
2278 fn task(n: u32) -> TaskId {
2279 TaskId::new_for_test(n, 1)
2280 }
2281 fn region(n: u32) -> RegionId {
2282 RegionId::new_for_test(n, 1)
2283 }
2284 fn obligation(n: u32) -> ObligationId {
2285 ObligationId::new_for_test(n, 1)
2286 }
2287
2288 fn scrub_browser_trace_fields(fields: &std::collections::BTreeMap<String, String>) -> Value {
2289 let mut value = serde_json::to_value(fields).expect("serialize browser trace fields");
2290 let obj = value
2291 .as_object_mut()
2292 .expect("browser trace fields serialize to an object");
2293
2294 for key in [
2295 "capture_host_time_ns",
2296 "capture_replay_key",
2297 "completion_vt",
2298 "deadline",
2299 "failure_vt",
2300 "from",
2301 "monitored",
2302 "new",
2303 "old",
2304 "parent",
2305 "region_a",
2306 "region_b",
2307 "seq",
2308 "task_a",
2309 "task_b",
2310 "to",
2311 "time_ns",
2312 "trace_id",
2313 "task",
2314 "region",
2315 "obligation",
2316 "sequence_group",
2317 "watcher",
2318 "watcher_region",
2319 ] {
2320 if obj.contains_key(key) {
2321 obj.insert(key.to_string(), Value::String(format!("[{key}]")));
2322 }
2323 }
2324
2325 value
2326 }
2327
2328 #[test]
2331 fn trace_event_version_is_set() {
2332 let event = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
2333 assert_eq!(event.version, TRACE_EVENT_SCHEMA_VERSION);
2334 }
2335
2336 #[test]
2337 fn trace_event_kind_stable_names_are_unique() {
2338 let mut names = BTreeSet::new();
2339 for kind in TraceEventKind::ALL {
2340 assert!(names.insert(kind.stable_name()));
2341 }
2342 }
2343
2344 #[test]
2345 fn trace_event_taxonomy_is_documented() {
2346 const DOC: &str = include_str!("../../docs/spork_deterministic_ordering.md");
2347 for kind in TraceEventKind::ALL {
2348 let marker = format!("- `{}` => `{}`", kind.stable_name(), kind.required_fields());
2349 assert!(
2350 DOC.contains(&marker),
2351 "missing taxonomy entry in docs/spork_deterministic_ordering.md for {}",
2352 kind.stable_name()
2353 );
2354 }
2355 }
2356
2357 #[test]
2358 fn all_array_has_42_kinds() {
2359 assert_eq!(TraceEventKind::ALL.len(), 45);
2360 }
2361
2362 #[test]
2363 fn all_kinds_are_distinct() {
2364 let set: BTreeSet<TraceEventKind> = TraceEventKind::ALL.iter().copied().collect();
2365 assert_eq!(set.len(), TraceEventKind::ALL.len());
2366 }
2367
2368 #[test]
2369 fn display_delegates_to_stable_name() {
2370 for kind in TraceEventKind::ALL {
2371 assert_eq!(format!("{kind}"), kind.stable_name());
2372 }
2373 }
2374
2375 #[test]
2376 fn kind_ord_is_consistent_with_eq() {
2377 for a in TraceEventKind::ALL {
2378 for b in TraceEventKind::ALL {
2379 if a == b {
2380 assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
2381 } else {
2382 assert_ne!(a.cmp(&b), std::cmp::Ordering::Equal);
2383 }
2384 }
2385 }
2386 }
2387
2388 #[test]
2389 fn required_fields_non_empty_for_all() {
2390 for kind in TraceEventKind::ALL {
2391 assert!(
2392 !kind.required_fields().is_empty(),
2393 "required_fields empty for {kind:?}"
2394 );
2395 }
2396 }
2397
2398 #[test]
2401 fn spawn_constructor() {
2402 let e = TraceEvent::spawn(1, Time::ZERO, task(10), region(20));
2403 assert_eq!(e.kind, TraceEventKind::Spawn);
2404 assert_eq!(e.seq, 1);
2405 assert_eq!(
2406 e.data,
2407 TraceData::Task {
2408 task: task(10),
2409 region: region(20)
2410 }
2411 );
2412 }
2413
2414 #[test]
2415 fn schedule_constructor() {
2416 let e = TraceEvent::schedule(2, Time::from_nanos(100), task(1), region(2));
2417 assert_eq!(e.kind, TraceEventKind::Schedule);
2418 assert_eq!(
2419 e.data,
2420 TraceData::Task {
2421 task: task(1),
2422 region: region(2)
2423 }
2424 );
2425 }
2426
2427 #[test]
2428 fn yield_task_constructor() {
2429 let e = TraceEvent::yield_task(3, Time::ZERO, task(5), region(6));
2430 assert_eq!(e.kind, TraceEventKind::Yield);
2431 assert_eq!(
2432 e.data,
2433 TraceData::Task {
2434 task: task(5),
2435 region: region(6)
2436 }
2437 );
2438 }
2439
2440 #[test]
2441 fn wake_constructor() {
2442 let e = TraceEvent::wake(4, Time::ZERO, task(7), region(8));
2443 assert_eq!(e.kind, TraceEventKind::Wake);
2444 assert_eq!(
2445 e.data,
2446 TraceData::Task {
2447 task: task(7),
2448 region: region(8)
2449 }
2450 );
2451 }
2452
2453 #[test]
2454 fn poll_constructor() {
2455 let e = TraceEvent::poll(5, Time::ZERO, task(9), region(10));
2456 assert_eq!(e.kind, TraceEventKind::Poll);
2457 assert_eq!(
2458 e.data,
2459 TraceData::Task {
2460 task: task(9),
2461 region: region(10)
2462 }
2463 );
2464 }
2465
2466 #[test]
2467 fn complete_constructor() {
2468 let e = TraceEvent::complete(6, Time::ZERO, task(11), region(12));
2469 assert_eq!(e.kind, TraceEventKind::Complete);
2470 assert_eq!(
2471 e.data,
2472 TraceData::Task {
2473 task: task(11),
2474 region: region(12)
2475 }
2476 );
2477 }
2478
2479 #[test]
2480 fn cancel_request_constructor() {
2481 let e =
2482 TraceEvent::cancel_request(7, Time::ZERO, task(1), region(2), CancelReason::timeout());
2483 assert_eq!(e.kind, TraceEventKind::CancelRequest);
2484 match &e.data {
2485 TraceData::Cancel {
2486 task: t,
2487 region: r,
2488 reason,
2489 } => {
2490 assert_eq!(*t, task(1));
2491 assert_eq!(*r, region(2));
2492 assert_eq!(reason.kind(), crate::types::CancelKind::Timeout);
2493 }
2494 other => panic!("expected Cancel, got {other:?}"),
2495 }
2496 }
2497
2498 #[test]
2499 fn region_created_constructor_with_parent() {
2500 let e = TraceEvent::region_created(8, Time::ZERO, region(3), Some(region(1)));
2501 assert_eq!(e.kind, TraceEventKind::RegionCreated);
2502 assert_eq!(
2503 e.data,
2504 TraceData::Region {
2505 region: region(3),
2506 parent: Some(region(1))
2507 }
2508 );
2509 }
2510
2511 #[test]
2512 fn region_created_constructor_without_parent() {
2513 let e = TraceEvent::region_created(9, Time::ZERO, region(3), None);
2514 assert_eq!(e.kind, TraceEventKind::RegionCreated);
2515 assert_eq!(
2516 e.data,
2517 TraceData::Region {
2518 region: region(3),
2519 parent: None
2520 }
2521 );
2522 }
2523
2524 #[test]
2525 fn region_cancelled_constructor() {
2526 let e = TraceEvent::region_cancelled(10, Time::ZERO, region(5), CancelReason::shutdown());
2527 assert_eq!(e.kind, TraceEventKind::RegionCancelled);
2528 match &e.data {
2529 TraceData::RegionCancel { region: r, .. } => assert_eq!(*r, region(5)),
2530 other => panic!("expected RegionCancel, got {other:?}"),
2531 }
2532 }
2533
2534 #[test]
2535 fn time_advance_constructor() {
2536 let e =
2537 TraceEvent::time_advance(11, Time::ZERO, Time::from_nanos(0), Time::from_nanos(100));
2538 assert_eq!(e.kind, TraceEventKind::TimeAdvance);
2539 assert_eq!(
2540 e.data,
2541 TraceData::Time {
2542 old: Time::from_nanos(0),
2543 new: Time::from_nanos(100)
2544 }
2545 );
2546 }
2547
2548 #[test]
2549 fn timer_scheduled_constructor() {
2550 let e = TraceEvent::timer_scheduled(12, Time::ZERO, 42, Time::from_millis(500));
2551 assert_eq!(e.kind, TraceEventKind::TimerScheduled);
2552 assert_eq!(
2553 e.data,
2554 TraceData::Timer {
2555 timer_id: 42,
2556 deadline: Some(Time::from_millis(500))
2557 }
2558 );
2559 }
2560
2561 #[test]
2562 fn timer_fired_constructor() {
2563 let e = TraceEvent::timer_fired(13, Time::ZERO, 42);
2564 assert_eq!(e.kind, TraceEventKind::TimerFired);
2565 assert_eq!(
2566 e.data,
2567 TraceData::Timer {
2568 timer_id: 42,
2569 deadline: None
2570 }
2571 );
2572 }
2573
2574 #[test]
2575 fn timer_cancelled_constructor() {
2576 let e = TraceEvent::timer_cancelled(14, Time::ZERO, 42);
2577 assert_eq!(e.kind, TraceEventKind::TimerCancelled);
2578 assert_eq!(
2579 e.data,
2580 TraceData::Timer {
2581 timer_id: 42,
2582 deadline: None
2583 }
2584 );
2585 }
2586
2587 #[test]
2588 fn io_requested_constructor() {
2589 let e = TraceEvent::io_requested(15, Time::ZERO, 99, 0x03);
2590 assert_eq!(e.kind, TraceEventKind::IoRequested);
2591 assert_eq!(
2592 e.data,
2593 TraceData::IoRequested {
2594 token: 99,
2595 interest: 0x03
2596 }
2597 );
2598 }
2599
2600 #[test]
2601 fn io_ready_constructor() {
2602 let e = TraceEvent::io_ready(16, Time::ZERO, 99, 0x01);
2603 assert_eq!(e.kind, TraceEventKind::IoReady);
2604 assert_eq!(
2605 e.data,
2606 TraceData::IoReady {
2607 token: 99,
2608 readiness: 0x01
2609 }
2610 );
2611 }
2612
2613 #[test]
2614 fn io_result_constructor() {
2615 let e = TraceEvent::io_result(17, Time::ZERO, 99, 1024);
2616 assert_eq!(e.kind, TraceEventKind::IoResult);
2617 assert_eq!(
2618 e.data,
2619 TraceData::IoResult {
2620 token: 99,
2621 bytes: 1024
2622 }
2623 );
2624 }
2625
2626 #[test]
2627 fn io_result_negative_bytes() {
2628 let e = TraceEvent::io_result(18, Time::ZERO, 99, -1);
2629 assert_eq!(
2630 e.data,
2631 TraceData::IoResult {
2632 token: 99,
2633 bytes: -1
2634 }
2635 );
2636 }
2637
2638 #[test]
2639 fn io_error_constructor() {
2640 let e = TraceEvent::io_error(19, Time::ZERO, 99, 13);
2641 assert_eq!(e.kind, TraceEventKind::IoError);
2642 assert_eq!(
2643 e.data,
2644 TraceData::IoError {
2645 token: 99,
2646 kind: 13
2647 }
2648 );
2649 }
2650
2651 #[test]
2652 fn rng_seed_constructor() {
2653 let e = TraceEvent::rng_seed(20, Time::ZERO, 0xDEAD_BEEF);
2654 assert_eq!(e.kind, TraceEventKind::RngSeed);
2655 assert_eq!(e.data, TraceData::RngSeed { seed: 0xDEAD_BEEF });
2656 }
2657
2658 #[test]
2659 fn rng_value_constructor() {
2660 let e = TraceEvent::rng_value(21, Time::ZERO, 42);
2661 assert_eq!(e.kind, TraceEventKind::RngValue);
2662 assert_eq!(e.data, TraceData::RngValue { value: 42 });
2663 }
2664
2665 #[test]
2666 fn checkpoint_constructor() {
2667 let e = TraceEvent::checkpoint(22, Time::ZERO, 7, 3, 2);
2668 assert_eq!(e.kind, TraceEventKind::Checkpoint);
2669 assert_eq!(
2670 e.data,
2671 TraceData::Checkpoint {
2672 sequence: 7,
2673 active_tasks: 3,
2674 active_regions: 2
2675 }
2676 );
2677 }
2678
2679 #[test]
2680 fn obligation_reserve_constructor() {
2681 let e = TraceEvent::obligation_reserve(
2682 23,
2683 Time::ZERO,
2684 obligation(1),
2685 task(2),
2686 region(3),
2687 ObligationKind::SendPermit,
2688 );
2689 assert_eq!(e.kind, TraceEventKind::ObligationReserve);
2690 match &e.data {
2691 TraceData::Obligation {
2692 state,
2693 duration_ns,
2694 abort_reason,
2695 ..
2696 } => {
2697 assert_eq!(*state, ObligationState::Reserved);
2698 assert_eq!(*duration_ns, None);
2699 assert_eq!(*abort_reason, None);
2700 }
2701 other => panic!("expected Obligation, got {other:?}"),
2702 }
2703 }
2704
2705 #[test]
2706 fn obligation_commit_constructor() {
2707 let e = TraceEvent::obligation_commit(
2708 24,
2709 Time::ZERO,
2710 obligation(1),
2711 task(2),
2712 region(3),
2713 ObligationKind::Ack,
2714 5000,
2715 );
2716 assert_eq!(e.kind, TraceEventKind::ObligationCommit);
2717 match &e.data {
2718 TraceData::Obligation {
2719 state,
2720 duration_ns,
2721 abort_reason,
2722 ..
2723 } => {
2724 assert_eq!(*state, ObligationState::Committed);
2725 assert_eq!(*duration_ns, Some(5000));
2726 assert_eq!(*abort_reason, None);
2727 }
2728 other => panic!("expected Obligation, got {other:?}"),
2729 }
2730 }
2731
2732 #[test]
2733 fn obligation_abort_constructor() {
2734 let e = TraceEvent::obligation_abort(
2735 25,
2736 Time::ZERO,
2737 obligation(1),
2738 task(2),
2739 region(3),
2740 ObligationKind::Lease,
2741 3000,
2742 ObligationAbortReason::Cancel,
2743 );
2744 assert_eq!(e.kind, TraceEventKind::ObligationAbort);
2745 match &e.data {
2746 TraceData::Obligation {
2747 state,
2748 duration_ns,
2749 abort_reason,
2750 ..
2751 } => {
2752 assert_eq!(*state, ObligationState::Aborted);
2753 assert_eq!(*duration_ns, Some(3000));
2754 assert_eq!(*abort_reason, Some(ObligationAbortReason::Cancel));
2755 }
2756 other => panic!("expected Obligation, got {other:?}"),
2757 }
2758 }
2759
2760 #[test]
2761 fn obligation_leak_constructor() {
2762 let e = TraceEvent::obligation_leak(
2763 26,
2764 Time::ZERO,
2765 obligation(1),
2766 task(2),
2767 region(3),
2768 ObligationKind::IoOp,
2769 9000,
2770 );
2771 assert_eq!(e.kind, TraceEventKind::ObligationLeak);
2772 match &e.data {
2773 TraceData::Obligation {
2774 state,
2775 duration_ns,
2776 abort_reason,
2777 ..
2778 } => {
2779 assert_eq!(*state, ObligationState::Leaked);
2780 assert_eq!(*duration_ns, Some(9000));
2781 assert_eq!(*abort_reason, None);
2782 }
2783 other => panic!("expected Obligation, got {other:?}"),
2784 }
2785 }
2786
2787 #[test]
2788 fn monitor_created_constructor() {
2789 let e = TraceEvent::monitor_created(27, Time::ZERO, 100, task(1), region(2), task(3));
2790 assert_eq!(e.kind, TraceEventKind::MonitorCreated);
2791 assert_eq!(
2792 e.data,
2793 TraceData::Monitor {
2794 monitor_ref: 100,
2795 watcher: task(1),
2796 watcher_region: region(2),
2797 monitored: task(3),
2798 }
2799 );
2800 }
2801
2802 #[test]
2803 fn monitor_dropped_constructor() {
2804 let e = TraceEvent::monitor_dropped(28, Time::ZERO, 100, task(1), region(2), task(3));
2805 assert_eq!(e.kind, TraceEventKind::MonitorDropped);
2806 assert_eq!(
2807 e.data,
2808 TraceData::Monitor {
2809 monitor_ref: 100,
2810 watcher: task(1),
2811 watcher_region: region(2),
2812 monitored: task(3),
2813 }
2814 );
2815 }
2816
2817 #[test]
2818 fn down_delivered_constructor() {
2819 let e = TraceEvent::down_delivered(
2820 29,
2821 Time::ZERO,
2822 100,
2823 task(1),
2824 task(3),
2825 Time::from_nanos(500),
2826 DownReason::Normal,
2827 );
2828 assert_eq!(e.kind, TraceEventKind::DownDelivered);
2829 assert_eq!(
2830 e.data,
2831 TraceData::Down {
2832 monitor_ref: 100,
2833 watcher: task(1),
2834 monitored: task(3),
2835 completion_vt: Time::from_nanos(500),
2836 reason: DownReason::Normal,
2837 }
2838 );
2839 }
2840
2841 #[test]
2842 fn link_created_constructor() {
2843 let e =
2844 TraceEvent::link_created(30, Time::ZERO, 200, task(1), region(2), task(3), region(4));
2845 assert_eq!(e.kind, TraceEventKind::LinkCreated);
2846 assert_eq!(
2847 e.data,
2848 TraceData::Link {
2849 link_ref: 200,
2850 task_a: task(1),
2851 region_a: region(2),
2852 task_b: task(3),
2853 region_b: region(4),
2854 }
2855 );
2856 }
2857
2858 #[test]
2859 fn link_dropped_constructor() {
2860 let e =
2861 TraceEvent::link_dropped(31, Time::ZERO, 200, task(1), region(2), task(3), region(4));
2862 assert_eq!(e.kind, TraceEventKind::LinkDropped);
2863 assert_eq!(
2864 e.data,
2865 TraceData::Link {
2866 link_ref: 200,
2867 task_a: task(1),
2868 region_a: region(2),
2869 task_b: task(3),
2870 region_b: region(4),
2871 }
2872 );
2873 }
2874
2875 #[test]
2876 fn exit_delivered_constructor() {
2877 let e = TraceEvent::exit_delivered(
2878 32,
2879 Time::ZERO,
2880 200,
2881 task(1),
2882 task(3),
2883 Time::from_nanos(999),
2884 DownReason::Normal,
2885 );
2886 assert_eq!(e.kind, TraceEventKind::ExitDelivered);
2887 assert_eq!(
2888 e.data,
2889 TraceData::Exit {
2890 link_ref: 200,
2891 from: task(1),
2892 to: task(3),
2893 failure_vt: Time::from_nanos(999),
2894 reason: DownReason::Normal,
2895 }
2896 );
2897 }
2898
2899 #[test]
2900 fn user_trace_constructor() {
2901 let e = TraceEvent::user_trace(33, Time::ZERO, "hello");
2902 assert_eq!(e.kind, TraceEventKind::UserTrace);
2903 assert_eq!(e.data, TraceData::Message("hello".into()));
2904 }
2905
2906 #[test]
2907 fn user_trace_accepts_string() {
2908 let e = TraceEvent::user_trace(34, Time::ZERO, String::from("world"));
2909 assert_eq!(e.data, TraceData::Message("world".into()));
2910 }
2911
2912 #[test]
2913 fn worker_lifecycle_constructors_preserve_payload_shape() {
2914 let e = TraceEvent::worker_cancel_requested(
2915 35,
2916 Time::ZERO,
2917 "worker-a",
2918 77,
2919 91,
2920 0x00C0_FFEE,
2921 task(9),
2922 region(10),
2923 obligation(11),
2924 );
2925 assert_eq!(e.kind, TraceEventKind::WorkerCancelRequested);
2926 assert_eq!(
2927 e.data,
2928 TraceData::Worker {
2929 worker_id: "worker-a".into(),
2930 job_id: 77,
2931 decision_seq: 91,
2932 replay_hash: 0x00C0_FFEE,
2933 task: task(9),
2934 region: region(10),
2935 obligation: obligation(11),
2936 }
2937 );
2938 }
2939
2940 #[test]
2943 fn with_logical_time_sets_field() {
2944 let lt = LogicalTime::Lamport(LamportTime::from_raw(42));
2945 let e = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None)
2946 .with_logical_time(lt);
2947 assert_eq!(
2948 e.logical_time,
2949 Some(LogicalTime::Lamport(LamportTime::from_raw(42)))
2950 );
2951 }
2952
2953 #[test]
2954 fn default_logical_time_is_none() {
2955 let e = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
2956 assert_eq!(e.logical_time, None);
2957 }
2958
2959 #[test]
2962 fn display_task_event() {
2963 let e = TraceEvent::spawn(1, Time::ZERO, task(10), region(20));
2964 let s = format!("{e}");
2965 assert!(s.contains("spawn"), "expected 'spawn' in {s}");
2966 assert!(s.contains("[000001]"), "expected seq in {s}");
2967 }
2968
2969 #[test]
2970 fn display_region_with_parent() {
2971 let e = TraceEvent::region_created(2, Time::ZERO, region(3), Some(region(1)));
2972 let s = format!("{e}");
2973 assert!(s.contains("region_created"), "expected kind in {s}");
2974 assert!(s.contains("parent"), "expected parent in {s}");
2975 }
2976
2977 #[test]
2978 fn display_region_without_parent() {
2979 let e = TraceEvent::region_created(3, Time::ZERO, region(3), None);
2980 let s = format!("{e}");
2981 assert!(s.contains("region_created"), "expected kind in {s}");
2982 assert!(!s.contains("parent"), "should not contain parent: {s}");
2983 }
2984
2985 #[test]
2986 fn display_obligation_with_duration_and_abort() {
2987 let e = TraceEvent::obligation_abort(
2988 4,
2989 Time::ZERO,
2990 obligation(1),
2991 task(2),
2992 region(3),
2993 ObligationKind::Lease,
2994 5000,
2995 ObligationAbortReason::Error,
2996 );
2997 let s = format!("{e}");
2998 assert!(s.contains("obligation_abort"), "expected kind in {s}");
2999 assert!(s.contains("duration=5000ns"), "expected duration in {s}");
3000 assert!(s.contains("abort_reason="), "expected abort_reason in {s}");
3001 }
3002
3003 #[test]
3004 fn display_obligation_reserve_no_duration() {
3005 let e = TraceEvent::obligation_reserve(
3006 5,
3007 Time::ZERO,
3008 obligation(1),
3009 task(2),
3010 region(3),
3011 ObligationKind::SendPermit,
3012 );
3013 let s = format!("{e}");
3014 assert!(
3015 !s.contains("duration="),
3016 "reserve should not show duration: {s}"
3017 );
3018 assert!(
3019 !s.contains("abort_reason="),
3020 "reserve should not show abort_reason: {s}"
3021 );
3022 }
3023
3024 #[test]
3025 fn display_cancel_event() {
3026 let e =
3027 TraceEvent::cancel_request(6, Time::ZERO, task(1), region(2), CancelReason::timeout());
3028 let s = format!("{e}");
3029 assert!(s.contains("cancel_request"), "expected kind in {s}");
3030 assert!(s.contains("reason="), "expected reason in {s}");
3031 }
3032
3033 #[test]
3034 fn display_region_cancel() {
3035 let e = TraceEvent::region_cancelled(7, Time::ZERO, region(5), CancelReason::shutdown());
3036 let s = format!("{e}");
3037 assert!(s.contains("region_cancelled"), "expected kind in {s}");
3038 assert!(s.contains("reason="), "expected reason in {s}");
3039 }
3040
3041 #[test]
3042 fn display_time_advance() {
3043 let e = TraceEvent::time_advance(8, Time::ZERO, Time::from_nanos(0), Time::from_nanos(100));
3044 let s = format!("{e}");
3045 assert!(s.contains("time_advance"), "expected kind in {s}");
3046 assert!(s.contains("->"), "expected arrow in {s}");
3047 }
3048
3049 #[test]
3050 fn display_timer_with_deadline() {
3051 let e = TraceEvent::timer_scheduled(9, Time::ZERO, 42, Time::from_millis(500));
3052 let s = format!("{e}");
3053 assert!(s.contains("timer=42"), "expected timer id in {s}");
3054 assert!(s.contains("deadline="), "expected deadline in {s}");
3055 }
3056
3057 #[test]
3058 fn display_timer_without_deadline() {
3059 let e = TraceEvent::timer_fired(10, Time::ZERO, 42);
3060 let s = format!("{e}");
3061 assert!(s.contains("timer=42"), "expected timer id in {s}");
3062 assert!(!s.contains("deadline="), "should not show deadline: {s}");
3063 }
3064
3065 #[test]
3066 fn display_io_requested() {
3067 let e = TraceEvent::io_requested(11, Time::ZERO, 99, 0x03);
3068 let s = format!("{e}");
3069 assert!(s.contains("io_requested"), "expected kind in {s}");
3070 assert!(s.contains("token=99"), "expected token in {s}");
3071 assert!(s.contains("interest=3"), "expected interest in {s}");
3072 }
3073
3074 #[test]
3075 fn display_io_ready() {
3076 let e = TraceEvent::io_ready(12, Time::ZERO, 99, 0x01);
3077 let s = format!("{e}");
3078 assert!(s.contains("io_ready"), "expected kind in {s}");
3079 assert!(s.contains("readiness=1"), "expected readiness in {s}");
3080 }
3081
3082 #[test]
3083 fn display_io_result() {
3084 let e = TraceEvent::io_result(13, Time::ZERO, 99, 1024);
3085 let s = format!("{e}");
3086 assert!(s.contains("io_result"), "expected kind in {s}");
3087 assert!(s.contains("bytes=1024"), "expected bytes in {s}");
3088 }
3089
3090 #[test]
3091 fn display_io_error() {
3092 let e = TraceEvent::io_error(14, Time::ZERO, 99, 13);
3093 let s = format!("{e}");
3094 assert!(s.contains("io_error"), "expected kind in {s}");
3095 assert!(s.contains("kind=13"), "expected kind in {s}");
3096 }
3097
3098 #[test]
3099 fn display_rng_seed() {
3100 let e = TraceEvent::rng_seed(15, Time::ZERO, 0xCAFE);
3101 let s = format!("{e}");
3102 assert!(s.contains("rng_seed=51966"), "expected seed in {s}");
3103 }
3104
3105 #[test]
3106 fn display_rng_value() {
3107 let e = TraceEvent::rng_value(16, Time::ZERO, 42);
3108 let s = format!("{e}");
3109 assert!(s.contains("rng_value=42"), "expected value in {s}");
3110 }
3111
3112 #[test]
3113 fn display_checkpoint() {
3114 let e = TraceEvent::checkpoint(17, Time::ZERO, 7, 3, 2);
3115 let s = format!("{e}");
3116 assert!(s.contains("checkpoint"), "expected kind in {s}");
3117 assert!(s.contains("seq=7"), "expected seq in {s}");
3118 assert!(s.contains("tasks=3"), "expected tasks in {s}");
3119 assert!(s.contains("regions=2"), "expected regions in {s}");
3120 }
3121
3122 #[test]
3123 fn display_futurelock_empty_held() {
3124 let e = TraceEvent::new(
3125 18,
3126 Time::ZERO,
3127 TraceEventKind::FuturelockDetected,
3128 TraceData::Futurelock {
3129 task: task(1),
3130 region: region(2),
3131 idle_steps: 10,
3132 held: vec![],
3133 },
3134 );
3135 let s = format!("{e}");
3136 assert!(s.contains("futurelock"), "expected kind in {s}");
3137 assert!(s.contains("idle=10"), "expected idle in {s}");
3138 assert!(s.contains("held=[]"), "expected empty held in {s}");
3139 }
3140
3141 #[test]
3142 fn display_futurelock_with_held() {
3143 let e = TraceEvent::new(
3144 19,
3145 Time::ZERO,
3146 TraceEventKind::FuturelockDetected,
3147 TraceData::Futurelock {
3148 task: task(1),
3149 region: region(2),
3150 idle_steps: 5,
3151 held: vec![(obligation(10), ObligationKind::SendPermit)],
3152 },
3153 );
3154 let s = format!("{e}");
3155 assert!(s.contains("held=["), "expected held in {s}");
3156 assert!(s.contains("SendPermit"), "expected kind in {s}");
3157 }
3158
3159 #[test]
3160 fn display_monitor() {
3161 let e = TraceEvent::monitor_created(20, Time::ZERO, 100, task(1), region(2), task(3));
3162 let s = format!("{e}");
3163 assert!(s.contains("monitor_ref=100"), "expected ref in {s}");
3164 }
3165
3166 #[test]
3167 fn display_down() {
3168 let e = TraceEvent::down_delivered(
3169 21,
3170 Time::ZERO,
3171 100,
3172 task(1),
3173 task(3),
3174 Time::from_nanos(500),
3175 DownReason::Normal,
3176 );
3177 let s = format!("{e}");
3178 assert!(s.contains("down"), "expected down in {s}");
3179 assert!(s.contains("monitor_ref=100"), "expected ref in {s}");
3180 }
3181
3182 #[test]
3183 fn display_link() {
3184 let e =
3185 TraceEvent::link_created(22, Time::ZERO, 200, task(1), region(2), task(3), region(4));
3186 let s = format!("{e}");
3187 assert!(s.contains("link_ref=200"), "expected ref in {s}");
3188 }
3189
3190 #[test]
3191 fn display_exit() {
3192 let e = TraceEvent::exit_delivered(
3193 23,
3194 Time::ZERO,
3195 200,
3196 task(1),
3197 task(3),
3198 Time::from_nanos(999),
3199 DownReason::Normal,
3200 );
3201 let s = format!("{e}");
3202 assert!(s.contains("exit"), "expected exit in {s}");
3203 assert!(s.contains("link_ref=200"), "expected ref in {s}");
3204 }
3205
3206 #[test]
3207 fn display_message() {
3208 let e = TraceEvent::user_trace(24, Time::ZERO, "hello world");
3209 let s = format!("{e}");
3210 assert!(s.contains("\"hello world\""), "expected msg in {s}");
3211 }
3212
3213 #[test]
3214 fn display_chaos_with_task() {
3215 let e = TraceEvent::new(
3216 25,
3217 Time::ZERO,
3218 TraceEventKind::ChaosInjection,
3219 TraceData::Chaos {
3220 kind: "delay".into(),
3221 task: Some(task(1)),
3222 detail: "200ns".into(),
3223 },
3224 );
3225 let s = format!("{e}");
3226 assert!(s.contains("chaos:delay"), "expected kind in {s}");
3227 assert!(s.contains("task="), "expected task in {s}");
3228 assert!(s.contains("200ns"), "expected detail in {s}");
3229 }
3230
3231 #[test]
3232 fn display_chaos_without_task() {
3233 let e = TraceEvent::new(
3234 26,
3235 Time::ZERO,
3236 TraceEventKind::ChaosInjection,
3237 TraceData::Chaos {
3238 kind: "budget_exhaust".into(),
3239 task: None,
3240 detail: "all".into(),
3241 },
3242 );
3243 let s = format!("{e}");
3244 assert!(s.contains("chaos:budget_exhaust"), "expected kind in {s}");
3245 assert!(!s.contains("task="), "should not show task: {s}");
3246 }
3247
3248 #[test]
3249 fn display_none_data() {
3250 let e = TraceEvent::new(27, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
3251 let s = format!("{e}");
3252 assert!(s.contains("user_trace"), "expected kind in {s}");
3254 }
3255
3256 #[test]
3257 fn display_with_logical_time() {
3258 let lt = LogicalTime::Lamport(LamportTime::from_raw(42));
3259 let e = TraceEvent::new(28, Time::ZERO, TraceEventKind::UserTrace, TraceData::None)
3260 .with_logical_time(lt);
3261 let s = format!("{e}");
3262 assert!(s.contains('@'), "expected @lt in {s}");
3263 }
3264
3265 #[test]
3268 fn events_equal_same_fields() {
3269 let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3270 let b = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3271 assert_eq!(a, b);
3272 }
3273
3274 #[test]
3275 fn events_differ_on_seq() {
3276 let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3277 let b = TraceEvent::spawn(2, Time::ZERO, task(1), region(2));
3278 assert_ne!(a, b);
3279 }
3280
3281 #[test]
3282 fn events_differ_on_kind() {
3283 let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3284 let b = TraceEvent::schedule(1, Time::ZERO, task(1), region(2));
3285 assert_ne!(a, b);
3286 }
3287
3288 #[test]
3289 fn events_differ_on_data() {
3290 let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3291 let b = TraceEvent::spawn(1, Time::ZERO, task(1), region(3));
3292 assert_ne!(a, b);
3293 }
3294
3295 #[test]
3296 fn trace_data_clone() {
3297 let data = TraceData::Task {
3298 task: task(1),
3299 region: region(2),
3300 };
3301 let cloned = data.clone();
3302 assert_eq!(data, cloned);
3303 }
3304
3305 #[test]
3306 fn trace_data_message_eq() {
3307 let a = TraceData::Message("hello".into());
3308 let b = TraceData::Message("hello".into());
3309 assert_eq!(a, b);
3310 }
3311
3312 #[test]
3313 fn trace_data_message_ne() {
3314 let a = TraceData::Message("hello".into());
3315 let b = TraceData::Message("world".into());
3316 assert_ne!(a, b);
3317 }
3318
3319 #[test]
3320 fn trace_data_none_variant() {
3321 assert_eq!(TraceData::None, TraceData::None);
3322 }
3323
3324 #[test]
3325 fn trace_event_clone() {
3326 let e = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3327 let c = e.clone();
3328 assert_eq!(e, c);
3329 }
3330
3331 #[test]
3334 fn obligation_reserve_all_kinds() {
3335 for kind in [
3336 ObligationKind::SendPermit,
3337 ObligationKind::Ack,
3338 ObligationKind::Lease,
3339 ObligationKind::IoOp,
3340 ] {
3341 let e = TraceEvent::obligation_reserve(
3342 1,
3343 Time::ZERO,
3344 obligation(1),
3345 task(2),
3346 region(3),
3347 kind,
3348 );
3349 match &e.data {
3350 TraceData::Obligation { kind: k, .. } => assert_eq!(*k, kind),
3351 _ => panic!("wrong variant"),
3352 }
3353 }
3354 }
3355
3356 #[test]
3357 fn obligation_abort_all_reasons() {
3358 for reason in [
3359 ObligationAbortReason::Cancel,
3360 ObligationAbortReason::Error,
3361 ObligationAbortReason::Explicit,
3362 ] {
3363 let e = TraceEvent::obligation_abort(
3364 1,
3365 Time::ZERO,
3366 obligation(1),
3367 task(2),
3368 region(3),
3369 ObligationKind::SendPermit,
3370 1000,
3371 reason,
3372 );
3373 match &e.data {
3374 TraceData::Obligation { abort_reason, .. } => {
3375 assert_eq!(*abort_reason, Some(reason));
3376 }
3377 _ => panic!("wrong variant"),
3378 }
3379 }
3380 }
3381
3382 #[test]
3385 fn down_delivered_with_error_reason() {
3386 let e = TraceEvent::down_delivered(
3387 1,
3388 Time::ZERO,
3389 50,
3390 task(1),
3391 task(2),
3392 Time::from_nanos(100),
3393 DownReason::Error("boom".into()),
3394 );
3395 match &e.data {
3396 TraceData::Down { reason, .. } => {
3397 assert_eq!(*reason, DownReason::Error("boom".into()));
3398 }
3399 _ => panic!("wrong variant"),
3400 }
3401 }
3402
3403 #[test]
3404 fn exit_delivered_with_cancelled_reason() {
3405 let e = TraceEvent::exit_delivered(
3406 1,
3407 Time::ZERO,
3408 50,
3409 task(1),
3410 task(2),
3411 Time::from_nanos(100),
3412 DownReason::Cancelled(CancelReason::timeout()),
3413 );
3414 match &e.data {
3415 TraceData::Exit { reason, .. } => {
3416 assert!(matches!(reason, DownReason::Cancelled(_)));
3417 }
3418 _ => panic!("wrong variant"),
3419 }
3420 }
3421
3422 #[test]
3425 fn seq_zero() {
3426 let e = TraceEvent::new(0, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
3427 assert_eq!(e.seq, 0);
3428 }
3429
3430 #[test]
3431 fn seq_max() {
3432 let e = TraceEvent::new(
3433 u64::MAX,
3434 Time::ZERO,
3435 TraceEventKind::UserTrace,
3436 TraceData::None,
3437 );
3438 assert_eq!(e.seq, u64::MAX);
3439 }
3440
3441 #[test]
3442 fn time_max() {
3443 let e = TraceEvent::new(1, Time::MAX, TraceEventKind::UserTrace, TraceData::None);
3444 assert_eq!(e.time, Time::MAX);
3445 }
3446
3447 #[test]
3448 fn io_result_zero_bytes() {
3449 let e = TraceEvent::io_result(1, Time::ZERO, 0, 0);
3450 assert_eq!(e.data, TraceData::IoResult { token: 0, bytes: 0 });
3451 }
3452
3453 #[test]
3454 fn checkpoint_zero_counts() {
3455 let e = TraceEvent::checkpoint(1, Time::ZERO, 0, 0, 0);
3456 assert_eq!(
3457 e.data,
3458 TraceData::Checkpoint {
3459 sequence: 0,
3460 active_tasks: 0,
3461 active_regions: 0
3462 }
3463 );
3464 }
3465
3466 #[test]
3467 fn futurelock_many_held() {
3468 let held: Vec<_> = (0..100)
3469 .map(|i| (obligation(i), ObligationKind::SendPermit))
3470 .collect();
3471 let e = TraceEvent::new(
3472 1,
3473 Time::ZERO,
3474 TraceEventKind::FuturelockDetected,
3475 TraceData::Futurelock {
3476 task: task(1),
3477 region: region(2),
3478 idle_steps: 1000,
3479 held,
3480 },
3481 );
3482 let s = format!("{e}");
3483 assert!(s.matches("SendPermit").count() == 100);
3485 }
3486
3487 #[test]
3490 fn trace_event_kind_debug_clone_copy_eq_ord_hash() {
3491 use std::collections::HashSet;
3492 let k = TraceEventKind::Spawn;
3493 let k2 = k; let k3 = k;
3495 assert_eq!(k, k2);
3496 assert_eq!(k, k3);
3497 assert_ne!(k, TraceEventKind::Complete);
3498 assert!(k < TraceEventKind::Complete);
3499 let dbg = format!("{k:?}");
3500 assert!(dbg.contains("Spawn"));
3501 let mut set = HashSet::new();
3502 set.insert(k);
3503 assert!(set.contains(&k2));
3504 }
3505
3506 #[test]
3507 fn trace_data_debug_clone_eq() {
3508 let d = TraceData::None;
3509 let d2 = d.clone();
3510 assert_eq!(d, d2);
3511 assert_ne!(d, TraceData::Message("hi".into()));
3512 let dbg = format!("{d:?}");
3513 assert!(dbg.contains("None"));
3514 }
3515
3516 #[test]
3517 fn trace_event_debug_clone_eq() {
3518 let e = TraceEvent::new(
3519 0,
3520 Time::from_nanos(100),
3521 TraceEventKind::UserTrace,
3522 TraceData::Message("hello".into()),
3523 );
3524 let e2 = e.clone();
3525 assert_eq!(e, e2);
3526 let dbg = format!("{e:?}");
3527 assert!(dbg.contains("TraceEvent"));
3528 }
3529
3530 #[test]
3531 fn browser_trace_schema_v1_validates() {
3532 let schema = browser_trace_schema_v1();
3533 validate_browser_trace_schema(&schema).expect("browser schema should validate");
3534 }
3535
3536 #[test]
3537 fn browser_trace_schema_round_trip_json() {
3538 let schema = browser_trace_schema_v1();
3539 let payload = serde_json::to_string(&schema).expect("serialize schema");
3540 let decoded = decode_browser_trace_schema(&payload).expect("decode schema");
3541 assert_eq!(schema, decoded);
3542 }
3543
3544 #[test]
3545 fn browser_trace_schema_timer_required_fields_match_payload_shape() {
3546 let schema = browser_trace_schema_v1();
3547 let scheduled = schema
3548 .event_specs
3549 .iter()
3550 .find(|entry| entry.event_kind == "timer_scheduled")
3551 .expect("timer_scheduled entry should exist");
3552 let fired = schema
3553 .event_specs
3554 .iter()
3555 .find(|entry| entry.event_kind == "timer_fired")
3556 .expect("timer_fired entry should exist");
3557 let cancelled = schema
3558 .event_specs
3559 .iter()
3560 .find(|entry| entry.event_kind == "timer_cancelled")
3561 .expect("timer_cancelled entry should exist");
3562
3563 assert_eq!(
3564 scheduled.required_fields,
3565 vec!["deadline".to_string(), "timer_id".to_string()]
3566 );
3567 assert_eq!(fired.required_fields, vec!["timer_id".to_string()]);
3568 assert_eq!(cancelled.required_fields, vec!["timer_id".to_string()]);
3569 }
3570
3571 #[test]
3572 fn browser_trace_schema_obligation_required_fields_match_payload_shape() {
3573 let schema = browser_trace_schema_v1();
3574 let reserve = schema
3575 .event_specs
3576 .iter()
3577 .find(|entry| entry.event_kind == "obligation_reserve")
3578 .expect("obligation_reserve entry should exist");
3579 let commit = schema
3580 .event_specs
3581 .iter()
3582 .find(|entry| entry.event_kind == "obligation_commit")
3583 .expect("obligation_commit entry should exist");
3584 let abort = schema
3585 .event_specs
3586 .iter()
3587 .find(|entry| entry.event_kind == "obligation_abort")
3588 .expect("obligation_abort entry should exist");
3589 let leak = schema
3590 .event_specs
3591 .iter()
3592 .find(|entry| entry.event_kind == "obligation_leak")
3593 .expect("obligation_leak entry should exist");
3594
3595 assert_eq!(
3596 reserve.required_fields,
3597 vec![
3598 "kind".to_string(),
3599 "obligation".to_string(),
3600 "region".to_string(),
3601 "state".to_string(),
3602 "task".to_string(),
3603 ]
3604 );
3605 assert_eq!(
3606 commit.required_fields,
3607 vec![
3608 "duration_ns".to_string(),
3609 "kind".to_string(),
3610 "obligation".to_string(),
3611 "region".to_string(),
3612 "state".to_string(),
3613 "task".to_string(),
3614 ]
3615 );
3616 assert_eq!(
3617 abort.required_fields,
3618 vec![
3619 "abort_reason".to_string(),
3620 "duration_ns".to_string(),
3621 "kind".to_string(),
3622 "obligation".to_string(),
3623 "region".to_string(),
3624 "state".to_string(),
3625 "task".to_string(),
3626 ]
3627 );
3628 assert_eq!(
3629 leak.required_fields,
3630 vec![
3631 "duration_ns".to_string(),
3632 "kind".to_string(),
3633 "obligation".to_string(),
3634 "region".to_string(),
3635 "state".to_string(),
3636 "task".to_string(),
3637 ]
3638 );
3639 }
3640
3641 #[test]
3642 fn browser_trace_schema_worker_required_fields_match_payload_shape() {
3643 let schema = browser_trace_schema_v1();
3644 for event_kind in [
3645 "worker_cancel_requested",
3646 "worker_cancel_acknowledged",
3647 "worker_drain_started",
3648 "worker_drain_completed",
3649 "worker_finalize_completed",
3650 ] {
3651 let entry = schema
3652 .event_specs
3653 .iter()
3654 .find(|entry| entry.event_kind == event_kind)
3655 .unwrap_or_else(|| panic!("{event_kind} entry should exist"));
3656 assert_eq!(entry.category, BrowserTraceCategory::CancellationTransition);
3657 assert_eq!(
3658 entry.required_fields,
3659 vec![
3660 "decision_seq".to_string(),
3661 "job_id".to_string(),
3662 "obligation".to_string(),
3663 "region".to_string(),
3664 "replay_hash".to_string(),
3665 "task".to_string(),
3666 "worker_id".to_string(),
3667 ]
3668 );
3669 }
3670 }
3671
3672 #[test]
3673 fn browser_trace_schema_decode_v0_migrates() {
3674 let legacy = serde_json::json!({
3675 "schema_version": "browser-trace-schema-v0",
3676 "required_envelope_fields": [
3677 "event_kind",
3678 "schema_version",
3679 "seq",
3680 "time_ns",
3681 "trace_id"
3682 ],
3683 "ordering_semantics": [
3684 "events must be strictly ordered by seq ascending",
3685 "logical_time must be monotonic for comparable causal domains",
3686 "trace streams must be deterministic for identical seed/config/replay inputs"
3687 ],
3688 "event_specs": browser_trace_schema_v1().event_specs
3689 });
3690 let payload = serde_json::to_string(&legacy).expect("serialize legacy schema");
3691 let decoded = decode_browser_trace_schema(&payload).expect("decode legacy schema");
3692 assert_eq!(
3693 decoded.schema_version,
3694 BROWSER_TRACE_SCHEMA_VERSION.to_string()
3695 );
3696 assert!(
3697 decoded
3698 .compatibility
3699 .backward_decode_aliases
3700 .iter()
3701 .any(|alias| alias == "browser-trace-schema-v0")
3702 );
3703 }
3704
3705 #[test]
3706 fn browser_trace_schema_decode_v0_sparse_event_specs_use_defaults() {
3707 let event_specs = TraceEventKind::ALL
3708 .iter()
3709 .map(|kind| serde_json::json!({ "event_kind": kind.stable_name() }))
3710 .collect::<Vec<_>>();
3711 let legacy = serde_json::json!({
3712 "schema_version": "browser-trace-schema-v0",
3713 "required_envelope_fields": [
3714 "event_kind",
3715 "schema_version",
3716 "seq",
3717 "time_ns",
3718 "trace_id"
3719 ],
3720 "ordering_semantics": [
3721 "events must be strictly ordered by seq ascending",
3722 "logical_time must be monotonic for comparable causal domains",
3723 "trace streams must be deterministic for identical seed/config/replay inputs"
3724 ],
3725 "event_specs": event_specs
3726 });
3727 let payload = serde_json::to_string(&legacy).expect("serialize sparse legacy schema");
3728 let decoded = decode_browser_trace_schema(&payload).expect("decode sparse legacy schema");
3729
3730 let user_trace = decoded
3731 .event_specs
3732 .iter()
3733 .find(|entry| entry.event_kind == "user_trace")
3734 .expect("user_trace entry should exist");
3735 assert_eq!(user_trace.category, BrowserTraceCategory::HostCallback);
3736 assert_eq!(user_trace.required_fields, vec!["message".to_string()]);
3737 assert_eq!(user_trace.redacted_fields, vec!["message".to_string()]);
3738 }
3739
3740 #[test]
3741 fn browser_trace_schema_decode_v0_unknown_event_kind_fails_closed() {
3742 let legacy = serde_json::json!({
3743 "schema_version": "browser-trace-schema-v0",
3744 "required_envelope_fields": [
3745 "event_kind",
3746 "schema_version",
3747 "seq",
3748 "time_ns",
3749 "trace_id"
3750 ],
3751 "ordering_semantics": [
3752 "events must be strictly ordered by seq ascending",
3753 "logical_time must be monotonic for comparable causal domains",
3754 "trace streams must be deterministic for identical seed/config/replay inputs"
3755 ],
3756 "event_specs": [{ "event_kind": "not_a_real_event_kind" }]
3757 });
3758 let payload = serde_json::to_string(&legacy).expect("serialize invalid legacy schema");
3759 let error = decode_browser_trace_schema(&payload)
3760 .expect_err("unknown legacy event kinds must fail decode");
3761 assert!(error.contains("unknown legacy event kind"));
3762 }
3763
3764 #[test]
3765 fn browser_trace_redaction_masks_message_payloads() {
3766 let event = TraceEvent::user_trace(4, Time::ZERO, "secret-token");
3767 let redacted = redact_browser_trace_event(&event);
3768 assert_eq!(
3769 redacted,
3770 TraceEvent::new(
3771 4,
3772 Time::ZERO,
3773 TraceEventKind::UserTrace,
3774 TraceData::Message("<redacted>".to_string())
3775 )
3776 );
3777 }
3778
3779 #[test]
3786 fn redact_scrubs_cancel_reason_message_92qzak() {
3787 let task = TaskId::new_for_test(0, 1);
3788 let region = RegionId::new_for_test(0, 1);
3789 let reason = crate::types::CancelReason::user("invalid token sk_live_ABC123");
3790 let mut event = TraceEvent::new(
3791 1,
3792 Time::ZERO,
3793 TraceEventKind::CancelRequest,
3794 TraceData::Cancel {
3795 task,
3796 region,
3797 reason,
3798 },
3799 );
3800 event.seq = 1;
3802 let redacted = redact_browser_trace_event(&event);
3803 match &redacted.data {
3804 TraceData::Cancel { reason, .. } => {
3805 assert_eq!(reason.message.as_deref(), Some("<redacted>"));
3806 }
3807 other => panic!("expected redacted Cancel, got {other:?}"),
3808 }
3809 }
3810
3811 #[test]
3812 fn redact_scrubs_region_cancel_reason_message_92qzak() {
3813 let region = RegionId::new_for_test(0, 1);
3814 let reason = crate::types::CancelReason::user("internal-secret");
3815 let event = TraceEvent::new(
3816 1,
3817 Time::ZERO,
3818 TraceEventKind::RegionCancelled,
3819 TraceData::RegionCancel { region, reason },
3820 );
3821 let redacted = redact_browser_trace_event(&event);
3822 match &redacted.data {
3823 TraceData::RegionCancel { reason, .. } => {
3824 assert_eq!(reason.message.as_deref(), Some("<redacted>"));
3825 }
3826 other => panic!("expected redacted RegionCancel, got {other:?}"),
3827 }
3828 }
3829
3830 #[test]
3831 fn redact_scrubs_worker_id_92qzak() {
3832 let task = TaskId::new_for_test(0, 1);
3833 let region = RegionId::new_for_test(0, 1);
3834 let obligation = ObligationId::new_for_test(0, 1);
3835 let event = TraceEvent::new(
3836 1,
3837 Time::ZERO,
3838 TraceEventKind::ChaosInjection,
3839 TraceData::Worker {
3840 worker_id: "worker-with-secret-suffix-token-abc".to_string(),
3841 job_id: 42,
3842 decision_seq: 7,
3843 replay_hash: 0xdeadbeef,
3844 task,
3845 region,
3846 obligation,
3847 },
3848 );
3849 let redacted = redact_browser_trace_event(&event);
3850 match &redacted.data {
3851 TraceData::Worker { worker_id, .. } => assert_eq!(worker_id, "<redacted>"),
3852 other => panic!("expected redacted Worker, got {other:?}"),
3853 }
3854 }
3855
3856 #[test]
3857 fn redact_scrubs_down_reason_error_string_92qzak() {
3858 let watcher = TaskId::new_for_test(0, 1);
3859 let monitored = TaskId::new_for_test(0, 2);
3860 let event = TraceEvent::new(
3861 1,
3862 Time::ZERO,
3863 TraceEventKind::ChaosInjection,
3864 TraceData::Down {
3865 monitor_ref: 1,
3866 watcher,
3867 monitored,
3868 completion_vt: Time::ZERO,
3869 reason: crate::monitor::DownReason::Error(
3870 "/etc/secret_path:42 panicked with bearer eyJ...".to_string(),
3871 ),
3872 },
3873 );
3874 let redacted = redact_browser_trace_event(&event);
3875 match &redacted.data {
3876 TraceData::Down { reason, .. } => match reason {
3877 crate::monitor::DownReason::Error(msg) => assert_eq!(msg, "<redacted>"),
3878 other => panic!("expected Error variant, got {other:?}"),
3879 },
3880 other => panic!("expected redacted Down, got {other:?}"),
3881 }
3882 }
3883
3884 #[test]
3885 fn redact_preserves_structural_identifiers_92qzak() {
3886 let task = TaskId::new_for_test(0, 1);
3890 let region = RegionId::new_for_test(0, 1);
3891 let event = TraceEvent::new(
3892 42,
3893 Time::from_nanos(1234),
3894 TraceEventKind::Spawn,
3895 TraceData::Task { task, region },
3896 );
3897 let redacted = redact_browser_trace_event(&event);
3898 assert_eq!(redacted.seq, 42);
3899 assert_eq!(redacted.time, Time::from_nanos(1234));
3900 match &redacted.data {
3901 TraceData::Task { task: t, region: r } => {
3902 assert_eq!(*t, task);
3903 assert_eq!(*r, region);
3904 }
3905 other => panic!("expected Task data, got {other:?}"),
3906 }
3907 }
3908
3909 #[test]
3910 fn browser_trace_log_fields_include_required_metadata() {
3911 let event = TraceEvent::timer_fired(9, Time::from_nanos(42), 10);
3912 let fields = browser_trace_log_fields(&event, "trace-browser-1", None);
3913
3914 assert_eq!(
3915 fields.get("schema_version"),
3916 Some(&BROWSER_TRACE_SCHEMA_VERSION.to_string())
3917 );
3918 assert_eq!(fields.get("trace_id"), Some(&"trace-browser-1".to_string()));
3919 assert_eq!(fields.get("event_kind"), Some(&"timer_fired".to_string()));
3920 assert_eq!(fields.get("seq"), Some(&"9".to_string()));
3921 assert_eq!(fields.get("capture_source"), Some(&"runtime".to_string()));
3922 assert_eq!(fields.get("capture_host_turn_seq"), Some(&"9".to_string()));
3923 assert_eq!(fields.get("capture_source_seq"), Some(&"9".to_string()));
3924 assert_eq!(fields.get("capture_host_time_ns"), Some(&"42".to_string()));
3925 assert_eq!(
3926 fields.get("capture_replay_key"),
3927 Some(&"runtime:9:9:42".to_string())
3928 );
3929 assert_eq!(fields.get("validation_status"), Some(&"valid".to_string()));
3930 assert_eq!(
3931 fields.get("validation_failure_category"),
3932 Some(&"none".to_string())
3933 );
3934 assert_eq!(fields.get("sequence_group"), Some(&"timer:10".to_string()));
3935 assert_eq!(fields.get("timer_id"), Some(&"10".to_string()));
3936 }
3937
3938 #[test]
3939 fn browser_trace_log_fields_with_capture_include_host_metadata() {
3940 let event = TraceEvent::timer_fired(17, Time::from_nanos(200), 11);
3941 let capture = BrowserCaptureMetadata {
3942 host_turn_seq: 71,
3943 source: BrowserCaptureSource::HostInput,
3944 source_seq: 4,
3945 host_time_ns: 9_001,
3946 };
3947 let fields =
3948 browser_trace_log_fields_with_capture(&event, "trace-browser-2", None, Some(&capture));
3949 assert_eq!(
3950 fields.get("capture_source"),
3951 Some(&"host_input".to_string())
3952 );
3953 assert_eq!(fields.get("capture_host_turn_seq"), Some(&"71".to_string()));
3954 assert_eq!(fields.get("capture_source_seq"), Some(&"4".to_string()));
3955 assert_eq!(
3956 fields.get("capture_host_time_ns"),
3957 Some(&"9001".to_string())
3958 );
3959 assert_eq!(
3960 fields.get("capture_replay_key"),
3961 Some(&"host_input:71:4:9001".to_string())
3962 );
3963 }
3964
3965 #[test]
3966 fn browser_trace_log_fields_sequence_group_tracks_causal_domain() {
3967 let first = TraceEvent::timer_fired(7, Time::from_nanos(10), 41);
3968 let second = TraceEvent::timer_cancelled(8, Time::from_nanos(11), 41);
3969 let unrelated = TraceEvent::timer_fired(9, Time::from_nanos(12), 99);
3970
3971 let first_fields = browser_trace_log_fields(&first, "trace-browser-group-1", None);
3972 let second_fields = browser_trace_log_fields(&second, "trace-browser-group-2", None);
3973 let unrelated_fields = browser_trace_log_fields(&unrelated, "trace-browser-group-3", None);
3974
3975 assert_eq!(
3976 first_fields.get("sequence_group"),
3977 Some(&"timer:41".to_string())
3978 );
3979 assert_eq!(
3980 first_fields.get("sequence_group"),
3981 second_fields.get("sequence_group")
3982 );
3983 assert_ne!(
3984 first_fields.get("sequence_group"),
3985 unrelated_fields.get("sequence_group")
3986 );
3987 }
3988
3989 #[test]
3990 fn browser_trace_log_fields_sequence_group_preserves_link_relationships() {
3991 let created = TraceEvent::link_created(
3992 20,
3993 Time::from_nanos(100),
3994 77,
3995 task(1),
3996 region(2),
3997 task(3),
3998 region(4),
3999 );
4000 let exited = TraceEvent::exit_delivered(
4001 21,
4002 Time::from_nanos(101),
4003 77,
4004 task(1),
4005 task(3),
4006 Time::from_nanos(55),
4007 DownReason::Normal,
4008 );
4009 let other = TraceEvent::link_dropped(
4010 22,
4011 Time::from_nanos(102),
4012 88,
4013 task(1),
4014 region(2),
4015 task(3),
4016 region(4),
4017 );
4018
4019 let created_fields = browser_trace_log_fields(&created, "trace-browser-link-1", None);
4020 let exited_fields = browser_trace_log_fields(&exited, "trace-browser-link-2", None);
4021 let other_fields = browser_trace_log_fields(&other, "trace-browser-link-3", None);
4022
4023 assert_eq!(
4024 created_fields.get("sequence_group"),
4025 Some(&"link:77".to_string())
4026 );
4027 assert_eq!(
4028 created_fields.get("sequence_group"),
4029 exited_fields.get("sequence_group")
4030 );
4031 assert_ne!(
4032 created_fields.get("sequence_group"),
4033 other_fields.get("sequence_group")
4034 );
4035 }
4036
4037 #[test]
4038 fn browser_trace_log_fields_mark_invalid_when_failure_category_is_set() {
4039 let event = TraceEvent::timer_fired(9, Time::from_nanos(42), 10);
4040 let fields =
4041 browser_trace_log_fields(&event, "trace-browser-1", Some("schema_version_mismatch"));
4042 assert_eq!(
4043 fields.get("validation_status"),
4044 Some(&"invalid".to_string())
4045 );
4046 assert_eq!(
4047 fields.get("validation_failure_category"),
4048 Some(&"schema_version_mismatch".to_string())
4049 );
4050 }
4051
4052 #[test]
4053 fn browser_trace_log_fields_redact_worker_identity_while_preserving_replay_linkage() {
4054 let raw_worker_id = "worker-a";
4055 let event = TraceEvent::worker_cancel_requested(
4056 21,
4057 Time::from_nanos(55),
4058 raw_worker_id,
4059 77,
4060 91,
4061 0x00C0_FFEE,
4062 task(9),
4063 region(10),
4064 obligation(11),
4065 );
4066 let fields = browser_trace_log_fields(&event, "trace-browser-worker-1", None);
4067 assert_eq!(fields.get("decision_seq"), Some(&"91".to_string()));
4068 assert_eq!(fields.get("job_id"), Some(&"77".to_string()));
4069 assert_eq!(fields.get("obligation"), Some(&obligation(11).to_string()));
4070 assert_eq!(fields.get("region"), Some(®ion(10).to_string()));
4071 assert_eq!(fields.get("replay_hash"), Some(&"12648430".to_string()));
4072 assert_eq!(fields.get("task"), Some(&task(9).to_string()));
4073 assert_eq!(fields.get("worker_id"), Some(&"<redacted>".to_string()));
4074 assert_eq!(
4075 fields.get("sequence_group"),
4076 Some(&"worker_job:77:<redacted>".to_string())
4077 );
4078 assert!(
4079 fields.values().all(|value| !value.contains(raw_worker_id)),
4080 "browser trace log fields must not leak raw worker identity: {fields:?}"
4081 );
4082 }
4083
4084 #[test]
4085 fn browser_trace_log_fields_snapshot_scrubs_ids_and_timestamps() {
4086 let event = TraceEvent::worker_cancel_requested(
4087 41,
4088 Time::from_nanos(123_456_789),
4089 "worker-browser-snapshot",
4090 88,
4091 17,
4092 0x00C0_FFEE,
4093 task(9),
4094 region(10),
4095 obligation(11),
4096 );
4097 let capture = BrowserCaptureMetadata {
4098 host_turn_seq: 7,
4099 source: BrowserCaptureSource::HostInput,
4100 source_seq: 19,
4101 host_time_ns: 1_726_133_456_789_000_000,
4102 };
4103
4104 let fields = browser_trace_log_fields_with_capture(
4105 &event,
4106 "trace-browser-snapshot-1",
4107 None,
4108 Some(&capture),
4109 );
4110
4111 insta::assert_json_snapshot!(
4112 "browser_trace_log_fields_worker_scrubbed",
4113 scrub_browser_trace_fields(&fields)
4114 );
4115 }
4116
4117 #[test]
4118 fn browser_trace_log_fields_timer_snapshot_scrubs_ids_and_timestamps() {
4119 let event =
4120 TraceEvent::timer_scheduled(14, Time::from_nanos(333), 42, Time::from_nanos(999));
4121 let fields = browser_trace_log_fields(&event, "trace-browser-timer-1", None);
4122
4123 insta::assert_json_snapshot!(
4124 "browser_trace_log_fields_timer_scrubbed",
4125 scrub_browser_trace_fields(&fields)
4126 );
4127 }
4128
4129 #[test]
4130 fn browser_trace_log_fields_obligation_abort_snapshot_scrubs_ids_and_timestamps() {
4131 let event = TraceEvent::obligation_abort(
4132 52,
4133 Time::from_nanos(7_777),
4134 obligation(4),
4135 task(8),
4136 region(9),
4137 ObligationKind::Lease,
4138 5_000,
4139 ObligationAbortReason::Error,
4140 );
4141 let fields = browser_trace_log_fields(&event, "trace-browser-obligation-1", None);
4142
4143 insta::assert_json_snapshot!(
4144 "browser_trace_log_fields_obligation_abort_scrubbed",
4145 scrub_browser_trace_fields(&fields)
4146 );
4147 }
4148
4149 #[test]
4150 fn browser_trace_log_fields_exit_snapshot_scrubs_ids_and_timestamps() {
4151 let event = TraceEvent::exit_delivered(
4152 61,
4153 Time::from_nanos(8_001),
4154 77,
4155 task(2),
4156 task(3),
4157 Time::from_nanos(4_444),
4158 DownReason::Normal,
4159 );
4160 let fields = browser_trace_log_fields(&event, "trace-browser-exit-1", None);
4161
4162 insta::assert_json_snapshot!(
4163 "browser_trace_log_fields_exit_scrubbed",
4164 scrub_browser_trace_fields(&fields)
4165 );
4166 }
4167
4168 #[test]
4169 fn browser_trace_log_fields_redact_large_worker_attributes() {
4170 let raw_worker_id = format!("worker-{}", "e\u{0301}".repeat(200));
4171 let event = TraceEvent::worker_cancel_requested(
4172 30,
4173 Time::from_nanos(60),
4174 raw_worker_id.clone(),
4175 123,
4176 456,
4177 0xDEAD_BEEF,
4178 task(5),
4179 region(6),
4180 obligation(7),
4181 );
4182 let fields = browser_trace_log_fields(&event, "trace-browser-worker-2", None);
4183
4184 let worker_id = fields
4185 .get("worker_id")
4186 .expect("worker_id field should be present");
4187 let sequence_group = fields
4188 .get("sequence_group")
4189 .expect("sequence_group field should be present");
4190
4191 assert_eq!(worker_id, "<redacted>");
4192 assert_eq!(sequence_group, "worker_job:123:<redacted>");
4193 assert!(
4194 fields.values().all(|value| !value.contains(&raw_worker_id)),
4195 "browser trace log fields must not leak large raw worker identity: {fields:?}"
4196 );
4197 }
4198
4199 #[test]
4200 fn browser_trace_attribute_cap_preserves_utf8_boundary() {
4201 let raw = format!("group:{}", "e\u{0301}".repeat(200));
4202 let capped = cap_browser_trace_attribute(&raw);
4203
4204 assert!(capped.len() <= MAX_BROWSER_TRACE_ATTRIBUTE_BYTES);
4205 assert!(capped.starts_with("group:"));
4206 assert!(capped.contains('#'));
4207 assert!(capped.is_char_boundary(capped.len()));
4208 }
4209
4210 #[test]
4219 fn trace_event_canonical_serialization_golden() {
4220 let events = vec![
4221 TraceEvent::new(
4222 1,
4223 Time::from_nanos(0),
4224 TraceEventKind::UserTrace,
4225 TraceData::None,
4226 ),
4227 TraceEvent::spawn(2, Time::from_nanos(100), task(1), region(1)),
4228 TraceEvent::region_created(3, Time::from_nanos(200), region(2), Some(region(1))),
4229 TraceEvent::obligation_commit(
4230 4,
4231 Time::from_nanos(300),
4232 obligation(5),
4233 task(1),
4234 region(1),
4235 ObligationKind::Lease,
4236 1_500,
4237 ),
4238 TraceEvent::obligation_abort(
4239 5,
4240 Time::from_nanos(310),
4241 obligation(6),
4242 task(1),
4243 region(1),
4244 ObligationKind::SendPermit,
4245 2_500,
4246 ObligationAbortReason::Error,
4247 ),
4248 TraceEvent::cancel_request(
4249 6,
4250 Time::from_nanos(400),
4251 task(1),
4252 region(1),
4253 CancelReason::timeout(),
4254 ),
4255 TraceEvent::worker_cancel_requested(
4256 7,
4257 Time::from_nanos(500),
4258 "worker-canonical",
4259 42,
4260 7,
4261 0xDEAD_BEEF,
4262 task(1),
4263 region(1),
4264 obligation(2),
4265 ),
4266 TraceEvent::region_cancelled(
4267 8,
4268 Time::from_nanos(600),
4269 region(1),
4270 CancelReason::shutdown(),
4271 ),
4272 TraceEvent::time_advance(
4273 9,
4274 Time::from_nanos(700),
4275 Time::from_nanos(700),
4276 Time::from_nanos(800),
4277 ),
4278 TraceEvent::timer_scheduled(10, Time::from_nanos(800), 100, Time::from_nanos(900)),
4279 TraceEvent::timer_fired(11, Time::from_nanos(810), 100),
4280 TraceEvent::io_requested(12, Time::from_nanos(900), 7, 3),
4281 TraceEvent::io_ready(13, Time::from_nanos(1_000), 7, 1),
4282 TraceEvent::io_result(14, Time::from_nanos(1_100), 7, 4_096),
4283 TraceEvent::io_error(15, Time::from_nanos(1_200), 7, 5),
4284 TraceEvent::rng_seed(16, Time::from_nanos(1_300), 0x00C0_FFEE),
4285 TraceEvent::rng_value(17, Time::from_nanos(1_400), 42),
4286 TraceEvent::checkpoint(18, Time::from_nanos(1_500), 1_000, 5, 2),
4287 TraceEvent::new(
4288 19,
4289 Time::from_nanos(1_600),
4290 TraceEventKind::FuturelockDetected,
4291 TraceData::Futurelock {
4292 task: task(2),
4293 region: region(1),
4294 idle_steps: 100,
4295 held: vec![(obligation(3), ObligationKind::SendPermit)],
4296 },
4297 ),
4298 TraceEvent::monitor_created(
4299 20,
4300 Time::from_nanos(1_700),
4301 50,
4302 task(1),
4303 region(1),
4304 task(2),
4305 ),
4306 TraceEvent::down_delivered(
4307 21,
4308 Time::from_nanos(1_800),
4309 50,
4310 task(1),
4311 task(2),
4312 Time::from_nanos(1_750),
4313 DownReason::Normal,
4314 ),
4315 TraceEvent::link_created(
4316 22,
4317 Time::from_nanos(1_900),
4318 60,
4319 task(1),
4320 region(1),
4321 task(2),
4322 region(2),
4323 ),
4324 TraceEvent::exit_delivered(
4325 23,
4326 Time::from_nanos(2_000),
4327 60,
4328 task(1),
4329 task(2),
4330 Time::from_nanos(1_950),
4331 DownReason::Normal,
4332 ),
4333 TraceEvent::user_trace(24, Time::from_nanos(2_100), "canonical-trace-marker"),
4334 TraceEvent::new(
4335 25,
4336 Time::from_nanos(2_200),
4337 TraceEventKind::ChaosInjection,
4338 TraceData::Chaos {
4339 kind: "delay".to_string(),
4340 task: Some(task(1)),
4341 detail: "injected 1ms delay".to_string(),
4342 },
4343 ),
4344 TraceEvent::poll(26, Time::from_nanos(2_300), task(1), region(1))
4345 .with_logical_time(LogicalTime::Lamport(LamportTime::from_raw(7))),
4346 ];
4347
4348 for event in &events {
4353 let json = serde_json::to_value(event).expect("serialize trace event");
4354 let decoded: TraceEvent =
4355 serde_json::from_value(json).expect("deserialize trace event");
4356 assert_eq!(*event, decoded, "round-trip mismatch for {event:?}");
4357 }
4358
4359 insta::assert_json_snapshot!("trace_event_canonical_serialization", events);
4360 }
4361
4362 #[test]
4370 fn otel_span_golden_tests() {
4371 let span_events = vec![
4372 TraceEvent::spawn(1, Time::from_nanos(1000), task(10), region(5)),
4374 TraceEvent::schedule(2, Time::from_nanos(1100), task(10), region(5)),
4376 TraceEvent::poll(3, Time::from_nanos(1200), task(10), region(5)),
4378 TraceEvent::user_trace(4, Time::from_nanos(1250), "otel-span-processing"),
4380 TraceEvent::complete(5, Time::from_nanos(1300), task(10), region(5)),
4382 ];
4383
4384 for event in &span_events {
4386 let json = serde_json::to_value(event).expect("serialize otel span event");
4387 let decoded: TraceEvent =
4388 serde_json::from_value(json).expect("deserialize otel span event");
4389 assert_eq!(
4390 *event, decoded,
4391 "otel span round-trip mismatch for {event:?}"
4392 );
4393 }
4394
4395 insta::assert_json_snapshot!("otel_span_golden_tests", span_events);
4396 }
4397
4398 fn trace_events_to_ndjson(events: &[TraceEvent]) -> Result<String, serde_json::Error> {
4403 let mut ndjson = String::new();
4404 for event in events {
4405 let json_line = serde_json::to_string(event)?;
4406 ndjson.push_str(&json_line);
4407 ndjson.push('\n');
4408 }
4409 Ok(ndjson)
4410 }
4411
4412 #[test]
4419 fn trace_event_canonical_ndjson_serialization() {
4420 let ndjson_events = vec![
4421 TraceEvent::spawn(1, Time::from_nanos(5000), task(20), region(10)),
4423 TraceEvent::schedule(2, Time::from_nanos(5100), task(20), region(10)),
4424 TraceEvent::poll(3, Time::from_nanos(5200), task(20), region(10)),
4425 TraceEvent::user_trace(4, Time::from_nanos(5250), "ndjson-stream-marker"),
4426 TraceEvent::complete(5, Time::from_nanos(5300), task(20), region(10)),
4427 ];
4428
4429 let ndjson_output =
4431 trace_events_to_ndjson(&ndjson_events).expect("NDJSON serialization should succeed");
4432
4433 for (i, line) in ndjson_output.lines().enumerate() {
4435 if !line.is_empty() {
4436 let parsed: serde_json::Value = serde_json::from_str(line)
4437 .unwrap_or_else(|e| panic!("Line {i} is not valid JSON: {e}"));
4438 assert!(parsed.is_object(), "Line {i} should be a JSON object");
4439 }
4440 }
4441
4442 let parsed_events: Result<Vec<TraceEvent>, _> = ndjson_output
4444 .lines()
4445 .filter(|line| !line.is_empty())
4446 .map(serde_json::from_str)
4447 .collect();
4448 let decoded_events = parsed_events.expect("NDJSON round-trip should succeed");
4449 assert_eq!(ndjson_events, decoded_events, "NDJSON round-trip mismatch");
4450
4451 insta::assert_snapshot!("trace_event_canonical_ndjson_serialization", ndjson_output);
4452 }
4453}