1use crate::monitor::DownReason;
7use crate::record::{ObligationAbortReason, ObligationKind, ObligationState};
8use crate::trace::distributed::LogicalTime;
9use crate::types::{CancelReason, ObligationId, 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)]
108pub enum TraceEventKind {
109 Spawn,
111 Schedule,
113 Yield,
115 Wake,
117 Poll,
119 Complete,
121 CancelRequest,
123 CancelAck,
125 WorkerCancelRequested,
127 WorkerCancelAcknowledged,
129 WorkerDrainStarted,
131 WorkerDrainCompleted,
133 WorkerFinalizeCompleted,
135 RegionCloseBegin,
137 RegionCloseComplete,
139 RegionCreated,
141 RegionCancelled,
143 ObligationReserve,
145 ObligationCommit,
147 ObligationAbort,
149 ObligationLeak,
151 TimeAdvance,
153 TimerScheduled,
155 TimerFired,
157 TimerCancelled,
159 IoRequested,
161 IoReady,
163 IoResult,
165 IoError,
167 RngSeed,
169 RngValue,
171 Checkpoint,
173 FuturelockDetected,
175 ChaosInjection,
177 UserTrace,
179 MonitorCreated,
181 MonitorDropped,
183 DownDelivered,
185 LinkCreated,
187 LinkDropped,
189 ExitDelivered,
191}
192
193impl TraceEventKind {
194 pub const ALL: [Self; 41] = [
199 Self::Spawn,
200 Self::Schedule,
201 Self::Yield,
202 Self::Wake,
203 Self::Poll,
204 Self::Complete,
205 Self::CancelRequest,
206 Self::CancelAck,
207 Self::WorkerCancelRequested,
208 Self::WorkerCancelAcknowledged,
209 Self::WorkerDrainStarted,
210 Self::WorkerDrainCompleted,
211 Self::WorkerFinalizeCompleted,
212 Self::RegionCloseBegin,
213 Self::RegionCloseComplete,
214 Self::RegionCreated,
215 Self::RegionCancelled,
216 Self::ObligationReserve,
217 Self::ObligationCommit,
218 Self::ObligationAbort,
219 Self::ObligationLeak,
220 Self::TimeAdvance,
221 Self::TimerScheduled,
222 Self::TimerFired,
223 Self::TimerCancelled,
224 Self::IoRequested,
225 Self::IoReady,
226 Self::IoResult,
227 Self::IoError,
228 Self::RngSeed,
229 Self::RngValue,
230 Self::Checkpoint,
231 Self::FuturelockDetected,
232 Self::ChaosInjection,
233 Self::UserTrace,
234 Self::MonitorCreated,
235 Self::MonitorDropped,
236 Self::DownDelivered,
237 Self::LinkCreated,
238 Self::LinkDropped,
239 Self::ExitDelivered,
240 ];
241
242 #[must_use]
244 pub const fn stable_name(self) -> &'static str {
245 match self {
246 Self::Spawn => "spawn",
247 Self::Schedule => "schedule",
248 Self::Yield => "yield",
249 Self::Wake => "wake",
250 Self::Poll => "poll",
251 Self::Complete => "complete",
252 Self::CancelRequest => "cancel_request",
253 Self::CancelAck => "cancel_ack",
254 Self::WorkerCancelRequested => "worker_cancel_requested",
255 Self::WorkerCancelAcknowledged => "worker_cancel_acknowledged",
256 Self::WorkerDrainStarted => "worker_drain_started",
257 Self::WorkerDrainCompleted => "worker_drain_completed",
258 Self::WorkerFinalizeCompleted => "worker_finalize_completed",
259 Self::RegionCloseBegin => "region_close_begin",
260 Self::RegionCloseComplete => "region_close_complete",
261 Self::RegionCreated => "region_created",
262 Self::RegionCancelled => "region_cancelled",
263 Self::ObligationReserve => "obligation_reserve",
264 Self::ObligationCommit => "obligation_commit",
265 Self::ObligationAbort => "obligation_abort",
266 Self::ObligationLeak => "obligation_leak",
267 Self::TimeAdvance => "time_advance",
268 Self::TimerScheduled => "timer_scheduled",
269 Self::TimerFired => "timer_fired",
270 Self::TimerCancelled => "timer_cancelled",
271 Self::IoRequested => "io_requested",
272 Self::IoReady => "io_ready",
273 Self::IoResult => "io_result",
274 Self::IoError => "io_error",
275 Self::RngSeed => "rng_seed",
276 Self::RngValue => "rng_value",
277 Self::Checkpoint => "checkpoint",
278 Self::FuturelockDetected => "futurelock_detected",
279 Self::ChaosInjection => "chaos_injection",
280 Self::UserTrace => "user_trace",
281 Self::MonitorCreated => "monitor_created",
282 Self::MonitorDropped => "monitor_dropped",
283 Self::DownDelivered => "down_delivered",
284 Self::LinkCreated => "link_created",
285 Self::LinkDropped => "link_dropped",
286 Self::ExitDelivered => "exit_delivered",
287 }
288 }
289
290 #[must_use]
292 pub const fn required_fields(self) -> &'static str {
293 match self {
294 Self::Spawn
295 | Self::Schedule
296 | Self::Yield
297 | Self::Wake
298 | Self::Poll
299 | Self::Complete => "task, region",
300 Self::CancelRequest | Self::CancelAck => "task, region, reason",
301 Self::WorkerCancelRequested
302 | Self::WorkerCancelAcknowledged
303 | Self::WorkerDrainStarted
304 | Self::WorkerDrainCompleted
305 | Self::WorkerFinalizeCompleted => {
306 "decision_seq, job_id, obligation, region, replay_hash, task, worker_id"
307 }
308 Self::RegionCloseBegin | Self::RegionCloseComplete | Self::RegionCreated => {
309 "region, parent"
310 }
311 Self::RegionCancelled => "region, reason",
312 Self::ObligationReserve => "obligation, task, region, kind, state",
313 Self::ObligationCommit | Self::ObligationLeak => {
314 "obligation, task, region, kind, state, duration_ns"
315 }
316 Self::ObligationAbort => {
317 "obligation, task, region, kind, state, duration_ns, abort_reason"
318 }
319 Self::TimeAdvance => "old, new",
320 Self::TimerScheduled => "timer_id, deadline",
321 Self::TimerFired | Self::TimerCancelled => "timer_id",
322 Self::IoRequested => "token, interest",
323 Self::IoReady => "token, readiness",
324 Self::IoResult => "token, bytes",
325 Self::IoError => "token, kind",
326 Self::RngSeed => "seed",
327 Self::RngValue => "value",
328 Self::Checkpoint => "sequence, active_tasks, active_regions",
329 Self::FuturelockDetected => "task, region, idle_steps, held",
330 Self::ChaosInjection => "kind, task, detail",
331 Self::UserTrace => "message",
332 Self::MonitorCreated | Self::MonitorDropped => {
333 "monitor_ref, watcher, watcher_region, monitored"
334 }
335 Self::DownDelivered => "monitor_ref, watcher, monitored, completion_vt, reason",
336 Self::LinkCreated | Self::LinkDropped => "link_ref, task_a, region_a, task_b, region_b",
337 Self::ExitDelivered => "link_ref, from, to, failure_vt, reason",
338 }
339 }
340}
341
342#[must_use]
344pub const fn browser_trace_category_for_kind(kind: TraceEventKind) -> BrowserTraceCategory {
345 match kind {
346 TraceEventKind::Spawn
347 | TraceEventKind::Schedule
348 | TraceEventKind::Yield
349 | TraceEventKind::Wake
350 | TraceEventKind::Poll
351 | TraceEventKind::Complete
352 | TraceEventKind::Checkpoint
353 | TraceEventKind::FuturelockDetected => BrowserTraceCategory::Scheduler,
354 TraceEventKind::TimeAdvance
355 | TraceEventKind::TimerScheduled
356 | TraceEventKind::TimerFired
357 | TraceEventKind::TimerCancelled => BrowserTraceCategory::Timer,
358 TraceEventKind::IoRequested
359 | TraceEventKind::IoReady
360 | TraceEventKind::IoResult
361 | TraceEventKind::IoError
362 | TraceEventKind::RngSeed
363 | TraceEventKind::RngValue
364 | TraceEventKind::UserTrace
365 | TraceEventKind::ChaosInjection => BrowserTraceCategory::HostCallback,
366 TraceEventKind::ObligationReserve
367 | TraceEventKind::ObligationCommit
368 | TraceEventKind::ObligationAbort
369 | TraceEventKind::ObligationLeak
370 | TraceEventKind::RegionCreated
371 | TraceEventKind::MonitorCreated
372 | TraceEventKind::MonitorDropped
373 | TraceEventKind::DownDelivered
374 | TraceEventKind::LinkCreated
375 | TraceEventKind::LinkDropped
376 | TraceEventKind::ExitDelivered => BrowserTraceCategory::CapabilityInvocation,
377 TraceEventKind::CancelRequest
378 | TraceEventKind::CancelAck
379 | TraceEventKind::WorkerCancelRequested
380 | TraceEventKind::WorkerCancelAcknowledged
381 | TraceEventKind::WorkerDrainStarted
382 | TraceEventKind::WorkerDrainCompleted
383 | TraceEventKind::WorkerFinalizeCompleted
384 | TraceEventKind::RegionCloseBegin
385 | TraceEventKind::RegionCloseComplete
386 | TraceEventKind::RegionCancelled => BrowserTraceCategory::CancellationTransition,
387 }
388}
389
390#[must_use]
392pub const fn browser_trace_category_name(category: BrowserTraceCategory) -> &'static str {
393 match category {
394 BrowserTraceCategory::Scheduler => "scheduler",
395 BrowserTraceCategory::Timer => "timer",
396 BrowserTraceCategory::HostCallback => "host_callback",
397 BrowserTraceCategory::CapabilityInvocation => "capability_invocation",
398 BrowserTraceCategory::CancellationTransition => "cancellation_transition",
399 }
400}
401
402fn redacted_fields_for_kind(kind: TraceEventKind) -> Vec<String> {
403 match kind {
404 TraceEventKind::UserTrace => vec!["message".to_string()],
405 TraceEventKind::ChaosInjection => vec!["detail".to_string()],
406 _ => Vec::new(),
407 }
408}
409
410fn split_required_fields_csv(csv: &str) -> Vec<String> {
411 let mut fields = csv
412 .split(',')
413 .map(str::trim)
414 .filter(|value| !value.is_empty())
415 .map(ToString::to_string)
416 .collect::<Vec<_>>();
417 fields.sort();
418 fields.dedup();
419 fields
420}
421
422fn trace_event_kind_from_stable_name(name: &str) -> Option<TraceEventKind> {
423 TraceEventKind::ALL
424 .iter()
425 .copied()
426 .find(|kind| kind.stable_name() == name)
427}
428
429fn validate_lexical_string_set(values: &[String], field: &str) -> Result<(), String> {
430 if values.is_empty() {
431 return Err(format!("{field} must be non-empty"));
432 }
433 for value in values {
434 if value.trim().is_empty() {
435 return Err(format!("{field} must not contain empty values"));
436 }
437 }
438 for window in values.windows(2) {
439 if window[0] >= window[1] {
440 return Err(format!("{field} must be lexically sorted and unique"));
441 }
442 }
443 Ok(())
444}
445
446#[must_use]
448pub fn browser_trace_schema_v1() -> BrowserTraceSchema {
449 let mut event_specs = TraceEventKind::ALL
450 .iter()
451 .map(|kind| {
452 let mut redacted_fields = redacted_fields_for_kind(*kind);
453 redacted_fields.sort();
454 redacted_fields.dedup();
455 BrowserTraceEventSpec {
456 event_kind: kind.stable_name().to_string(),
457 category: browser_trace_category_for_kind(*kind),
458 required_fields: split_required_fields_csv(kind.required_fields()),
459 redacted_fields,
460 }
461 })
462 .collect::<Vec<_>>();
463 event_specs.sort_by(|left, right| left.event_kind.cmp(&right.event_kind));
464
465 BrowserTraceSchema {
466 schema_version: BROWSER_TRACE_SCHEMA_VERSION.to_string(),
467 required_envelope_fields: vec![
468 "event_kind".to_string(),
469 "schema_version".to_string(),
470 "seq".to_string(),
471 "time_ns".to_string(),
472 "trace_id".to_string(),
473 ],
474 ordering_semantics: vec![
475 "events must be strictly ordered by seq ascending".to_string(),
476 "logical_time must be monotonic for comparable causal domains".to_string(),
477 "trace streams must be deterministic for identical seed/config/replay inputs"
478 .to_string(),
479 ],
480 structured_log_required_fields: vec![
481 "capture_host_time_ns".to_string(),
482 "capture_host_turn_seq".to_string(),
483 "capture_replay_key".to_string(),
484 "capture_source".to_string(),
485 "capture_source_seq".to_string(),
486 "event_kind".to_string(),
487 "schema_version".to_string(),
488 "seq".to_string(),
489 "sequence_group".to_string(),
490 "time_ns".to_string(),
491 "trace_id".to_string(),
492 "validation_failure_category".to_string(),
493 "validation_status".to_string(),
494 ],
495 validation_failure_categories: vec![
496 "invalid_event_payload".to_string(),
497 "missing_required_field".to_string(),
498 "schema_version_mismatch".to_string(),
499 "sequence_regression".to_string(),
500 ],
501 event_specs,
502 compatibility: BrowserTraceCompatibility {
503 minimum_reader_version: "browser-trace-schema-v0".to_string(),
504 supported_reader_versions: vec![
505 "browser-trace-schema-v0".to_string(),
506 BROWSER_TRACE_SCHEMA_VERSION.to_string(),
507 ],
508 backward_decode_aliases: vec!["browser-trace-schema-v0".to_string()],
509 },
510 }
511}
512
513#[allow(clippy::too_many_lines)]
520pub fn validate_browser_trace_schema(schema: &BrowserTraceSchema) -> Result<(), String> {
521 if schema.schema_version != BROWSER_TRACE_SCHEMA_VERSION {
522 return Err(format!(
523 "unsupported browser trace schema version {}",
524 schema.schema_version
525 ));
526 }
527
528 validate_lexical_string_set(&schema.required_envelope_fields, "required_envelope_fields")?;
529 validate_lexical_string_set(&schema.ordering_semantics, "ordering_semantics")?;
530 validate_lexical_string_set(
531 &schema.structured_log_required_fields,
532 "structured_log_required_fields",
533 )?;
534 validate_lexical_string_set(
535 &schema.validation_failure_categories,
536 "validation_failure_categories",
537 )?;
538
539 for required in [
540 "capture_host_time_ns",
541 "capture_host_turn_seq",
542 "capture_replay_key",
543 "capture_source",
544 "capture_source_seq",
545 "trace_id",
546 "time_ns",
547 "seq",
548 "sequence_group",
549 "event_kind",
550 "schema_version",
551 "validation_failure_category",
552 "validation_status",
553 ] {
554 if !schema
555 .structured_log_required_fields
556 .iter()
557 .any(|field| field == required)
558 {
559 return Err(format!("structured_log_required_fields missing {required}"));
560 }
561 }
562
563 if schema.event_specs.is_empty() {
564 return Err("event_specs must be non-empty".to_string());
565 }
566 let event_kinds = schema
567 .event_specs
568 .iter()
569 .map(|entry| entry.event_kind.clone())
570 .collect::<Vec<_>>();
571 validate_lexical_string_set(&event_kinds, "event_specs.event_kind")?;
572
573 let expected = TraceEventKind::ALL
574 .iter()
575 .map(|kind| kind.stable_name().to_string())
576 .collect::<BTreeSet<_>>();
577 let observed = event_kinds.into_iter().collect::<BTreeSet<_>>();
578 if expected != observed {
579 return Err("event_specs must include exactly all TraceEventKind stable names".to_string());
580 }
581
582 for entry in &schema.event_specs {
583 validate_lexical_string_set(
584 &entry.required_fields,
585 &format!("event_specs[{}].required_fields", entry.event_kind),
586 )?;
587 if !entry.redacted_fields.is_empty() {
588 validate_lexical_string_set(
589 &entry.redacted_fields,
590 &format!("event_specs[{}].redacted_fields", entry.event_kind),
591 )?;
592 for field in &entry.redacted_fields {
593 if !entry
594 .required_fields
595 .iter()
596 .any(|required| required == field)
597 {
598 return Err(format!(
599 "event_specs[{}].redacted_fields contains unknown field {}",
600 entry.event_kind, field
601 ));
602 }
603 }
604 }
605 }
606
607 if schema
608 .compatibility
609 .minimum_reader_version
610 .trim()
611 .is_empty()
612 {
613 return Err("compatibility.minimum_reader_version must be non-empty".to_string());
614 }
615 validate_lexical_string_set(
616 &schema.compatibility.supported_reader_versions,
617 "compatibility.supported_reader_versions",
618 )?;
619 if !schema
620 .compatibility
621 .supported_reader_versions
622 .iter()
623 .any(|version| version == &schema.compatibility.minimum_reader_version)
624 {
625 return Err("minimum_reader_version missing from supported_reader_versions".to_string());
626 }
627 if !schema
628 .compatibility
629 .supported_reader_versions
630 .iter()
631 .any(|version| version == BROWSER_TRACE_SCHEMA_VERSION)
632 {
633 return Err("supported_reader_versions must include browser-trace-schema-v1".to_string());
634 }
635 validate_lexical_string_set(
636 &schema.compatibility.backward_decode_aliases,
637 "compatibility.backward_decode_aliases",
638 )?;
639
640 Ok(())
641}
642
643#[derive(Debug, Deserialize)]
644struct BrowserTraceSchemaLegacyV0 {
645 schema_version: String,
646 required_envelope_fields: Vec<String>,
647 ordering_semantics: Vec<String>,
648 event_specs: Vec<BrowserTraceEventSpecLegacyV0>,
649}
650
651#[derive(Debug, Deserialize)]
652struct BrowserTraceEventSpecLegacyV0 {
653 event_kind: String,
654 category: Option<BrowserTraceCategory>,
655 required_fields: Option<Vec<String>>,
656 redacted_fields: Option<Vec<String>>,
657}
658
659fn upgrade_legacy_event_specs(
660 legacy_specs: Vec<BrowserTraceEventSpecLegacyV0>,
661) -> Result<Vec<BrowserTraceEventSpec>, String> {
662 let mut event_specs = Vec::with_capacity(legacy_specs.len());
663 for legacy in legacy_specs {
664 let kind = trace_event_kind_from_stable_name(legacy.event_kind.as_str())
665 .ok_or_else(|| format!("unknown legacy event kind {}", legacy.event_kind))?;
666
667 let mut required_fields = legacy
668 .required_fields
669 .unwrap_or_else(|| split_required_fields_csv(kind.required_fields()));
670 required_fields.sort();
671 required_fields.dedup();
672
673 let mut redacted_fields = legacy
674 .redacted_fields
675 .unwrap_or_else(|| redacted_fields_for_kind(kind));
676 redacted_fields.sort();
677 redacted_fields.dedup();
678
679 event_specs.push(BrowserTraceEventSpec {
680 event_kind: kind.stable_name().to_string(),
681 category: legacy
682 .category
683 .unwrap_or_else(|| browser_trace_category_for_kind(kind)),
684 required_fields,
685 redacted_fields,
686 });
687 }
688 event_specs.sort_by(|left, right| left.event_kind.cmp(&right.event_kind));
689 Ok(event_specs)
690}
691
692pub fn decode_browser_trace_schema(payload: &str) -> Result<BrowserTraceSchema, String> {
698 let value: serde_json::Value =
699 serde_json::from_str(payload).map_err(|err| format!("invalid schema JSON: {err}"))?;
700 let version = value
701 .get("schema_version")
702 .and_then(serde_json::Value::as_str)
703 .ok_or_else(|| "schema_version must be a string".to_string())?;
704
705 let schema = match version {
706 BROWSER_TRACE_SCHEMA_VERSION => serde_json::from_value::<BrowserTraceSchema>(value)
707 .map_err(|err| format!("invalid browser-trace-schema-v1 payload: {err}"))?,
708 "browser-trace-schema-v0" => {
709 let legacy = serde_json::from_value::<BrowserTraceSchemaLegacyV0>(value)
710 .map_err(|err| format!("invalid browser-trace-schema-v0 payload: {err}"))?;
711 if legacy.schema_version != "browser-trace-schema-v0" {
712 return Err(format!(
713 "invalid legacy schema version {}",
714 legacy.schema_version
715 ));
716 }
717 let mut schema = browser_trace_schema_v1();
718 schema.required_envelope_fields = legacy.required_envelope_fields;
719 schema.ordering_semantics = legacy.ordering_semantics;
720 schema.event_specs = upgrade_legacy_event_specs(legacy.event_specs)?;
721 schema.compatibility.backward_decode_aliases =
722 vec!["browser-trace-schema-v0".to_string()];
723 schema.compatibility.minimum_reader_version = "browser-trace-schema-v0".to_string();
724 schema
725 }
726 other => {
727 return Err(format!("unsupported browser trace schema version {other}"));
728 }
729 };
730
731 validate_browser_trace_schema(&schema)?;
732 Ok(schema)
733}
734
735#[must_use]
737pub fn redact_browser_trace_event(event: &TraceEvent) -> TraceEvent {
738 let mut redacted = event.clone();
739 match (&event.kind, &event.data) {
740 (TraceEventKind::UserTrace, TraceData::Message(_)) => {
741 redacted.data = TraceData::Message("<redacted>".to_string());
742 }
743 (
744 TraceEventKind::ChaosInjection,
745 TraceData::Chaos {
746 kind,
747 task,
748 detail: _,
749 },
750 ) => {
751 redacted.data = TraceData::Chaos {
752 kind: kind.clone(),
753 task: *task,
754 detail: "<redacted>".to_string(),
755 };
756 }
757 _ => {}
758 }
759 redacted
760}
761
762fn default_browser_capture_metadata(event: &TraceEvent) -> BrowserCaptureMetadata {
763 BrowserCaptureMetadata {
764 host_turn_seq: event.seq,
765 source: BrowserCaptureSource::Runtime,
766 source_seq: event.seq,
767 host_time_ns: event.time.as_nanos(),
768 }
769}
770
771fn stable_browser_trace_hash(bytes: &[u8]) -> u64 {
772 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
773 for &byte in bytes {
774 hash ^= u64::from(byte);
775 hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
776 }
777 hash
778}
779
780fn cap_browser_trace_attribute(value: &str) -> String {
781 if value.len() <= MAX_BROWSER_TRACE_ATTRIBUTE_BYTES {
782 return value.to_string();
783 }
784
785 let suffix = format!("#{:016x}", stable_browser_trace_hash(value.as_bytes()));
786 let mut cut = MAX_BROWSER_TRACE_ATTRIBUTE_BYTES.saturating_sub(suffix.len());
787 while cut > 0 && !value.is_char_boundary(cut) {
788 cut -= 1;
789 }
790
791 let mut capped = value[..cut].to_string();
792 capped.push_str(&suffix);
793 capped
794}
795
796fn obligation_state_name(state: ObligationState) -> &'static str {
797 match state {
798 ObligationState::Reserved => "reserved",
799 ObligationState::Committed => "committed",
800 ObligationState::Aborted => "aborted",
801 ObligationState::Leaked => "leaked",
802 }
803}
804
805fn optional_time_field(value: Option<Time>) -> String {
806 value.map_or_else(|| "none".to_string(), |time| time.as_nanos().to_string())
807}
808
809fn optional_display_field<T: fmt::Display>(value: Option<T>) -> String {
810 value.map_or_else(|| "none".to_string(), |value| value.to_string())
811}
812
813fn futurelock_held_field(held: &[(ObligationId, ObligationKind)]) -> String {
814 let held = held
815 .iter()
816 .map(|(obligation, kind)| format!("{obligation}:{kind}"))
817 .collect::<Vec<_>>();
818 serde_json::to_string(&held).expect("futurelock held obligations serialize")
819}
820
821fn insert_browser_trace_payload_fields(fields: &mut BTreeMap<String, String>, event: &TraceEvent) {
822 match &event.data {
823 TraceData::None => {}
824 TraceData::Task { task, region } => {
825 fields.insert("task".to_string(), task.to_string());
826 fields.insert("region".to_string(), region.to_string());
827 }
828 TraceData::Region { region, parent } => {
829 fields.insert("region".to_string(), region.to_string());
830 fields.insert("parent".to_string(), optional_display_field(*parent));
831 }
832 TraceData::Obligation {
833 obligation,
834 task,
835 region,
836 kind,
837 state,
838 duration_ns,
839 abort_reason,
840 } => {
841 fields.insert("obligation".to_string(), obligation.to_string());
842 fields.insert("task".to_string(), task.to_string());
843 fields.insert("region".to_string(), region.to_string());
844 fields.insert("kind".to_string(), kind.to_string());
845 fields.insert(
846 "state".to_string(),
847 obligation_state_name(*state).to_string(),
848 );
849
850 if matches!(
851 event.kind,
852 TraceEventKind::ObligationCommit
853 | TraceEventKind::ObligationAbort
854 | TraceEventKind::ObligationLeak
855 ) {
856 fields.insert(
857 "duration_ns".to_string(),
858 duration_ns.map_or_else(|| "none".to_string(), |value| value.to_string()),
859 );
860 }
861
862 if matches!(event.kind, TraceEventKind::ObligationAbort) {
863 fields.insert(
864 "abort_reason".to_string(),
865 abort_reason.map_or_else(|| "none".to_string(), |reason| reason.to_string()),
866 );
867 }
868 }
869 TraceData::Cancel {
870 task,
871 region,
872 reason,
873 } => {
874 fields.insert("task".to_string(), task.to_string());
875 fields.insert("region".to_string(), region.to_string());
876 fields.insert("reason".to_string(), reason.to_string());
877 }
878 TraceData::Worker {
879 worker_id,
880 job_id,
881 decision_seq,
882 replay_hash,
883 task,
884 region,
885 obligation,
886 } => {
887 fields.insert("decision_seq".to_string(), decision_seq.to_string());
888 fields.insert("job_id".to_string(), job_id.to_string());
889 fields.insert("obligation".to_string(), obligation.to_string());
890 fields.insert("region".to_string(), region.to_string());
891 fields.insert("replay_hash".to_string(), replay_hash.to_string());
892 fields.insert("task".to_string(), task.to_string());
893 fields.insert(
894 "worker_id".to_string(),
895 cap_browser_trace_attribute(worker_id),
896 );
897 }
898 TraceData::RegionCancel { region, reason } => {
899 fields.insert("region".to_string(), region.to_string());
900 fields.insert("reason".to_string(), reason.to_string());
901 }
902 TraceData::Time { old, new } => {
903 fields.insert("old".to_string(), old.as_nanos().to_string());
904 fields.insert("new".to_string(), new.as_nanos().to_string());
905 }
906 TraceData::Timer { timer_id, deadline } => {
907 fields.insert("timer_id".to_string(), timer_id.to_string());
908 if matches!(event.kind, TraceEventKind::TimerScheduled) || deadline.is_some() {
909 fields.insert("deadline".to_string(), optional_time_field(*deadline));
910 }
911 }
912 TraceData::IoRequested { token, interest } => {
913 fields.insert("token".to_string(), token.to_string());
914 fields.insert("interest".to_string(), interest.to_string());
915 }
916 TraceData::IoReady { token, readiness } => {
917 fields.insert("token".to_string(), token.to_string());
918 fields.insert("readiness".to_string(), readiness.to_string());
919 }
920 TraceData::IoResult { token, bytes } => {
921 fields.insert("token".to_string(), token.to_string());
922 fields.insert("bytes".to_string(), bytes.to_string());
923 }
924 TraceData::IoError { token, kind } => {
925 fields.insert("token".to_string(), token.to_string());
926 fields.insert("kind".to_string(), kind.to_string());
927 }
928 TraceData::RngSeed { seed } => {
929 fields.insert("seed".to_string(), seed.to_string());
930 }
931 TraceData::RngValue { value } => {
932 fields.insert("value".to_string(), value.to_string());
933 }
934 TraceData::Checkpoint {
935 sequence,
936 active_tasks,
937 active_regions,
938 } => {
939 fields.insert("sequence".to_string(), sequence.to_string());
940 fields.insert("active_tasks".to_string(), active_tasks.to_string());
941 fields.insert("active_regions".to_string(), active_regions.to_string());
942 }
943 TraceData::Futurelock {
944 task,
945 region,
946 idle_steps,
947 held,
948 } => {
949 fields.insert("task".to_string(), task.to_string());
950 fields.insert("region".to_string(), region.to_string());
951 fields.insert("idle_steps".to_string(), idle_steps.to_string());
952 fields.insert("held".to_string(), futurelock_held_field(held));
953 }
954 TraceData::Monitor {
955 monitor_ref,
956 watcher,
957 watcher_region,
958 monitored,
959 } => {
960 fields.insert("monitor_ref".to_string(), monitor_ref.to_string());
961 fields.insert("watcher".to_string(), watcher.to_string());
962 fields.insert("watcher_region".to_string(), watcher_region.to_string());
963 fields.insert("monitored".to_string(), monitored.to_string());
964 }
965 TraceData::Down {
966 monitor_ref,
967 watcher,
968 monitored,
969 completion_vt,
970 reason,
971 } => {
972 fields.insert("monitor_ref".to_string(), monitor_ref.to_string());
973 fields.insert("watcher".to_string(), watcher.to_string());
974 fields.insert("monitored".to_string(), monitored.to_string());
975 fields.insert(
976 "completion_vt".to_string(),
977 completion_vt.as_nanos().to_string(),
978 );
979 fields.insert("reason".to_string(), reason.to_string());
980 }
981 TraceData::Link {
982 link_ref,
983 task_a,
984 region_a,
985 task_b,
986 region_b,
987 } => {
988 fields.insert("link_ref".to_string(), link_ref.to_string());
989 fields.insert("task_a".to_string(), task_a.to_string());
990 fields.insert("region_a".to_string(), region_a.to_string());
991 fields.insert("task_b".to_string(), task_b.to_string());
992 fields.insert("region_b".to_string(), region_b.to_string());
993 }
994 TraceData::Exit {
995 link_ref,
996 from,
997 to,
998 failure_vt,
999 reason,
1000 } => {
1001 fields.insert("link_ref".to_string(), link_ref.to_string());
1002 fields.insert("from".to_string(), from.to_string());
1003 fields.insert("to".to_string(), to.to_string());
1004 fields.insert("failure_vt".to_string(), failure_vt.as_nanos().to_string());
1005 fields.insert("reason".to_string(), reason.to_string());
1006 }
1007 TraceData::Message(message) => {
1008 fields.insert("message".to_string(), message.clone());
1009 }
1010 TraceData::Chaos { kind, task, detail } => {
1011 fields.insert("kind".to_string(), kind.clone());
1012 fields.insert("task".to_string(), optional_display_field(*task));
1013 fields.insert("detail".to_string(), detail.clone());
1014 }
1015 }
1016}
1017
1018fn browser_trace_sequence_group(event: &TraceEvent) -> String {
1019 let raw = match &event.data {
1023 TraceData::Task { task, .. }
1024 | TraceData::Cancel { task, .. }
1025 | TraceData::Futurelock { task, .. } => format!("task:{task}"),
1026 TraceData::Region { region, .. } | TraceData::RegionCancel { region, .. } => {
1027 format!("region:{region}")
1028 }
1029 TraceData::Obligation { obligation, .. } => format!("obligation:{obligation}"),
1030 TraceData::Worker {
1031 worker_id, job_id, ..
1032 } => format!("worker_job:{job_id}:{worker_id}"),
1033 TraceData::Time { .. } => "time".to_string(),
1034 TraceData::Timer { timer_id, .. } => format!("timer:{timer_id}"),
1035 TraceData::IoRequested { token, .. }
1036 | TraceData::IoReady { token, .. }
1037 | TraceData::IoResult { token, .. }
1038 | TraceData::IoError { token, .. } => format!("io:{token}"),
1039 TraceData::RngSeed { .. } | TraceData::RngValue { .. } => "rng".to_string(),
1040 TraceData::Checkpoint { sequence, .. } => format!("checkpoint:{sequence}"),
1041 TraceData::Monitor { monitor_ref, .. } | TraceData::Down { monitor_ref, .. } => {
1042 format!("monitor:{monitor_ref}")
1043 }
1044 TraceData::Link { link_ref, .. } | TraceData::Exit { link_ref, .. } => {
1045 format!("link:{link_ref}")
1046 }
1047 TraceData::Message(_) => "user_trace".to_string(),
1048 TraceData::Chaos {
1049 task: Some(task), ..
1050 } => format!("task:{task}"),
1051 TraceData::Chaos { task: None, .. } => "chaos".to_string(),
1052 TraceData::None => format!("kind:{}", event.kind.stable_name()),
1053 };
1054 cap_browser_trace_attribute(&raw)
1055}
1056
1057fn browser_capture_replay_key(metadata: &BrowserCaptureMetadata) -> String {
1058 format!(
1059 "{}:{}:{}:{}",
1060 match metadata.source {
1061 BrowserCaptureSource::Runtime => "runtime",
1062 BrowserCaptureSource::Time => "time",
1063 BrowserCaptureSource::Event => "event",
1064 BrowserCaptureSource::HostInput => "host_input",
1065 },
1066 metadata.host_turn_seq,
1067 metadata.source_seq,
1068 metadata.host_time_ns
1069 )
1070}
1071
1072#[must_use]
1077pub fn browser_trace_log_fields_with_capture(
1078 event: &TraceEvent,
1079 trace_id: &str,
1080 validation_failure_category: Option<&str>,
1081 capture_metadata: Option<&BrowserCaptureMetadata>,
1082) -> BTreeMap<String, String> {
1083 let capture = capture_metadata
1084 .cloned()
1085 .unwrap_or_else(|| default_browser_capture_metadata(event));
1086 let mut fields = BTreeMap::new();
1087 fields.insert(
1088 "capture_host_time_ns".to_string(),
1089 capture.host_time_ns.to_string(),
1090 );
1091 fields.insert(
1092 "capture_host_turn_seq".to_string(),
1093 capture.host_turn_seq.to_string(),
1094 );
1095 fields.insert(
1096 "capture_replay_key".to_string(),
1097 browser_capture_replay_key(&capture),
1098 );
1099 fields.insert(
1100 "capture_source".to_string(),
1101 match capture.source {
1102 BrowserCaptureSource::Runtime => "runtime".to_string(),
1103 BrowserCaptureSource::Time => "time".to_string(),
1104 BrowserCaptureSource::Event => "event".to_string(),
1105 BrowserCaptureSource::HostInput => "host_input".to_string(),
1106 },
1107 );
1108 fields.insert(
1109 "capture_source_seq".to_string(),
1110 capture.source_seq.to_string(),
1111 );
1112 fields.insert(
1113 "event_kind".to_string(),
1114 event.kind.stable_name().to_string(),
1115 );
1116 fields.insert(
1117 "schema_version".to_string(),
1118 BROWSER_TRACE_SCHEMA_VERSION.to_string(),
1119 );
1120 fields.insert("seq".to_string(), event.seq.to_string());
1121 fields.insert("time_ns".to_string(), event.time.as_nanos().to_string());
1122 fields.insert("trace_id".to_string(), trace_id.to_string());
1123 fields.insert(
1124 "sequence_group".to_string(),
1125 browser_trace_sequence_group(event),
1126 );
1127 let failure_category = validation_failure_category
1128 .filter(|category| !category.trim().is_empty())
1129 .unwrap_or("none");
1130 fields.insert(
1131 "validation_failure_category".to_string(),
1132 failure_category.to_string(),
1133 );
1134 fields.insert(
1135 "validation_status".to_string(),
1136 if failure_category == "none" {
1137 "valid".to_string()
1138 } else {
1139 "invalid".to_string()
1140 },
1141 );
1142 insert_browser_trace_payload_fields(&mut fields, event);
1143 fields
1144}
1145
1146#[must_use]
1148pub fn browser_trace_log_fields(
1149 event: &TraceEvent,
1150 trace_id: &str,
1151 validation_failure_category: Option<&str>,
1152) -> BTreeMap<String, String> {
1153 browser_trace_log_fields_with_capture(event, trace_id, validation_failure_category, None)
1154}
1155
1156impl fmt::Display for TraceEventKind {
1157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1158 f.write_str(self.stable_name())
1159 }
1160}
1161
1162#[derive(Debug, Clone, PartialEq, Eq)]
1164pub enum TraceData {
1165 None,
1167 Task {
1169 task: TaskId,
1171 region: RegionId,
1173 },
1174 Region {
1176 region: RegionId,
1178 parent: Option<RegionId>,
1180 },
1181 Obligation {
1183 obligation: ObligationId,
1185 task: TaskId,
1187 region: RegionId,
1189 kind: ObligationKind,
1191 state: ObligationState,
1193 duration_ns: Option<u64>,
1195 abort_reason: Option<ObligationAbortReason>,
1197 },
1198 Cancel {
1200 task: TaskId,
1202 region: RegionId,
1204 reason: CancelReason,
1206 },
1207 Worker {
1209 worker_id: String,
1211 job_id: u64,
1213 decision_seq: u64,
1215 replay_hash: u64,
1217 task: TaskId,
1219 region: RegionId,
1221 obligation: ObligationId,
1223 },
1224 RegionCancel {
1226 region: RegionId,
1228 reason: CancelReason,
1230 },
1231 Time {
1233 old: Time,
1235 new: Time,
1237 },
1238 Timer {
1240 timer_id: u64,
1242 deadline: Option<Time>,
1244 },
1245 IoRequested {
1247 token: u64,
1249 interest: u8,
1251 },
1252 IoReady {
1254 token: u64,
1256 readiness: u8,
1258 },
1259 IoResult {
1261 token: u64,
1263 bytes: i64,
1265 },
1266 IoError {
1268 token: u64,
1270 kind: u8,
1272 },
1273 RngSeed {
1275 seed: u64,
1277 },
1278 RngValue {
1280 value: u64,
1282 },
1283 Checkpoint {
1285 sequence: u64,
1287 active_tasks: u32,
1289 active_regions: u32,
1291 },
1292 Futurelock {
1294 task: TaskId,
1296 region: RegionId,
1298 idle_steps: u64,
1300 held: Vec<(ObligationId, ObligationKind)>,
1302 },
1303 Monitor {
1305 monitor_ref: u64,
1307 watcher: TaskId,
1309 watcher_region: RegionId,
1311 monitored: TaskId,
1313 },
1314 Down {
1318 monitor_ref: u64,
1320 watcher: TaskId,
1322 monitored: TaskId,
1324 completion_vt: Time,
1326 reason: DownReason,
1328 },
1329 Link {
1331 link_ref: u64,
1333 task_a: TaskId,
1335 region_a: RegionId,
1337 task_b: TaskId,
1339 region_b: RegionId,
1341 },
1342 Exit {
1346 link_ref: u64,
1348 from: TaskId,
1350 to: TaskId,
1352 failure_vt: Time,
1354 reason: DownReason,
1356 },
1357 Message(String),
1359 Chaos {
1361 kind: String,
1363 task: Option<TaskId>,
1365 detail: String,
1367 },
1368}
1369
1370#[derive(Debug, Clone, PartialEq, Eq)]
1372pub struct TraceEvent {
1373 pub version: u32,
1375 pub seq: u64,
1377 pub time: Time,
1379 pub logical_time: Option<LogicalTime>,
1385 pub kind: TraceEventKind,
1387 pub data: TraceData,
1389}
1390
1391impl TraceEvent {
1392 #[must_use]
1394 #[inline]
1395 pub fn new(seq: u64, time: Time, kind: TraceEventKind, data: TraceData) -> Self {
1396 Self {
1397 version: TRACE_EVENT_SCHEMA_VERSION,
1398 seq,
1399 time,
1400 logical_time: None,
1401 kind,
1402 data,
1403 }
1404 }
1405
1406 #[inline]
1408 #[must_use]
1409 pub fn with_logical_time(mut self, logical_time: LogicalTime) -> Self {
1410 self.logical_time = Some(logical_time);
1411 self
1412 }
1413
1414 #[must_use]
1416 pub fn spawn(seq: u64, time: Time, task: TaskId, region: RegionId) -> Self {
1417 Self::new(
1418 seq,
1419 time,
1420 TraceEventKind::Spawn,
1421 TraceData::Task { task, region },
1422 )
1423 }
1424
1425 #[must_use]
1427 pub fn schedule(seq: u64, time: Time, task: TaskId, region: RegionId) -> Self {
1428 Self::new(
1429 seq,
1430 time,
1431 TraceEventKind::Schedule,
1432 TraceData::Task { task, region },
1433 )
1434 }
1435
1436 #[must_use]
1438 pub fn yield_task(seq: u64, time: Time, task: TaskId, region: RegionId) -> Self {
1439 Self::new(
1440 seq,
1441 time,
1442 TraceEventKind::Yield,
1443 TraceData::Task { task, region },
1444 )
1445 }
1446
1447 #[must_use]
1449 pub fn wake(seq: u64, time: Time, task: TaskId, region: RegionId) -> Self {
1450 Self::new(
1451 seq,
1452 time,
1453 TraceEventKind::Wake,
1454 TraceData::Task { task, region },
1455 )
1456 }
1457
1458 #[must_use]
1460 pub fn poll(seq: u64, time: Time, task: TaskId, region: RegionId) -> Self {
1461 Self::new(
1462 seq,
1463 time,
1464 TraceEventKind::Poll,
1465 TraceData::Task { task, region },
1466 )
1467 }
1468
1469 #[must_use]
1471 pub fn complete(seq: u64, time: Time, task: TaskId, region: RegionId) -> Self {
1472 Self::new(
1473 seq,
1474 time,
1475 TraceEventKind::Complete,
1476 TraceData::Task { task, region },
1477 )
1478 }
1479
1480 #[must_use]
1482 pub fn cancel_request(
1483 seq: u64,
1484 time: Time,
1485 task: TaskId,
1486 region: RegionId,
1487 reason: CancelReason,
1488 ) -> Self {
1489 Self::new(
1490 seq,
1491 time,
1492 TraceEventKind::CancelRequest,
1493 TraceData::Cancel {
1494 task,
1495 region,
1496 reason,
1497 },
1498 )
1499 }
1500
1501 #[allow(clippy::too_many_arguments)]
1502 fn worker_lifecycle(
1503 seq: u64,
1504 time: Time,
1505 kind: TraceEventKind,
1506 worker_id: impl Into<String>,
1507 job_id: u64,
1508 decision_seq: u64,
1509 replay_hash: u64,
1510 task: TaskId,
1511 region: RegionId,
1512 obligation: ObligationId,
1513 ) -> Self {
1514 Self::new(
1515 seq,
1516 time,
1517 kind,
1518 TraceData::Worker {
1519 worker_id: worker_id.into(),
1520 job_id,
1521 decision_seq,
1522 replay_hash,
1523 task,
1524 region,
1525 obligation,
1526 },
1527 )
1528 }
1529
1530 #[allow(clippy::too_many_arguments)]
1532 #[must_use]
1533 pub fn worker_cancel_requested(
1534 seq: u64,
1535 time: Time,
1536 worker_id: impl Into<String>,
1537 job_id: u64,
1538 decision_seq: u64,
1539 replay_hash: u64,
1540 task: TaskId,
1541 region: RegionId,
1542 obligation: ObligationId,
1543 ) -> Self {
1544 Self::worker_lifecycle(
1545 seq,
1546 time,
1547 TraceEventKind::WorkerCancelRequested,
1548 worker_id,
1549 job_id,
1550 decision_seq,
1551 replay_hash,
1552 task,
1553 region,
1554 obligation,
1555 )
1556 }
1557
1558 #[allow(clippy::too_many_arguments)]
1560 #[must_use]
1561 pub fn worker_cancel_acknowledged(
1562 seq: u64,
1563 time: Time,
1564 worker_id: impl Into<String>,
1565 job_id: u64,
1566 decision_seq: u64,
1567 replay_hash: u64,
1568 task: TaskId,
1569 region: RegionId,
1570 obligation: ObligationId,
1571 ) -> Self {
1572 Self::worker_lifecycle(
1573 seq,
1574 time,
1575 TraceEventKind::WorkerCancelAcknowledged,
1576 worker_id,
1577 job_id,
1578 decision_seq,
1579 replay_hash,
1580 task,
1581 region,
1582 obligation,
1583 )
1584 }
1585
1586 #[allow(clippy::too_many_arguments)]
1588 #[must_use]
1589 pub fn worker_drain_started(
1590 seq: u64,
1591 time: Time,
1592 worker_id: impl Into<String>,
1593 job_id: u64,
1594 decision_seq: u64,
1595 replay_hash: u64,
1596 task: TaskId,
1597 region: RegionId,
1598 obligation: ObligationId,
1599 ) -> Self {
1600 Self::worker_lifecycle(
1601 seq,
1602 time,
1603 TraceEventKind::WorkerDrainStarted,
1604 worker_id,
1605 job_id,
1606 decision_seq,
1607 replay_hash,
1608 task,
1609 region,
1610 obligation,
1611 )
1612 }
1613
1614 #[allow(clippy::too_many_arguments)]
1616 #[must_use]
1617 pub fn worker_drain_completed(
1618 seq: u64,
1619 time: Time,
1620 worker_id: impl Into<String>,
1621 job_id: u64,
1622 decision_seq: u64,
1623 replay_hash: u64,
1624 task: TaskId,
1625 region: RegionId,
1626 obligation: ObligationId,
1627 ) -> Self {
1628 Self::worker_lifecycle(
1629 seq,
1630 time,
1631 TraceEventKind::WorkerDrainCompleted,
1632 worker_id,
1633 job_id,
1634 decision_seq,
1635 replay_hash,
1636 task,
1637 region,
1638 obligation,
1639 )
1640 }
1641
1642 #[allow(clippy::too_many_arguments)]
1644 #[must_use]
1645 pub fn worker_finalize_completed(
1646 seq: u64,
1647 time: Time,
1648 worker_id: impl Into<String>,
1649 job_id: u64,
1650 decision_seq: u64,
1651 replay_hash: u64,
1652 task: TaskId,
1653 region: RegionId,
1654 obligation: ObligationId,
1655 ) -> Self {
1656 Self::worker_lifecycle(
1657 seq,
1658 time,
1659 TraceEventKind::WorkerFinalizeCompleted,
1660 worker_id,
1661 job_id,
1662 decision_seq,
1663 replay_hash,
1664 task,
1665 region,
1666 obligation,
1667 )
1668 }
1669
1670 #[must_use]
1672 pub fn region_created(
1673 seq: u64,
1674 time: Time,
1675 region: RegionId,
1676 parent: Option<RegionId>,
1677 ) -> Self {
1678 Self::new(
1679 seq,
1680 time,
1681 TraceEventKind::RegionCreated,
1682 TraceData::Region { region, parent },
1683 )
1684 }
1685
1686 #[must_use]
1688 pub fn region_cancelled(seq: u64, time: Time, region: RegionId, reason: CancelReason) -> Self {
1689 Self::new(
1690 seq,
1691 time,
1692 TraceEventKind::RegionCancelled,
1693 TraceData::RegionCancel { region, reason },
1694 )
1695 }
1696
1697 #[must_use]
1699 pub fn time_advance(seq: u64, time: Time, old: Time, new: Time) -> Self {
1700 Self::new(
1701 seq,
1702 time,
1703 TraceEventKind::TimeAdvance,
1704 TraceData::Time { old, new },
1705 )
1706 }
1707
1708 #[must_use]
1710 pub fn timer_scheduled(seq: u64, time: Time, timer_id: u64, deadline: Time) -> Self {
1711 Self::new(
1712 seq,
1713 time,
1714 TraceEventKind::TimerScheduled,
1715 TraceData::Timer {
1716 timer_id,
1717 deadline: Some(deadline),
1718 },
1719 )
1720 }
1721
1722 #[must_use]
1724 pub fn timer_fired(seq: u64, time: Time, timer_id: u64) -> Self {
1725 Self::new(
1726 seq,
1727 time,
1728 TraceEventKind::TimerFired,
1729 TraceData::Timer {
1730 timer_id,
1731 deadline: None,
1732 },
1733 )
1734 }
1735
1736 #[must_use]
1738 pub fn timer_cancelled(seq: u64, time: Time, timer_id: u64) -> Self {
1739 Self::new(
1740 seq,
1741 time,
1742 TraceEventKind::TimerCancelled,
1743 TraceData::Timer {
1744 timer_id,
1745 deadline: None,
1746 },
1747 )
1748 }
1749
1750 #[must_use]
1752 pub fn io_requested(seq: u64, time: Time, token: u64, interest: u8) -> Self {
1753 Self::new(
1754 seq,
1755 time,
1756 TraceEventKind::IoRequested,
1757 TraceData::IoRequested { token, interest },
1758 )
1759 }
1760
1761 #[must_use]
1763 pub fn io_ready(seq: u64, time: Time, token: u64, readiness: u8) -> Self {
1764 Self::new(
1765 seq,
1766 time,
1767 TraceEventKind::IoReady,
1768 TraceData::IoReady { token, readiness },
1769 )
1770 }
1771
1772 #[must_use]
1774 pub fn io_result(seq: u64, time: Time, token: u64, bytes: i64) -> Self {
1775 Self::new(
1776 seq,
1777 time,
1778 TraceEventKind::IoResult,
1779 TraceData::IoResult { token, bytes },
1780 )
1781 }
1782
1783 #[must_use]
1785 pub fn io_error(seq: u64, time: Time, token: u64, kind: u8) -> Self {
1786 Self::new(
1787 seq,
1788 time,
1789 TraceEventKind::IoError,
1790 TraceData::IoError { token, kind },
1791 )
1792 }
1793
1794 #[must_use]
1796 pub fn rng_seed(seq: u64, time: Time, seed: u64) -> Self {
1797 Self::new(
1798 seq,
1799 time,
1800 TraceEventKind::RngSeed,
1801 TraceData::RngSeed { seed },
1802 )
1803 }
1804
1805 #[must_use]
1807 pub fn rng_value(seq: u64, time: Time, value: u64) -> Self {
1808 Self::new(
1809 seq,
1810 time,
1811 TraceEventKind::RngValue,
1812 TraceData::RngValue { value },
1813 )
1814 }
1815
1816 #[must_use]
1818 pub fn checkpoint(
1819 seq: u64,
1820 time: Time,
1821 sequence: u64,
1822 active_tasks: u32,
1823 active_regions: u32,
1824 ) -> Self {
1825 Self::new(
1826 seq,
1827 time,
1828 TraceEventKind::Checkpoint,
1829 TraceData::Checkpoint {
1830 sequence,
1831 active_tasks,
1832 active_regions,
1833 },
1834 )
1835 }
1836
1837 #[must_use]
1839 pub fn obligation_reserve(
1840 seq: u64,
1841 time: Time,
1842 obligation: ObligationId,
1843 task: TaskId,
1844 region: RegionId,
1845 kind: ObligationKind,
1846 ) -> Self {
1847 Self::new(
1848 seq,
1849 time,
1850 TraceEventKind::ObligationReserve,
1851 TraceData::Obligation {
1852 obligation,
1853 task,
1854 region,
1855 kind,
1856 state: ObligationState::Reserved,
1857 duration_ns: None,
1858 abort_reason: None,
1859 },
1860 )
1861 }
1862
1863 #[must_use]
1865 pub fn obligation_commit(
1866 seq: u64,
1867 time: Time,
1868 obligation: ObligationId,
1869 task: TaskId,
1870 region: RegionId,
1871 kind: ObligationKind,
1872 duration_ns: u64,
1873 ) -> Self {
1874 Self::new(
1875 seq,
1876 time,
1877 TraceEventKind::ObligationCommit,
1878 TraceData::Obligation {
1879 obligation,
1880 task,
1881 region,
1882 kind,
1883 state: ObligationState::Committed,
1884 duration_ns: Some(duration_ns),
1885 abort_reason: None,
1886 },
1887 )
1888 }
1889
1890 #[must_use]
1892 #[allow(clippy::too_many_arguments)]
1893 pub fn obligation_abort(
1894 seq: u64,
1895 time: Time,
1896 obligation: ObligationId,
1897 task: TaskId,
1898 region: RegionId,
1899 kind: ObligationKind,
1900 duration_ns: u64,
1901 reason: ObligationAbortReason,
1902 ) -> Self {
1903 Self::new(
1904 seq,
1905 time,
1906 TraceEventKind::ObligationAbort,
1907 TraceData::Obligation {
1908 obligation,
1909 task,
1910 region,
1911 kind,
1912 state: ObligationState::Aborted,
1913 duration_ns: Some(duration_ns),
1914 abort_reason: Some(reason),
1915 },
1916 )
1917 }
1918
1919 #[must_use]
1921 pub fn obligation_leak(
1922 seq: u64,
1923 time: Time,
1924 obligation: ObligationId,
1925 task: TaskId,
1926 region: RegionId,
1927 kind: ObligationKind,
1928 duration_ns: u64,
1929 ) -> Self {
1930 Self::new(
1931 seq,
1932 time,
1933 TraceEventKind::ObligationLeak,
1934 TraceData::Obligation {
1935 obligation,
1936 task,
1937 region,
1938 kind,
1939 state: ObligationState::Leaked,
1940 duration_ns: Some(duration_ns),
1941 abort_reason: None,
1942 },
1943 )
1944 }
1945
1946 #[must_use]
1948 pub fn monitor_created(
1949 seq: u64,
1950 time: Time,
1951 monitor_ref: u64,
1952 watcher: TaskId,
1953 watcher_region: RegionId,
1954 monitored: TaskId,
1955 ) -> Self {
1956 Self::new(
1957 seq,
1958 time,
1959 TraceEventKind::MonitorCreated,
1960 TraceData::Monitor {
1961 monitor_ref,
1962 watcher,
1963 watcher_region,
1964 monitored,
1965 },
1966 )
1967 }
1968
1969 #[must_use]
1971 pub fn monitor_dropped(
1972 seq: u64,
1973 time: Time,
1974 monitor_ref: u64,
1975 watcher: TaskId,
1976 watcher_region: RegionId,
1977 monitored: TaskId,
1978 ) -> Self {
1979 Self::new(
1980 seq,
1981 time,
1982 TraceEventKind::MonitorDropped,
1983 TraceData::Monitor {
1984 monitor_ref,
1985 watcher,
1986 watcher_region,
1987 monitored,
1988 },
1989 )
1990 }
1991
1992 #[must_use]
1994 pub fn down_delivered(
1995 seq: u64,
1996 time: Time,
1997 monitor_ref: u64,
1998 watcher: TaskId,
1999 monitored: TaskId,
2000 completion_vt: Time,
2001 reason: DownReason,
2002 ) -> Self {
2003 Self::new(
2004 seq,
2005 time,
2006 TraceEventKind::DownDelivered,
2007 TraceData::Down {
2008 monitor_ref,
2009 watcher,
2010 monitored,
2011 completion_vt,
2012 reason,
2013 },
2014 )
2015 }
2016
2017 #[must_use]
2019 pub fn link_created(
2020 seq: u64,
2021 time: Time,
2022 link_ref: u64,
2023 task_a: TaskId,
2024 region_a: RegionId,
2025 task_b: TaskId,
2026 region_b: RegionId,
2027 ) -> Self {
2028 Self::new(
2029 seq,
2030 time,
2031 TraceEventKind::LinkCreated,
2032 TraceData::Link {
2033 link_ref,
2034 task_a,
2035 region_a,
2036 task_b,
2037 region_b,
2038 },
2039 )
2040 }
2041
2042 #[must_use]
2044 pub fn link_dropped(
2045 seq: u64,
2046 time: Time,
2047 link_ref: u64,
2048 task_a: TaskId,
2049 region_a: RegionId,
2050 task_b: TaskId,
2051 region_b: RegionId,
2052 ) -> Self {
2053 Self::new(
2054 seq,
2055 time,
2056 TraceEventKind::LinkDropped,
2057 TraceData::Link {
2058 link_ref,
2059 task_a,
2060 region_a,
2061 task_b,
2062 region_b,
2063 },
2064 )
2065 }
2066
2067 #[must_use]
2069 pub fn exit_delivered(
2070 seq: u64,
2071 time: Time,
2072 link_ref: u64,
2073 from: TaskId,
2074 to: TaskId,
2075 failure_vt: Time,
2076 reason: DownReason,
2077 ) -> Self {
2078 Self::new(
2079 seq,
2080 time,
2081 TraceEventKind::ExitDelivered,
2082 TraceData::Exit {
2083 link_ref,
2084 from,
2085 to,
2086 failure_vt,
2087 reason,
2088 },
2089 )
2090 }
2091
2092 #[must_use]
2094 pub fn user_trace(seq: u64, time: Time, message: impl Into<String>) -> Self {
2095 Self::new(
2096 seq,
2097 time,
2098 TraceEventKind::UserTrace,
2099 TraceData::Message(message.into()),
2100 )
2101 }
2102}
2103
2104impl fmt::Display for TraceEvent {
2105 #[allow(clippy::too_many_lines)]
2106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2107 write!(f, "[{:06}] {} {}", self.seq, self.time, self.kind)?;
2108 if let Some(ref lt) = self.logical_time {
2109 write!(f, " @{lt:?}")?;
2110 }
2111 match &self.data {
2112 TraceData::None => {}
2113 TraceData::Task { task, region } => write!(f, " {task} in {region}")?,
2114 TraceData::Region { region, parent } => {
2115 write!(f, " {region}")?;
2116 if let Some(p) = parent {
2117 write!(f, " (parent: {p})")?;
2118 }
2119 }
2120 TraceData::Obligation {
2121 obligation,
2122 task,
2123 region,
2124 kind,
2125 state,
2126 duration_ns,
2127 abort_reason,
2128 } => {
2129 write!(
2130 f,
2131 " {obligation} {kind:?} {state:?} holder={task} region={region}"
2132 )?;
2133 if let Some(duration) = duration_ns {
2134 write!(f, " duration={duration}ns")?;
2135 }
2136 if let Some(reason) = abort_reason {
2137 write!(f, " abort_reason={reason}")?;
2138 }
2139 }
2140 TraceData::Cancel {
2141 task,
2142 region,
2143 reason,
2144 } => write!(f, " {task} in {region} reason={reason}")?,
2145 TraceData::Worker {
2146 worker_id,
2147 job_id,
2148 decision_seq,
2149 replay_hash,
2150 task,
2151 region,
2152 obligation,
2153 } => write!(
2154 f,
2155 " worker={worker_id} job_id={job_id} {task} in {region} obligation={obligation} decision_seq={decision_seq} replay_hash={replay_hash}"
2156 )?,
2157 TraceData::RegionCancel { region, reason } => {
2158 write!(f, " {region} reason={reason}")?;
2159 }
2160 TraceData::Time { old, new } => write!(f, " {old} -> {new}")?,
2161 TraceData::Timer { timer_id, deadline } => {
2162 write!(f, " timer={timer_id}")?;
2163 if let Some(dl) = deadline {
2164 write!(f, " deadline={dl}")?;
2165 }
2166 }
2167 TraceData::IoRequested { token, interest } => {
2168 write!(f, " io_requested token={token} interest={interest}")?;
2169 }
2170 TraceData::IoReady { token, readiness } => {
2171 write!(f, " io_ready token={token} readiness={readiness}")?;
2172 }
2173 TraceData::IoResult { token, bytes } => {
2174 write!(f, " io_result token={token} bytes={bytes}")?;
2175 }
2176 TraceData::IoError { token, kind } => {
2177 write!(f, " io_error token={token} kind={kind}")?;
2178 }
2179 TraceData::RngSeed { seed } => write!(f, " rng_seed={seed}")?,
2180 TraceData::RngValue { value } => write!(f, " rng_value={value}")?,
2181 TraceData::Checkpoint {
2182 sequence,
2183 active_tasks,
2184 active_regions,
2185 } => write!(
2186 f,
2187 " checkpoint seq={sequence} tasks={active_tasks} regions={active_regions}"
2188 )?,
2189 TraceData::Futurelock {
2190 task,
2191 region,
2192 idle_steps,
2193 held,
2194 } => {
2195 write!(f, " futurelock: {task} in {region} idle={idle_steps}")?;
2196 write!(f, " held=[")?;
2197 for (i, (oid, kind)) in held.iter().enumerate() {
2198 if i > 0 {
2199 write!(f, ", ")?;
2200 }
2201 write!(f, "{oid}:{kind:?}")?;
2202 }
2203 write!(f, "]")?;
2204 }
2205 TraceData::Monitor {
2206 monitor_ref,
2207 watcher,
2208 watcher_region,
2209 monitored,
2210 } => write!(
2211 f,
2212 " monitor_ref={monitor_ref} watcher={watcher} watcher_region={watcher_region} monitored={monitored}"
2213 )?,
2214 TraceData::Down {
2215 monitor_ref,
2216 watcher,
2217 monitored,
2218 completion_vt,
2219 reason,
2220 } => write!(
2221 f,
2222 " down monitor_ref={monitor_ref} watcher={watcher} monitored={monitored} completion_vt={completion_vt} reason={reason}"
2223 )?,
2224 TraceData::Link {
2225 link_ref,
2226 task_a,
2227 region_a,
2228 task_b,
2229 region_b,
2230 } => write!(
2231 f,
2232 " link_ref={link_ref} a={task_a} region_a={region_a} b={task_b} region_b={region_b}"
2233 )?,
2234 TraceData::Exit {
2235 link_ref,
2236 from,
2237 to,
2238 failure_vt,
2239 reason,
2240 } => write!(
2241 f,
2242 " exit link_ref={link_ref} from={from} to={to} failure_vt={failure_vt} reason={reason}"
2243 )?,
2244 TraceData::Message(msg) => write!(f, " \"{msg}\"")?,
2245 TraceData::Chaos { kind, task, detail } => {
2246 write!(f, " chaos:{kind}")?;
2247 if let Some(t) = task {
2248 write!(f, " task={t}")?;
2249 }
2250 write!(f, " {detail}")?;
2251 }
2252 }
2253 Ok(())
2254 }
2255}
2256
2257#[cfg(test)]
2258mod tests {
2259 use super::*;
2260 use crate::monitor::DownReason;
2261 use crate::record::{ObligationAbortReason, ObligationKind, ObligationState};
2262 use crate::trace::distributed::LamportTime;
2263 use crate::types::CancelReason;
2264 use serde_json::Value;
2265 use std::collections::BTreeSet;
2266
2267 fn task(n: u32) -> TaskId {
2268 TaskId::new_for_test(n, 1)
2269 }
2270 fn region(n: u32) -> RegionId {
2271 RegionId::new_for_test(n, 1)
2272 }
2273 fn obligation(n: u32) -> ObligationId {
2274 ObligationId::new_for_test(n, 1)
2275 }
2276
2277 fn scrub_browser_trace_fields(fields: &std::collections::BTreeMap<String, String>) -> Value {
2278 let mut value = serde_json::to_value(fields).expect("serialize browser trace fields");
2279 let obj = value
2280 .as_object_mut()
2281 .expect("browser trace fields serialize to an object");
2282
2283 for key in [
2284 "capture_host_time_ns",
2285 "capture_replay_key",
2286 "completion_vt",
2287 "deadline",
2288 "failure_vt",
2289 "from",
2290 "monitored",
2291 "new",
2292 "old",
2293 "parent",
2294 "region_a",
2295 "region_b",
2296 "seq",
2297 "task_a",
2298 "task_b",
2299 "to",
2300 "time_ns",
2301 "trace_id",
2302 "task",
2303 "region",
2304 "obligation",
2305 "sequence_group",
2306 "watcher",
2307 "watcher_region",
2308 ] {
2309 if obj.contains_key(key) {
2310 obj.insert(key.to_string(), Value::String(format!("[{key}]")));
2311 }
2312 }
2313
2314 value
2315 }
2316
2317 #[test]
2320 fn trace_event_version_is_set() {
2321 let event = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
2322 assert_eq!(event.version, TRACE_EVENT_SCHEMA_VERSION);
2323 }
2324
2325 #[test]
2326 fn trace_event_kind_stable_names_are_unique() {
2327 let mut names = BTreeSet::new();
2328 for kind in TraceEventKind::ALL {
2329 assert!(names.insert(kind.stable_name()));
2330 }
2331 }
2332
2333 #[test]
2334 fn trace_event_taxonomy_is_documented() {
2335 const DOC: &str = include_str!("../../docs/spork_deterministic_ordering.md");
2336 for kind in TraceEventKind::ALL {
2337 let marker = format!("- `{}` => `{}`", kind.stable_name(), kind.required_fields());
2338 assert!(
2339 DOC.contains(&marker),
2340 "missing taxonomy entry in docs/spork_deterministic_ordering.md for {}",
2341 kind.stable_name()
2342 );
2343 }
2344 }
2345
2346 #[test]
2347 fn all_array_has_41_kinds() {
2348 assert_eq!(TraceEventKind::ALL.len(), 41);
2349 }
2350
2351 #[test]
2352 fn all_kinds_are_distinct() {
2353 let set: BTreeSet<TraceEventKind> = TraceEventKind::ALL.iter().copied().collect();
2354 assert_eq!(set.len(), TraceEventKind::ALL.len());
2355 }
2356
2357 #[test]
2358 fn display_delegates_to_stable_name() {
2359 for kind in TraceEventKind::ALL {
2360 assert_eq!(format!("{kind}"), kind.stable_name());
2361 }
2362 }
2363
2364 #[test]
2365 fn kind_ord_is_consistent_with_eq() {
2366 for a in TraceEventKind::ALL {
2367 for b in TraceEventKind::ALL {
2368 if a == b {
2369 assert_eq!(a.cmp(&b), std::cmp::Ordering::Equal);
2370 } else {
2371 assert_ne!(a.cmp(&b), std::cmp::Ordering::Equal);
2372 }
2373 }
2374 }
2375 }
2376
2377 #[test]
2378 fn required_fields_non_empty_for_all() {
2379 for kind in TraceEventKind::ALL {
2380 assert!(
2381 !kind.required_fields().is_empty(),
2382 "required_fields empty for {kind:?}"
2383 );
2384 }
2385 }
2386
2387 #[test]
2390 fn spawn_constructor() {
2391 let e = TraceEvent::spawn(1, Time::ZERO, task(10), region(20));
2392 assert_eq!(e.kind, TraceEventKind::Spawn);
2393 assert_eq!(e.seq, 1);
2394 assert_eq!(
2395 e.data,
2396 TraceData::Task {
2397 task: task(10),
2398 region: region(20)
2399 }
2400 );
2401 }
2402
2403 #[test]
2404 fn schedule_constructor() {
2405 let e = TraceEvent::schedule(2, Time::from_nanos(100), task(1), region(2));
2406 assert_eq!(e.kind, TraceEventKind::Schedule);
2407 assert_eq!(
2408 e.data,
2409 TraceData::Task {
2410 task: task(1),
2411 region: region(2)
2412 }
2413 );
2414 }
2415
2416 #[test]
2417 fn yield_task_constructor() {
2418 let e = TraceEvent::yield_task(3, Time::ZERO, task(5), region(6));
2419 assert_eq!(e.kind, TraceEventKind::Yield);
2420 assert_eq!(
2421 e.data,
2422 TraceData::Task {
2423 task: task(5),
2424 region: region(6)
2425 }
2426 );
2427 }
2428
2429 #[test]
2430 fn wake_constructor() {
2431 let e = TraceEvent::wake(4, Time::ZERO, task(7), region(8));
2432 assert_eq!(e.kind, TraceEventKind::Wake);
2433 assert_eq!(
2434 e.data,
2435 TraceData::Task {
2436 task: task(7),
2437 region: region(8)
2438 }
2439 );
2440 }
2441
2442 #[test]
2443 fn poll_constructor() {
2444 let e = TraceEvent::poll(5, Time::ZERO, task(9), region(10));
2445 assert_eq!(e.kind, TraceEventKind::Poll);
2446 assert_eq!(
2447 e.data,
2448 TraceData::Task {
2449 task: task(9),
2450 region: region(10)
2451 }
2452 );
2453 }
2454
2455 #[test]
2456 fn complete_constructor() {
2457 let e = TraceEvent::complete(6, Time::ZERO, task(11), region(12));
2458 assert_eq!(e.kind, TraceEventKind::Complete);
2459 assert_eq!(
2460 e.data,
2461 TraceData::Task {
2462 task: task(11),
2463 region: region(12)
2464 }
2465 );
2466 }
2467
2468 #[test]
2469 fn cancel_request_constructor() {
2470 let e =
2471 TraceEvent::cancel_request(7, Time::ZERO, task(1), region(2), CancelReason::timeout());
2472 assert_eq!(e.kind, TraceEventKind::CancelRequest);
2473 match &e.data {
2474 TraceData::Cancel {
2475 task: t,
2476 region: r,
2477 reason,
2478 } => {
2479 assert_eq!(*t, task(1));
2480 assert_eq!(*r, region(2));
2481 assert_eq!(reason.kind(), crate::types::CancelKind::Timeout);
2482 }
2483 other => panic!("expected Cancel, got {other:?}"),
2484 }
2485 }
2486
2487 #[test]
2488 fn region_created_constructor_with_parent() {
2489 let e = TraceEvent::region_created(8, Time::ZERO, region(3), Some(region(1)));
2490 assert_eq!(e.kind, TraceEventKind::RegionCreated);
2491 assert_eq!(
2492 e.data,
2493 TraceData::Region {
2494 region: region(3),
2495 parent: Some(region(1))
2496 }
2497 );
2498 }
2499
2500 #[test]
2501 fn region_created_constructor_without_parent() {
2502 let e = TraceEvent::region_created(9, Time::ZERO, region(3), None);
2503 assert_eq!(e.kind, TraceEventKind::RegionCreated);
2504 assert_eq!(
2505 e.data,
2506 TraceData::Region {
2507 region: region(3),
2508 parent: None
2509 }
2510 );
2511 }
2512
2513 #[test]
2514 fn region_cancelled_constructor() {
2515 let e = TraceEvent::region_cancelled(10, Time::ZERO, region(5), CancelReason::shutdown());
2516 assert_eq!(e.kind, TraceEventKind::RegionCancelled);
2517 match &e.data {
2518 TraceData::RegionCancel { region: r, .. } => assert_eq!(*r, region(5)),
2519 other => panic!("expected RegionCancel, got {other:?}"),
2520 }
2521 }
2522
2523 #[test]
2524 fn time_advance_constructor() {
2525 let e =
2526 TraceEvent::time_advance(11, Time::ZERO, Time::from_nanos(0), Time::from_nanos(100));
2527 assert_eq!(e.kind, TraceEventKind::TimeAdvance);
2528 assert_eq!(
2529 e.data,
2530 TraceData::Time {
2531 old: Time::from_nanos(0),
2532 new: Time::from_nanos(100)
2533 }
2534 );
2535 }
2536
2537 #[test]
2538 fn timer_scheduled_constructor() {
2539 let e = TraceEvent::timer_scheduled(12, Time::ZERO, 42, Time::from_millis(500));
2540 assert_eq!(e.kind, TraceEventKind::TimerScheduled);
2541 assert_eq!(
2542 e.data,
2543 TraceData::Timer {
2544 timer_id: 42,
2545 deadline: Some(Time::from_millis(500))
2546 }
2547 );
2548 }
2549
2550 #[test]
2551 fn timer_fired_constructor() {
2552 let e = TraceEvent::timer_fired(13, Time::ZERO, 42);
2553 assert_eq!(e.kind, TraceEventKind::TimerFired);
2554 assert_eq!(
2555 e.data,
2556 TraceData::Timer {
2557 timer_id: 42,
2558 deadline: None
2559 }
2560 );
2561 }
2562
2563 #[test]
2564 fn timer_cancelled_constructor() {
2565 let e = TraceEvent::timer_cancelled(14, Time::ZERO, 42);
2566 assert_eq!(e.kind, TraceEventKind::TimerCancelled);
2567 assert_eq!(
2568 e.data,
2569 TraceData::Timer {
2570 timer_id: 42,
2571 deadline: None
2572 }
2573 );
2574 }
2575
2576 #[test]
2577 fn io_requested_constructor() {
2578 let e = TraceEvent::io_requested(15, Time::ZERO, 99, 0x03);
2579 assert_eq!(e.kind, TraceEventKind::IoRequested);
2580 assert_eq!(
2581 e.data,
2582 TraceData::IoRequested {
2583 token: 99,
2584 interest: 0x03
2585 }
2586 );
2587 }
2588
2589 #[test]
2590 fn io_ready_constructor() {
2591 let e = TraceEvent::io_ready(16, Time::ZERO, 99, 0x01);
2592 assert_eq!(e.kind, TraceEventKind::IoReady);
2593 assert_eq!(
2594 e.data,
2595 TraceData::IoReady {
2596 token: 99,
2597 readiness: 0x01
2598 }
2599 );
2600 }
2601
2602 #[test]
2603 fn io_result_constructor() {
2604 let e = TraceEvent::io_result(17, Time::ZERO, 99, 1024);
2605 assert_eq!(e.kind, TraceEventKind::IoResult);
2606 assert_eq!(
2607 e.data,
2608 TraceData::IoResult {
2609 token: 99,
2610 bytes: 1024
2611 }
2612 );
2613 }
2614
2615 #[test]
2616 fn io_result_negative_bytes() {
2617 let e = TraceEvent::io_result(18, Time::ZERO, 99, -1);
2618 assert_eq!(
2619 e.data,
2620 TraceData::IoResult {
2621 token: 99,
2622 bytes: -1
2623 }
2624 );
2625 }
2626
2627 #[test]
2628 fn io_error_constructor() {
2629 let e = TraceEvent::io_error(19, Time::ZERO, 99, 13);
2630 assert_eq!(e.kind, TraceEventKind::IoError);
2631 assert_eq!(
2632 e.data,
2633 TraceData::IoError {
2634 token: 99,
2635 kind: 13
2636 }
2637 );
2638 }
2639
2640 #[test]
2641 fn rng_seed_constructor() {
2642 let e = TraceEvent::rng_seed(20, Time::ZERO, 0xDEAD_BEEF);
2643 assert_eq!(e.kind, TraceEventKind::RngSeed);
2644 assert_eq!(e.data, TraceData::RngSeed { seed: 0xDEAD_BEEF });
2645 }
2646
2647 #[test]
2648 fn rng_value_constructor() {
2649 let e = TraceEvent::rng_value(21, Time::ZERO, 42);
2650 assert_eq!(e.kind, TraceEventKind::RngValue);
2651 assert_eq!(e.data, TraceData::RngValue { value: 42 });
2652 }
2653
2654 #[test]
2655 fn checkpoint_constructor() {
2656 let e = TraceEvent::checkpoint(22, Time::ZERO, 7, 3, 2);
2657 assert_eq!(e.kind, TraceEventKind::Checkpoint);
2658 assert_eq!(
2659 e.data,
2660 TraceData::Checkpoint {
2661 sequence: 7,
2662 active_tasks: 3,
2663 active_regions: 2
2664 }
2665 );
2666 }
2667
2668 #[test]
2669 fn obligation_reserve_constructor() {
2670 let e = TraceEvent::obligation_reserve(
2671 23,
2672 Time::ZERO,
2673 obligation(1),
2674 task(2),
2675 region(3),
2676 ObligationKind::SendPermit,
2677 );
2678 assert_eq!(e.kind, TraceEventKind::ObligationReserve);
2679 match &e.data {
2680 TraceData::Obligation {
2681 state,
2682 duration_ns,
2683 abort_reason,
2684 ..
2685 } => {
2686 assert_eq!(*state, ObligationState::Reserved);
2687 assert_eq!(*duration_ns, None);
2688 assert_eq!(*abort_reason, None);
2689 }
2690 other => panic!("expected Obligation, got {other:?}"),
2691 }
2692 }
2693
2694 #[test]
2695 fn obligation_commit_constructor() {
2696 let e = TraceEvent::obligation_commit(
2697 24,
2698 Time::ZERO,
2699 obligation(1),
2700 task(2),
2701 region(3),
2702 ObligationKind::Ack,
2703 5000,
2704 );
2705 assert_eq!(e.kind, TraceEventKind::ObligationCommit);
2706 match &e.data {
2707 TraceData::Obligation {
2708 state,
2709 duration_ns,
2710 abort_reason,
2711 ..
2712 } => {
2713 assert_eq!(*state, ObligationState::Committed);
2714 assert_eq!(*duration_ns, Some(5000));
2715 assert_eq!(*abort_reason, None);
2716 }
2717 other => panic!("expected Obligation, got {other:?}"),
2718 }
2719 }
2720
2721 #[test]
2722 fn obligation_abort_constructor() {
2723 let e = TraceEvent::obligation_abort(
2724 25,
2725 Time::ZERO,
2726 obligation(1),
2727 task(2),
2728 region(3),
2729 ObligationKind::Lease,
2730 3000,
2731 ObligationAbortReason::Cancel,
2732 );
2733 assert_eq!(e.kind, TraceEventKind::ObligationAbort);
2734 match &e.data {
2735 TraceData::Obligation {
2736 state,
2737 duration_ns,
2738 abort_reason,
2739 ..
2740 } => {
2741 assert_eq!(*state, ObligationState::Aborted);
2742 assert_eq!(*duration_ns, Some(3000));
2743 assert_eq!(*abort_reason, Some(ObligationAbortReason::Cancel));
2744 }
2745 other => panic!("expected Obligation, got {other:?}"),
2746 }
2747 }
2748
2749 #[test]
2750 fn obligation_leak_constructor() {
2751 let e = TraceEvent::obligation_leak(
2752 26,
2753 Time::ZERO,
2754 obligation(1),
2755 task(2),
2756 region(3),
2757 ObligationKind::IoOp,
2758 9000,
2759 );
2760 assert_eq!(e.kind, TraceEventKind::ObligationLeak);
2761 match &e.data {
2762 TraceData::Obligation {
2763 state,
2764 duration_ns,
2765 abort_reason,
2766 ..
2767 } => {
2768 assert_eq!(*state, ObligationState::Leaked);
2769 assert_eq!(*duration_ns, Some(9000));
2770 assert_eq!(*abort_reason, None);
2771 }
2772 other => panic!("expected Obligation, got {other:?}"),
2773 }
2774 }
2775
2776 #[test]
2777 fn monitor_created_constructor() {
2778 let e = TraceEvent::monitor_created(27, Time::ZERO, 100, task(1), region(2), task(3));
2779 assert_eq!(e.kind, TraceEventKind::MonitorCreated);
2780 assert_eq!(
2781 e.data,
2782 TraceData::Monitor {
2783 monitor_ref: 100,
2784 watcher: task(1),
2785 watcher_region: region(2),
2786 monitored: task(3),
2787 }
2788 );
2789 }
2790
2791 #[test]
2792 fn monitor_dropped_constructor() {
2793 let e = TraceEvent::monitor_dropped(28, Time::ZERO, 100, task(1), region(2), task(3));
2794 assert_eq!(e.kind, TraceEventKind::MonitorDropped);
2795 assert_eq!(
2796 e.data,
2797 TraceData::Monitor {
2798 monitor_ref: 100,
2799 watcher: task(1),
2800 watcher_region: region(2),
2801 monitored: task(3),
2802 }
2803 );
2804 }
2805
2806 #[test]
2807 fn down_delivered_constructor() {
2808 let e = TraceEvent::down_delivered(
2809 29,
2810 Time::ZERO,
2811 100,
2812 task(1),
2813 task(3),
2814 Time::from_nanos(500),
2815 DownReason::Normal,
2816 );
2817 assert_eq!(e.kind, TraceEventKind::DownDelivered);
2818 assert_eq!(
2819 e.data,
2820 TraceData::Down {
2821 monitor_ref: 100,
2822 watcher: task(1),
2823 monitored: task(3),
2824 completion_vt: Time::from_nanos(500),
2825 reason: DownReason::Normal,
2826 }
2827 );
2828 }
2829
2830 #[test]
2831 fn link_created_constructor() {
2832 let e =
2833 TraceEvent::link_created(30, Time::ZERO, 200, task(1), region(2), task(3), region(4));
2834 assert_eq!(e.kind, TraceEventKind::LinkCreated);
2835 assert_eq!(
2836 e.data,
2837 TraceData::Link {
2838 link_ref: 200,
2839 task_a: task(1),
2840 region_a: region(2),
2841 task_b: task(3),
2842 region_b: region(4),
2843 }
2844 );
2845 }
2846
2847 #[test]
2848 fn link_dropped_constructor() {
2849 let e =
2850 TraceEvent::link_dropped(31, Time::ZERO, 200, task(1), region(2), task(3), region(4));
2851 assert_eq!(e.kind, TraceEventKind::LinkDropped);
2852 assert_eq!(
2853 e.data,
2854 TraceData::Link {
2855 link_ref: 200,
2856 task_a: task(1),
2857 region_a: region(2),
2858 task_b: task(3),
2859 region_b: region(4),
2860 }
2861 );
2862 }
2863
2864 #[test]
2865 fn exit_delivered_constructor() {
2866 let e = TraceEvent::exit_delivered(
2867 32,
2868 Time::ZERO,
2869 200,
2870 task(1),
2871 task(3),
2872 Time::from_nanos(999),
2873 DownReason::Normal,
2874 );
2875 assert_eq!(e.kind, TraceEventKind::ExitDelivered);
2876 assert_eq!(
2877 e.data,
2878 TraceData::Exit {
2879 link_ref: 200,
2880 from: task(1),
2881 to: task(3),
2882 failure_vt: Time::from_nanos(999),
2883 reason: DownReason::Normal,
2884 }
2885 );
2886 }
2887
2888 #[test]
2889 fn user_trace_constructor() {
2890 let e = TraceEvent::user_trace(33, Time::ZERO, "hello");
2891 assert_eq!(e.kind, TraceEventKind::UserTrace);
2892 assert_eq!(e.data, TraceData::Message("hello".into()));
2893 }
2894
2895 #[test]
2896 fn user_trace_accepts_string() {
2897 let e = TraceEvent::user_trace(34, Time::ZERO, String::from("world"));
2898 assert_eq!(e.data, TraceData::Message("world".into()));
2899 }
2900
2901 #[test]
2902 fn worker_lifecycle_constructors_preserve_payload_shape() {
2903 let e = TraceEvent::worker_cancel_requested(
2904 35,
2905 Time::ZERO,
2906 "worker-a",
2907 77,
2908 91,
2909 0x00C0_FFEE,
2910 task(9),
2911 region(10),
2912 obligation(11),
2913 );
2914 assert_eq!(e.kind, TraceEventKind::WorkerCancelRequested);
2915 assert_eq!(
2916 e.data,
2917 TraceData::Worker {
2918 worker_id: "worker-a".into(),
2919 job_id: 77,
2920 decision_seq: 91,
2921 replay_hash: 0x00C0_FFEE,
2922 task: task(9),
2923 region: region(10),
2924 obligation: obligation(11),
2925 }
2926 );
2927 }
2928
2929 #[test]
2932 fn with_logical_time_sets_field() {
2933 let lt = LogicalTime::Lamport(LamportTime::from_raw(42));
2934 let e = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None)
2935 .with_logical_time(lt);
2936 assert_eq!(
2937 e.logical_time,
2938 Some(LogicalTime::Lamport(LamportTime::from_raw(42)))
2939 );
2940 }
2941
2942 #[test]
2943 fn default_logical_time_is_none() {
2944 let e = TraceEvent::new(1, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
2945 assert_eq!(e.logical_time, None);
2946 }
2947
2948 #[test]
2951 fn display_task_event() {
2952 let e = TraceEvent::spawn(1, Time::ZERO, task(10), region(20));
2953 let s = format!("{e}");
2954 assert!(s.contains("spawn"), "expected 'spawn' in {s}");
2955 assert!(s.contains("[000001]"), "expected seq in {s}");
2956 }
2957
2958 #[test]
2959 fn display_region_with_parent() {
2960 let e = TraceEvent::region_created(2, Time::ZERO, region(3), Some(region(1)));
2961 let s = format!("{e}");
2962 assert!(s.contains("region_created"), "expected kind in {s}");
2963 assert!(s.contains("parent"), "expected parent in {s}");
2964 }
2965
2966 #[test]
2967 fn display_region_without_parent() {
2968 let e = TraceEvent::region_created(3, Time::ZERO, region(3), None);
2969 let s = format!("{e}");
2970 assert!(s.contains("region_created"), "expected kind in {s}");
2971 assert!(!s.contains("parent"), "should not contain parent: {s}");
2972 }
2973
2974 #[test]
2975 fn display_obligation_with_duration_and_abort() {
2976 let e = TraceEvent::obligation_abort(
2977 4,
2978 Time::ZERO,
2979 obligation(1),
2980 task(2),
2981 region(3),
2982 ObligationKind::Lease,
2983 5000,
2984 ObligationAbortReason::Error,
2985 );
2986 let s = format!("{e}");
2987 assert!(s.contains("obligation_abort"), "expected kind in {s}");
2988 assert!(s.contains("duration=5000ns"), "expected duration in {s}");
2989 assert!(s.contains("abort_reason="), "expected abort_reason in {s}");
2990 }
2991
2992 #[test]
2993 fn display_obligation_reserve_no_duration() {
2994 let e = TraceEvent::obligation_reserve(
2995 5,
2996 Time::ZERO,
2997 obligation(1),
2998 task(2),
2999 region(3),
3000 ObligationKind::SendPermit,
3001 );
3002 let s = format!("{e}");
3003 assert!(
3004 !s.contains("duration="),
3005 "reserve should not show duration: {s}"
3006 );
3007 assert!(
3008 !s.contains("abort_reason="),
3009 "reserve should not show abort_reason: {s}"
3010 );
3011 }
3012
3013 #[test]
3014 fn display_cancel_event() {
3015 let e =
3016 TraceEvent::cancel_request(6, Time::ZERO, task(1), region(2), CancelReason::timeout());
3017 let s = format!("{e}");
3018 assert!(s.contains("cancel_request"), "expected kind in {s}");
3019 assert!(s.contains("reason="), "expected reason in {s}");
3020 }
3021
3022 #[test]
3023 fn display_region_cancel() {
3024 let e = TraceEvent::region_cancelled(7, Time::ZERO, region(5), CancelReason::shutdown());
3025 let s = format!("{e}");
3026 assert!(s.contains("region_cancelled"), "expected kind in {s}");
3027 assert!(s.contains("reason="), "expected reason in {s}");
3028 }
3029
3030 #[test]
3031 fn display_time_advance() {
3032 let e = TraceEvent::time_advance(8, Time::ZERO, Time::from_nanos(0), Time::from_nanos(100));
3033 let s = format!("{e}");
3034 assert!(s.contains("time_advance"), "expected kind in {s}");
3035 assert!(s.contains("->"), "expected arrow in {s}");
3036 }
3037
3038 #[test]
3039 fn display_timer_with_deadline() {
3040 let e = TraceEvent::timer_scheduled(9, Time::ZERO, 42, Time::from_millis(500));
3041 let s = format!("{e}");
3042 assert!(s.contains("timer=42"), "expected timer id in {s}");
3043 assert!(s.contains("deadline="), "expected deadline in {s}");
3044 }
3045
3046 #[test]
3047 fn display_timer_without_deadline() {
3048 let e = TraceEvent::timer_fired(10, Time::ZERO, 42);
3049 let s = format!("{e}");
3050 assert!(s.contains("timer=42"), "expected timer id in {s}");
3051 assert!(!s.contains("deadline="), "should not show deadline: {s}");
3052 }
3053
3054 #[test]
3055 fn display_io_requested() {
3056 let e = TraceEvent::io_requested(11, Time::ZERO, 99, 0x03);
3057 let s = format!("{e}");
3058 assert!(s.contains("io_requested"), "expected kind in {s}");
3059 assert!(s.contains("token=99"), "expected token in {s}");
3060 assert!(s.contains("interest=3"), "expected interest in {s}");
3061 }
3062
3063 #[test]
3064 fn display_io_ready() {
3065 let e = TraceEvent::io_ready(12, Time::ZERO, 99, 0x01);
3066 let s = format!("{e}");
3067 assert!(s.contains("io_ready"), "expected kind in {s}");
3068 assert!(s.contains("readiness=1"), "expected readiness in {s}");
3069 }
3070
3071 #[test]
3072 fn display_io_result() {
3073 let e = TraceEvent::io_result(13, Time::ZERO, 99, 1024);
3074 let s = format!("{e}");
3075 assert!(s.contains("io_result"), "expected kind in {s}");
3076 assert!(s.contains("bytes=1024"), "expected bytes in {s}");
3077 }
3078
3079 #[test]
3080 fn display_io_error() {
3081 let e = TraceEvent::io_error(14, Time::ZERO, 99, 13);
3082 let s = format!("{e}");
3083 assert!(s.contains("io_error"), "expected kind in {s}");
3084 assert!(s.contains("kind=13"), "expected kind in {s}");
3085 }
3086
3087 #[test]
3088 fn display_rng_seed() {
3089 let e = TraceEvent::rng_seed(15, Time::ZERO, 0xCAFE);
3090 let s = format!("{e}");
3091 assert!(s.contains("rng_seed=51966"), "expected seed in {s}");
3092 }
3093
3094 #[test]
3095 fn display_rng_value() {
3096 let e = TraceEvent::rng_value(16, Time::ZERO, 42);
3097 let s = format!("{e}");
3098 assert!(s.contains("rng_value=42"), "expected value in {s}");
3099 }
3100
3101 #[test]
3102 fn display_checkpoint() {
3103 let e = TraceEvent::checkpoint(17, Time::ZERO, 7, 3, 2);
3104 let s = format!("{e}");
3105 assert!(s.contains("checkpoint"), "expected kind in {s}");
3106 assert!(s.contains("seq=7"), "expected seq in {s}");
3107 assert!(s.contains("tasks=3"), "expected tasks in {s}");
3108 assert!(s.contains("regions=2"), "expected regions in {s}");
3109 }
3110
3111 #[test]
3112 fn display_futurelock_empty_held() {
3113 let e = TraceEvent::new(
3114 18,
3115 Time::ZERO,
3116 TraceEventKind::FuturelockDetected,
3117 TraceData::Futurelock {
3118 task: task(1),
3119 region: region(2),
3120 idle_steps: 10,
3121 held: vec![],
3122 },
3123 );
3124 let s = format!("{e}");
3125 assert!(s.contains("futurelock"), "expected kind in {s}");
3126 assert!(s.contains("idle=10"), "expected idle in {s}");
3127 assert!(s.contains("held=[]"), "expected empty held in {s}");
3128 }
3129
3130 #[test]
3131 fn display_futurelock_with_held() {
3132 let e = TraceEvent::new(
3133 19,
3134 Time::ZERO,
3135 TraceEventKind::FuturelockDetected,
3136 TraceData::Futurelock {
3137 task: task(1),
3138 region: region(2),
3139 idle_steps: 5,
3140 held: vec![(obligation(10), ObligationKind::SendPermit)],
3141 },
3142 );
3143 let s = format!("{e}");
3144 assert!(s.contains("held=["), "expected held in {s}");
3145 assert!(s.contains("SendPermit"), "expected kind in {s}");
3146 }
3147
3148 #[test]
3149 fn display_monitor() {
3150 let e = TraceEvent::monitor_created(20, Time::ZERO, 100, task(1), region(2), task(3));
3151 let s = format!("{e}");
3152 assert!(s.contains("monitor_ref=100"), "expected ref in {s}");
3153 }
3154
3155 #[test]
3156 fn display_down() {
3157 let e = TraceEvent::down_delivered(
3158 21,
3159 Time::ZERO,
3160 100,
3161 task(1),
3162 task(3),
3163 Time::from_nanos(500),
3164 DownReason::Normal,
3165 );
3166 let s = format!("{e}");
3167 assert!(s.contains("down"), "expected down in {s}");
3168 assert!(s.contains("monitor_ref=100"), "expected ref in {s}");
3169 }
3170
3171 #[test]
3172 fn display_link() {
3173 let e =
3174 TraceEvent::link_created(22, Time::ZERO, 200, task(1), region(2), task(3), region(4));
3175 let s = format!("{e}");
3176 assert!(s.contains("link_ref=200"), "expected ref in {s}");
3177 }
3178
3179 #[test]
3180 fn display_exit() {
3181 let e = TraceEvent::exit_delivered(
3182 23,
3183 Time::ZERO,
3184 200,
3185 task(1),
3186 task(3),
3187 Time::from_nanos(999),
3188 DownReason::Normal,
3189 );
3190 let s = format!("{e}");
3191 assert!(s.contains("exit"), "expected exit in {s}");
3192 assert!(s.contains("link_ref=200"), "expected ref in {s}");
3193 }
3194
3195 #[test]
3196 fn display_message() {
3197 let e = TraceEvent::user_trace(24, Time::ZERO, "hello world");
3198 let s = format!("{e}");
3199 assert!(s.contains("\"hello world\""), "expected msg in {s}");
3200 }
3201
3202 #[test]
3203 fn display_chaos_with_task() {
3204 let e = TraceEvent::new(
3205 25,
3206 Time::ZERO,
3207 TraceEventKind::ChaosInjection,
3208 TraceData::Chaos {
3209 kind: "delay".into(),
3210 task: Some(task(1)),
3211 detail: "200ns".into(),
3212 },
3213 );
3214 let s = format!("{e}");
3215 assert!(s.contains("chaos:delay"), "expected kind in {s}");
3216 assert!(s.contains("task="), "expected task in {s}");
3217 assert!(s.contains("200ns"), "expected detail in {s}");
3218 }
3219
3220 #[test]
3221 fn display_chaos_without_task() {
3222 let e = TraceEvent::new(
3223 26,
3224 Time::ZERO,
3225 TraceEventKind::ChaosInjection,
3226 TraceData::Chaos {
3227 kind: "budget_exhaust".into(),
3228 task: None,
3229 detail: "all".into(),
3230 },
3231 );
3232 let s = format!("{e}");
3233 assert!(s.contains("chaos:budget_exhaust"), "expected kind in {s}");
3234 assert!(!s.contains("task="), "should not show task: {s}");
3235 }
3236
3237 #[test]
3238 fn display_none_data() {
3239 let e = TraceEvent::new(27, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
3240 let s = format!("{e}");
3241 assert!(s.contains("user_trace"), "expected kind in {s}");
3243 }
3244
3245 #[test]
3246 fn display_with_logical_time() {
3247 let lt = LogicalTime::Lamport(LamportTime::from_raw(42));
3248 let e = TraceEvent::new(28, Time::ZERO, TraceEventKind::UserTrace, TraceData::None)
3249 .with_logical_time(lt);
3250 let s = format!("{e}");
3251 assert!(s.contains('@'), "expected @lt in {s}");
3252 }
3253
3254 #[test]
3257 fn events_equal_same_fields() {
3258 let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3259 let b = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3260 assert_eq!(a, b);
3261 }
3262
3263 #[test]
3264 fn events_differ_on_seq() {
3265 let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3266 let b = TraceEvent::spawn(2, Time::ZERO, task(1), region(2));
3267 assert_ne!(a, b);
3268 }
3269
3270 #[test]
3271 fn events_differ_on_kind() {
3272 let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3273 let b = TraceEvent::schedule(1, Time::ZERO, task(1), region(2));
3274 assert_ne!(a, b);
3275 }
3276
3277 #[test]
3278 fn events_differ_on_data() {
3279 let a = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3280 let b = TraceEvent::spawn(1, Time::ZERO, task(1), region(3));
3281 assert_ne!(a, b);
3282 }
3283
3284 #[test]
3285 fn trace_data_clone() {
3286 let data = TraceData::Task {
3287 task: task(1),
3288 region: region(2),
3289 };
3290 let cloned = data.clone();
3291 assert_eq!(data, cloned);
3292 }
3293
3294 #[test]
3295 fn trace_data_message_eq() {
3296 let a = TraceData::Message("hello".into());
3297 let b = TraceData::Message("hello".into());
3298 assert_eq!(a, b);
3299 }
3300
3301 #[test]
3302 fn trace_data_message_ne() {
3303 let a = TraceData::Message("hello".into());
3304 let b = TraceData::Message("world".into());
3305 assert_ne!(a, b);
3306 }
3307
3308 #[test]
3309 fn trace_data_none_variant() {
3310 assert_eq!(TraceData::None, TraceData::None);
3311 }
3312
3313 #[test]
3314 fn trace_event_clone() {
3315 let e = TraceEvent::spawn(1, Time::ZERO, task(1), region(2));
3316 let c = e.clone();
3317 assert_eq!(e, c);
3318 }
3319
3320 #[test]
3323 fn obligation_reserve_all_kinds() {
3324 for kind in [
3325 ObligationKind::SendPermit,
3326 ObligationKind::Ack,
3327 ObligationKind::Lease,
3328 ObligationKind::IoOp,
3329 ] {
3330 let e = TraceEvent::obligation_reserve(
3331 1,
3332 Time::ZERO,
3333 obligation(1),
3334 task(2),
3335 region(3),
3336 kind,
3337 );
3338 match &e.data {
3339 TraceData::Obligation { kind: k, .. } => assert_eq!(*k, kind),
3340 _ => panic!("wrong variant"),
3341 }
3342 }
3343 }
3344
3345 #[test]
3346 fn obligation_abort_all_reasons() {
3347 for reason in [
3348 ObligationAbortReason::Cancel,
3349 ObligationAbortReason::Error,
3350 ObligationAbortReason::Explicit,
3351 ] {
3352 let e = TraceEvent::obligation_abort(
3353 1,
3354 Time::ZERO,
3355 obligation(1),
3356 task(2),
3357 region(3),
3358 ObligationKind::SendPermit,
3359 1000,
3360 reason,
3361 );
3362 match &e.data {
3363 TraceData::Obligation { abort_reason, .. } => {
3364 assert_eq!(*abort_reason, Some(reason));
3365 }
3366 _ => panic!("wrong variant"),
3367 }
3368 }
3369 }
3370
3371 #[test]
3374 fn down_delivered_with_error_reason() {
3375 let e = TraceEvent::down_delivered(
3376 1,
3377 Time::ZERO,
3378 50,
3379 task(1),
3380 task(2),
3381 Time::from_nanos(100),
3382 DownReason::Error("boom".into()),
3383 );
3384 match &e.data {
3385 TraceData::Down { reason, .. } => {
3386 assert_eq!(*reason, DownReason::Error("boom".into()));
3387 }
3388 _ => panic!("wrong variant"),
3389 }
3390 }
3391
3392 #[test]
3393 fn exit_delivered_with_cancelled_reason() {
3394 let e = TraceEvent::exit_delivered(
3395 1,
3396 Time::ZERO,
3397 50,
3398 task(1),
3399 task(2),
3400 Time::from_nanos(100),
3401 DownReason::Cancelled(CancelReason::timeout()),
3402 );
3403 match &e.data {
3404 TraceData::Exit { reason, .. } => {
3405 assert!(matches!(reason, DownReason::Cancelled(_)));
3406 }
3407 _ => panic!("wrong variant"),
3408 }
3409 }
3410
3411 #[test]
3414 fn seq_zero() {
3415 let e = TraceEvent::new(0, Time::ZERO, TraceEventKind::UserTrace, TraceData::None);
3416 assert_eq!(e.seq, 0);
3417 }
3418
3419 #[test]
3420 fn seq_max() {
3421 let e = TraceEvent::new(
3422 u64::MAX,
3423 Time::ZERO,
3424 TraceEventKind::UserTrace,
3425 TraceData::None,
3426 );
3427 assert_eq!(e.seq, u64::MAX);
3428 }
3429
3430 #[test]
3431 fn time_max() {
3432 let e = TraceEvent::new(1, Time::MAX, TraceEventKind::UserTrace, TraceData::None);
3433 assert_eq!(e.time, Time::MAX);
3434 }
3435
3436 #[test]
3437 fn io_result_zero_bytes() {
3438 let e = TraceEvent::io_result(1, Time::ZERO, 0, 0);
3439 assert_eq!(e.data, TraceData::IoResult { token: 0, bytes: 0 });
3440 }
3441
3442 #[test]
3443 fn checkpoint_zero_counts() {
3444 let e = TraceEvent::checkpoint(1, Time::ZERO, 0, 0, 0);
3445 assert_eq!(
3446 e.data,
3447 TraceData::Checkpoint {
3448 sequence: 0,
3449 active_tasks: 0,
3450 active_regions: 0
3451 }
3452 );
3453 }
3454
3455 #[test]
3456 fn futurelock_many_held() {
3457 let held: Vec<_> = (0..100)
3458 .map(|i| (obligation(i), ObligationKind::SendPermit))
3459 .collect();
3460 let e = TraceEvent::new(
3461 1,
3462 Time::ZERO,
3463 TraceEventKind::FuturelockDetected,
3464 TraceData::Futurelock {
3465 task: task(1),
3466 region: region(2),
3467 idle_steps: 1000,
3468 held,
3469 },
3470 );
3471 let s = format!("{e}");
3472 assert!(s.matches("SendPermit").count() == 100);
3474 }
3475
3476 #[test]
3479 fn trace_event_kind_debug_clone_copy_eq_ord_hash() {
3480 use std::collections::HashSet;
3481 let k = TraceEventKind::Spawn;
3482 let k2 = k; let k3 = k;
3484 assert_eq!(k, k2);
3485 assert_eq!(k, k3);
3486 assert_ne!(k, TraceEventKind::Complete);
3487 assert!(k < TraceEventKind::Complete);
3488 let dbg = format!("{k:?}");
3489 assert!(dbg.contains("Spawn"));
3490 let mut set = HashSet::new();
3491 set.insert(k);
3492 assert!(set.contains(&k2));
3493 }
3494
3495 #[test]
3496 fn trace_data_debug_clone_eq() {
3497 let d = TraceData::None;
3498 let d2 = d.clone();
3499 assert_eq!(d, d2);
3500 assert_ne!(d, TraceData::Message("hi".into()));
3501 let dbg = format!("{d:?}");
3502 assert!(dbg.contains("None"));
3503 }
3504
3505 #[test]
3506 fn trace_event_debug_clone_eq() {
3507 let e = TraceEvent::new(
3508 0,
3509 Time::from_nanos(100),
3510 TraceEventKind::UserTrace,
3511 TraceData::Message("hello".into()),
3512 );
3513 let e2 = e.clone();
3514 assert_eq!(e, e2);
3515 let dbg = format!("{e:?}");
3516 assert!(dbg.contains("TraceEvent"));
3517 }
3518
3519 #[test]
3520 fn browser_trace_schema_v1_validates() {
3521 let schema = browser_trace_schema_v1();
3522 validate_browser_trace_schema(&schema).expect("browser schema should validate");
3523 }
3524
3525 #[test]
3526 fn browser_trace_schema_round_trip_json() {
3527 let schema = browser_trace_schema_v1();
3528 let payload = serde_json::to_string(&schema).expect("serialize schema");
3529 let decoded = decode_browser_trace_schema(&payload).expect("decode schema");
3530 assert_eq!(schema, decoded);
3531 }
3532
3533 #[test]
3534 fn browser_trace_schema_timer_required_fields_match_payload_shape() {
3535 let schema = browser_trace_schema_v1();
3536 let scheduled = schema
3537 .event_specs
3538 .iter()
3539 .find(|entry| entry.event_kind == "timer_scheduled")
3540 .expect("timer_scheduled entry should exist");
3541 let fired = schema
3542 .event_specs
3543 .iter()
3544 .find(|entry| entry.event_kind == "timer_fired")
3545 .expect("timer_fired entry should exist");
3546 let cancelled = schema
3547 .event_specs
3548 .iter()
3549 .find(|entry| entry.event_kind == "timer_cancelled")
3550 .expect("timer_cancelled entry should exist");
3551
3552 assert_eq!(
3553 scheduled.required_fields,
3554 vec!["deadline".to_string(), "timer_id".to_string()]
3555 );
3556 assert_eq!(fired.required_fields, vec!["timer_id".to_string()]);
3557 assert_eq!(cancelled.required_fields, vec!["timer_id".to_string()]);
3558 }
3559
3560 #[test]
3561 fn browser_trace_schema_obligation_required_fields_match_payload_shape() {
3562 let schema = browser_trace_schema_v1();
3563 let reserve = schema
3564 .event_specs
3565 .iter()
3566 .find(|entry| entry.event_kind == "obligation_reserve")
3567 .expect("obligation_reserve entry should exist");
3568 let commit = schema
3569 .event_specs
3570 .iter()
3571 .find(|entry| entry.event_kind == "obligation_commit")
3572 .expect("obligation_commit entry should exist");
3573 let abort = schema
3574 .event_specs
3575 .iter()
3576 .find(|entry| entry.event_kind == "obligation_abort")
3577 .expect("obligation_abort entry should exist");
3578 let leak = schema
3579 .event_specs
3580 .iter()
3581 .find(|entry| entry.event_kind == "obligation_leak")
3582 .expect("obligation_leak entry should exist");
3583
3584 assert_eq!(
3585 reserve.required_fields,
3586 vec![
3587 "kind".to_string(),
3588 "obligation".to_string(),
3589 "region".to_string(),
3590 "state".to_string(),
3591 "task".to_string(),
3592 ]
3593 );
3594 assert_eq!(
3595 commit.required_fields,
3596 vec![
3597 "duration_ns".to_string(),
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 abort.required_fields,
3607 vec![
3608 "abort_reason".to_string(),
3609 "duration_ns".to_string(),
3610 "kind".to_string(),
3611 "obligation".to_string(),
3612 "region".to_string(),
3613 "state".to_string(),
3614 "task".to_string(),
3615 ]
3616 );
3617 assert_eq!(
3618 leak.required_fields,
3619 vec![
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 }
3629
3630 #[test]
3631 fn browser_trace_schema_worker_required_fields_match_payload_shape() {
3632 let schema = browser_trace_schema_v1();
3633 for event_kind in [
3634 "worker_cancel_requested",
3635 "worker_cancel_acknowledged",
3636 "worker_drain_started",
3637 "worker_drain_completed",
3638 "worker_finalize_completed",
3639 ] {
3640 let entry = schema
3641 .event_specs
3642 .iter()
3643 .find(|entry| entry.event_kind == event_kind)
3644 .unwrap_or_else(|| panic!("{event_kind} entry should exist"));
3645 assert_eq!(entry.category, BrowserTraceCategory::CancellationTransition);
3646 assert_eq!(
3647 entry.required_fields,
3648 vec![
3649 "decision_seq".to_string(),
3650 "job_id".to_string(),
3651 "obligation".to_string(),
3652 "region".to_string(),
3653 "replay_hash".to_string(),
3654 "task".to_string(),
3655 "worker_id".to_string(),
3656 ]
3657 );
3658 }
3659 }
3660
3661 #[test]
3662 fn browser_trace_schema_decode_v0_migrates() {
3663 let legacy = serde_json::json!({
3664 "schema_version": "browser-trace-schema-v0",
3665 "required_envelope_fields": [
3666 "event_kind",
3667 "schema_version",
3668 "seq",
3669 "time_ns",
3670 "trace_id"
3671 ],
3672 "ordering_semantics": [
3673 "events must be strictly ordered by seq ascending",
3674 "logical_time must be monotonic for comparable causal domains",
3675 "trace streams must be deterministic for identical seed/config/replay inputs"
3676 ],
3677 "event_specs": browser_trace_schema_v1().event_specs
3678 });
3679 let payload = serde_json::to_string(&legacy).expect("serialize legacy schema");
3680 let decoded = decode_browser_trace_schema(&payload).expect("decode legacy schema");
3681 assert_eq!(
3682 decoded.schema_version,
3683 BROWSER_TRACE_SCHEMA_VERSION.to_string()
3684 );
3685 assert!(
3686 decoded
3687 .compatibility
3688 .backward_decode_aliases
3689 .iter()
3690 .any(|alias| alias == "browser-trace-schema-v0")
3691 );
3692 }
3693
3694 #[test]
3695 fn browser_trace_schema_decode_v0_sparse_event_specs_use_defaults() {
3696 let event_specs = TraceEventKind::ALL
3697 .iter()
3698 .map(|kind| serde_json::json!({ "event_kind": kind.stable_name() }))
3699 .collect::<Vec<_>>();
3700 let legacy = serde_json::json!({
3701 "schema_version": "browser-trace-schema-v0",
3702 "required_envelope_fields": [
3703 "event_kind",
3704 "schema_version",
3705 "seq",
3706 "time_ns",
3707 "trace_id"
3708 ],
3709 "ordering_semantics": [
3710 "events must be strictly ordered by seq ascending",
3711 "logical_time must be monotonic for comparable causal domains",
3712 "trace streams must be deterministic for identical seed/config/replay inputs"
3713 ],
3714 "event_specs": event_specs
3715 });
3716 let payload = serde_json::to_string(&legacy).expect("serialize sparse legacy schema");
3717 let decoded = decode_browser_trace_schema(&payload).expect("decode sparse legacy schema");
3718
3719 let user_trace = decoded
3720 .event_specs
3721 .iter()
3722 .find(|entry| entry.event_kind == "user_trace")
3723 .expect("user_trace entry should exist");
3724 assert_eq!(user_trace.category, BrowserTraceCategory::HostCallback);
3725 assert_eq!(user_trace.required_fields, vec!["message".to_string()]);
3726 assert_eq!(user_trace.redacted_fields, vec!["message".to_string()]);
3727 }
3728
3729 #[test]
3730 fn browser_trace_schema_decode_v0_unknown_event_kind_fails_closed() {
3731 let legacy = serde_json::json!({
3732 "schema_version": "browser-trace-schema-v0",
3733 "required_envelope_fields": [
3734 "event_kind",
3735 "schema_version",
3736 "seq",
3737 "time_ns",
3738 "trace_id"
3739 ],
3740 "ordering_semantics": [
3741 "events must be strictly ordered by seq ascending",
3742 "logical_time must be monotonic for comparable causal domains",
3743 "trace streams must be deterministic for identical seed/config/replay inputs"
3744 ],
3745 "event_specs": [{ "event_kind": "not_a_real_event_kind" }]
3746 });
3747 let payload = serde_json::to_string(&legacy).expect("serialize invalid legacy schema");
3748 let error = decode_browser_trace_schema(&payload)
3749 .expect_err("unknown legacy event kinds must fail decode");
3750 assert!(error.contains("unknown legacy event kind"));
3751 }
3752
3753 #[test]
3754 fn browser_trace_redaction_masks_message_payloads() {
3755 let event = TraceEvent::user_trace(4, Time::ZERO, "secret-token");
3756 let redacted = redact_browser_trace_event(&event);
3757 assert_eq!(
3758 redacted,
3759 TraceEvent::new(
3760 4,
3761 Time::ZERO,
3762 TraceEventKind::UserTrace,
3763 TraceData::Message("<redacted>".to_string())
3764 )
3765 );
3766 }
3767
3768 #[test]
3769 fn browser_trace_log_fields_include_required_metadata() {
3770 let event = TraceEvent::timer_fired(9, Time::from_nanos(42), 10);
3771 let fields = browser_trace_log_fields(&event, "trace-browser-1", None);
3772
3773 assert_eq!(
3774 fields.get("schema_version"),
3775 Some(&BROWSER_TRACE_SCHEMA_VERSION.to_string())
3776 );
3777 assert_eq!(fields.get("trace_id"), Some(&"trace-browser-1".to_string()));
3778 assert_eq!(fields.get("event_kind"), Some(&"timer_fired".to_string()));
3779 assert_eq!(fields.get("seq"), Some(&"9".to_string()));
3780 assert_eq!(fields.get("capture_source"), Some(&"runtime".to_string()));
3781 assert_eq!(fields.get("capture_host_turn_seq"), Some(&"9".to_string()));
3782 assert_eq!(fields.get("capture_source_seq"), Some(&"9".to_string()));
3783 assert_eq!(fields.get("capture_host_time_ns"), Some(&"42".to_string()));
3784 assert_eq!(
3785 fields.get("capture_replay_key"),
3786 Some(&"runtime:9:9:42".to_string())
3787 );
3788 assert_eq!(fields.get("validation_status"), Some(&"valid".to_string()));
3789 assert_eq!(
3790 fields.get("validation_failure_category"),
3791 Some(&"none".to_string())
3792 );
3793 assert_eq!(fields.get("sequence_group"), Some(&"timer:10".to_string()));
3794 assert_eq!(fields.get("timer_id"), Some(&"10".to_string()));
3795 }
3796
3797 #[test]
3798 fn browser_trace_log_fields_with_capture_include_host_metadata() {
3799 let event = TraceEvent::timer_fired(17, Time::from_nanos(200), 11);
3800 let capture = BrowserCaptureMetadata {
3801 host_turn_seq: 71,
3802 source: BrowserCaptureSource::HostInput,
3803 source_seq: 4,
3804 host_time_ns: 9_001,
3805 };
3806 let fields =
3807 browser_trace_log_fields_with_capture(&event, "trace-browser-2", None, Some(&capture));
3808 assert_eq!(
3809 fields.get("capture_source"),
3810 Some(&"host_input".to_string())
3811 );
3812 assert_eq!(fields.get("capture_host_turn_seq"), Some(&"71".to_string()));
3813 assert_eq!(fields.get("capture_source_seq"), Some(&"4".to_string()));
3814 assert_eq!(
3815 fields.get("capture_host_time_ns"),
3816 Some(&"9001".to_string())
3817 );
3818 assert_eq!(
3819 fields.get("capture_replay_key"),
3820 Some(&"host_input:71:4:9001".to_string())
3821 );
3822 }
3823
3824 #[test]
3825 fn browser_trace_log_fields_sequence_group_tracks_causal_domain() {
3826 let first = TraceEvent::timer_fired(7, Time::from_nanos(10), 41);
3827 let second = TraceEvent::timer_cancelled(8, Time::from_nanos(11), 41);
3828 let unrelated = TraceEvent::timer_fired(9, Time::from_nanos(12), 99);
3829
3830 let first_fields = browser_trace_log_fields(&first, "trace-browser-group-1", None);
3831 let second_fields = browser_trace_log_fields(&second, "trace-browser-group-2", None);
3832 let unrelated_fields = browser_trace_log_fields(&unrelated, "trace-browser-group-3", None);
3833
3834 assert_eq!(
3835 first_fields.get("sequence_group"),
3836 Some(&"timer:41".to_string())
3837 );
3838 assert_eq!(
3839 first_fields.get("sequence_group"),
3840 second_fields.get("sequence_group")
3841 );
3842 assert_ne!(
3843 first_fields.get("sequence_group"),
3844 unrelated_fields.get("sequence_group")
3845 );
3846 }
3847
3848 #[test]
3849 fn browser_trace_log_fields_sequence_group_preserves_link_relationships() {
3850 let created = TraceEvent::link_created(
3851 20,
3852 Time::from_nanos(100),
3853 77,
3854 task(1),
3855 region(2),
3856 task(3),
3857 region(4),
3858 );
3859 let exited = TraceEvent::exit_delivered(
3860 21,
3861 Time::from_nanos(101),
3862 77,
3863 task(1),
3864 task(3),
3865 Time::from_nanos(55),
3866 DownReason::Normal,
3867 );
3868 let other = TraceEvent::link_dropped(
3869 22,
3870 Time::from_nanos(102),
3871 88,
3872 task(1),
3873 region(2),
3874 task(3),
3875 region(4),
3876 );
3877
3878 let created_fields = browser_trace_log_fields(&created, "trace-browser-link-1", None);
3879 let exited_fields = browser_trace_log_fields(&exited, "trace-browser-link-2", None);
3880 let other_fields = browser_trace_log_fields(&other, "trace-browser-link-3", None);
3881
3882 assert_eq!(
3883 created_fields.get("sequence_group"),
3884 Some(&"link:77".to_string())
3885 );
3886 assert_eq!(
3887 created_fields.get("sequence_group"),
3888 exited_fields.get("sequence_group")
3889 );
3890 assert_ne!(
3891 created_fields.get("sequence_group"),
3892 other_fields.get("sequence_group")
3893 );
3894 }
3895
3896 #[test]
3897 fn browser_trace_log_fields_mark_invalid_when_failure_category_is_set() {
3898 let event = TraceEvent::timer_fired(9, Time::from_nanos(42), 10);
3899 let fields =
3900 browser_trace_log_fields(&event, "trace-browser-1", Some("schema_version_mismatch"));
3901 assert_eq!(
3902 fields.get("validation_status"),
3903 Some(&"invalid".to_string())
3904 );
3905 assert_eq!(
3906 fields.get("validation_failure_category"),
3907 Some(&"schema_version_mismatch".to_string())
3908 );
3909 }
3910
3911 #[test]
3912 fn browser_trace_log_fields_include_worker_replay_linkage() {
3913 let event = TraceEvent::worker_cancel_requested(
3914 21,
3915 Time::from_nanos(55),
3916 "worker-a",
3917 77,
3918 91,
3919 0x00C0_FFEE,
3920 task(9),
3921 region(10),
3922 obligation(11),
3923 );
3924 let fields = browser_trace_log_fields(&event, "trace-browser-worker-1", None);
3925 assert_eq!(fields.get("decision_seq"), Some(&"91".to_string()));
3926 assert_eq!(fields.get("job_id"), Some(&"77".to_string()));
3927 assert_eq!(fields.get("obligation"), Some(&obligation(11).to_string()));
3928 assert_eq!(fields.get("region"), Some(®ion(10).to_string()));
3929 assert_eq!(fields.get("replay_hash"), Some(&"12648430".to_string()));
3930 assert_eq!(fields.get("task"), Some(&task(9).to_string()));
3931 assert_eq!(fields.get("worker_id"), Some(&"worker-a".to_string()));
3932 assert_eq!(
3933 fields.get("sequence_group"),
3934 Some(&"worker_job:77:worker-a".to_string())
3935 );
3936 }
3937
3938 #[test]
3939 fn browser_trace_log_fields_snapshot_scrubs_ids_and_timestamps() {
3940 let event = TraceEvent::worker_cancel_requested(
3941 41,
3942 Time::from_nanos(123_456_789),
3943 "worker-browser-snapshot",
3944 88,
3945 17,
3946 0x00C0_FFEE,
3947 task(9),
3948 region(10),
3949 obligation(11),
3950 );
3951 let capture = BrowserCaptureMetadata {
3952 host_turn_seq: 7,
3953 source: BrowserCaptureSource::HostInput,
3954 source_seq: 19,
3955 host_time_ns: 1_726_133_456_789_000_000,
3956 };
3957
3958 let fields = browser_trace_log_fields_with_capture(
3959 &event,
3960 "trace-browser-snapshot-1",
3961 None,
3962 Some(&capture),
3963 );
3964
3965 insta::assert_json_snapshot!(
3966 "browser_trace_log_fields_worker_scrubbed",
3967 scrub_browser_trace_fields(&fields)
3968 );
3969 }
3970
3971 #[test]
3972 fn browser_trace_log_fields_timer_snapshot_scrubs_ids_and_timestamps() {
3973 let event =
3974 TraceEvent::timer_scheduled(14, Time::from_nanos(333), 42, Time::from_nanos(999));
3975 let fields = browser_trace_log_fields(&event, "trace-browser-timer-1", None);
3976
3977 insta::assert_json_snapshot!(
3978 "browser_trace_log_fields_timer_scrubbed",
3979 scrub_browser_trace_fields(&fields)
3980 );
3981 }
3982
3983 #[test]
3984 fn browser_trace_log_fields_obligation_abort_snapshot_scrubs_ids_and_timestamps() {
3985 let event = TraceEvent::obligation_abort(
3986 52,
3987 Time::from_nanos(7_777),
3988 obligation(4),
3989 task(8),
3990 region(9),
3991 ObligationKind::Lease,
3992 5_000,
3993 ObligationAbortReason::Error,
3994 );
3995 let fields = browser_trace_log_fields(&event, "trace-browser-obligation-1", None);
3996
3997 insta::assert_json_snapshot!(
3998 "browser_trace_log_fields_obligation_abort_scrubbed",
3999 scrub_browser_trace_fields(&fields)
4000 );
4001 }
4002
4003 #[test]
4004 fn browser_trace_log_fields_exit_snapshot_scrubs_ids_and_timestamps() {
4005 let event = TraceEvent::exit_delivered(
4006 61,
4007 Time::from_nanos(8_001),
4008 77,
4009 task(2),
4010 task(3),
4011 Time::from_nanos(4_444),
4012 DownReason::Normal,
4013 );
4014 let fields = browser_trace_log_fields(&event, "trace-browser-exit-1", None);
4015
4016 insta::assert_json_snapshot!(
4017 "browser_trace_log_fields_exit_scrubbed",
4018 scrub_browser_trace_fields(&fields)
4019 );
4020 }
4021
4022 #[test]
4023 fn browser_trace_log_fields_cap_large_worker_attributes_without_utf8_breakage() {
4024 let worker_id = format!("worker-{}", "e\u{0301}".repeat(200));
4025 let event = TraceEvent::worker_cancel_requested(
4026 30,
4027 Time::from_nanos(60),
4028 worker_id,
4029 123,
4030 456,
4031 0xDEAD_BEEF,
4032 task(5),
4033 region(6),
4034 obligation(7),
4035 );
4036 let fields = browser_trace_log_fields(&event, "trace-browser-worker-2", None);
4037
4038 let worker_id = fields
4039 .get("worker_id")
4040 .expect("worker_id field should be present");
4041 let sequence_group = fields
4042 .get("sequence_group")
4043 .expect("sequence_group field should be present");
4044
4045 assert!(worker_id.len() <= MAX_BROWSER_TRACE_ATTRIBUTE_BYTES);
4046 assert!(sequence_group.len() <= MAX_BROWSER_TRACE_ATTRIBUTE_BYTES);
4047 assert!(worker_id.starts_with("worker-"));
4048 assert!(sequence_group.starts_with("worker_job:123:worker-"));
4049 assert!(worker_id.contains('#'));
4050 assert!(sequence_group.contains('#'));
4051 }
4052}