1use crate::json::{outcome_name, quote, stop_reason_name};
26use crate::sync::Mutex;
27use alloc::{string::String, sync::Arc, vec::Vec};
28use core::{
29 fmt,
30 panic::Location,
31 sync::atomic::{AtomicBool, AtomicUsize, Ordering},
32 time::Duration,
33};
34use how_far::{Outcome, Report, Stop, StopReason};
35use how_far_along::NodeId;
36
37pub trait Clock: Send + Sync {
42 fn now(&self) -> Duration;
44}
45impl<C: Clock + ?Sized> Clock for Arc<C> {
46 fn now(&self) -> Duration {
47 (**self).now()
48 }
49}
50
51#[derive(Clone, Copy, Debug)]
53pub struct StdClock(std::time::Instant);
54impl StdClock {
55 pub fn new() -> Self {
57 Self(std::time::Instant::now())
58 }
59}
60impl Default for StdClock {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65impl Clock for StdClock {
66 fn now(&self) -> Duration {
67 self.0.elapsed()
68 }
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73#[non_exhaustive]
74pub enum SpanKind {
75 Work,
77 Queued,
79 Wait,
81 Yield,
83 Callback,
85}
86
87impl SpanKind {
88 fn name(self) -> &'static str {
89 match self {
90 Self::Work => "Work",
91 Self::Queued => "Queued",
92 Self::Wait => "Wait",
93 Self::Yield => "Yield",
94 Self::Callback => "Callback",
95 }
96 }
97}
98
99#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
101pub struct SpanId(usize);
102
103impl SpanId {
104 pub const fn get(self) -> usize {
106 self.0
107 }
108}
109
110impl fmt::Display for SpanId {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 self.0.fmt(f)
113 }
114}
115
116#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
118#[non_exhaustive]
119pub struct SourceSite {
120 pub file: &'static str,
122 pub line: u32,
124 pub column: u32,
126}
127
128impl SourceSite {
129 pub(crate) fn from_location(at: &'static Location<'static>) -> Self {
130 Self {
131 file: at.file(),
132 line: at.line(),
133 column: at.column(),
134 }
135 }
136}
137
138impl fmt::Display for SourceSite {
139 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(f, "{}:{}:{}", self.file, self.line, self.column)
141 }
142}
143
144#[derive(Clone, Debug)]
146#[non_exhaustive]
147pub struct SiteStats {
148 pub file: &'static str,
150 pub line: u32,
152 pub column: u32,
154 pub checks: u64,
156 pub reports: u64,
158 pub units: u64,
160 pub max_gap_before_check: Duration,
162 pub time: Duration,
166 pub active: Option<(Duration, Duration)>,
169}
170
171impl SiteStats {
172 fn hit(&mut self, at: u64, interval: u64) {
175 let at = Duration::from_nanos(at);
176 self.time += Duration::from_nanos(interval);
177 self.active = Some(match self.active {
178 Some((first, last)) => (first.min(at), last.max(at)),
179 None => (at, at),
180 });
181 }
182}
183
184#[derive(Clone, Debug)]
187#[non_exhaustive]
188pub struct ReportGap {
189 pub start: Duration,
191 pub duration: Duration,
193 pub checks: u64,
195 pub max_check_gap: Duration,
197 pub from: Option<SourceSite>,
199 pub to: Option<SourceSite>,
201}
202
203#[derive(Clone, Debug)]
211#[non_exhaustive]
212pub struct UnitPace {
213 pub quarters: [Duration; 4],
215}
216
217#[derive(Clone, Debug, Default)]
220#[non_exhaustive]
221pub struct Stats {
222 pub checks: u64,
224 pub reports: u64,
226 pub units: u64,
228 pub overflowed: bool,
230 pub max_check_gap: Duration,
233 pub max_check_gap_start: Duration,
235 pub max_report_gap: Option<ReportGap>,
237 pub unit_pace: Option<UnitPace>,
240 pub check_time: Duration,
248 pub stopped_at: Option<Duration>,
250 pub stop_reason: Option<StopReason>,
252 pub clock_regressions: u64,
256 pub sites: Vec<SiteStats>,
258 pub unattributed_calls: u64,
260}
261
262#[derive(Clone, Debug)]
264#[non_exhaustive]
265pub struct SpanRecord {
266 pub id: SpanId,
268 pub parent: Option<SpanId>,
270 pub node: Option<NodeId>,
272 pub task: String,
274 pub kind: SpanKind,
276 pub start: Duration,
278 pub end: Duration,
280 pub outcome: Outcome,
282 pub stats: Stats,
284}
285
286impl SpanRecord {
287 pub fn elapsed(&self) -> Duration {
290 self.end.saturating_sub(self.start)
291 }
292 pub fn checks_per_second(&self) -> f64 {
294 if self.elapsed().is_zero() {
295 0.0
296 } else {
297 self.stats.checks as f64 / self.elapsed().as_secs_f64()
298 }
299 }
300 pub fn is_poll_storm(&self, minimum_checks: u64, checks_per_second: f64) -> bool {
303 !self.stats.overflowed
304 && self.stats.checks >= minimum_checks
305 && self.checks_per_second() > checks_per_second
306 }
307}
308
309struct Inner {
310 clock: Arc<dyn Clock>,
311 capacity: usize,
312 report_timing: AtomicBool,
313 next_id: AtomicUsize,
314 active: AtomicUsize,
315 state: Mutex<Trace>,
316}
317
318#[derive(Clone)]
321pub struct Profiler {
322 inner: Arc<Inner>,
323}
324
325impl fmt::Debug for Profiler {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 f.debug_struct("Profiler")
328 .field("capacity", &self.inner.capacity)
329 .finish_non_exhaustive()
330 }
331}
332
333impl Profiler {
334 pub(crate) fn incident(&self, incident: crate::diagnostics::Incident) {
335 let mut state = self.inner.state.lock();
336 if state.incidents.len() < self.inner.capacity {
337 state.incidents.push(incident);
338 } else {
339 state.dropped_incidents = state.dropped_incidents.saturating_add(1);
340 }
341 }
342
343 pub fn new(clock: impl Clock + 'static, capacity: usize) -> Self {
346 Self {
347 inner: Arc::new(Inner {
348 clock: Arc::new(clock),
349 capacity,
350 report_timing: AtomicBool::new(false),
351 next_id: AtomicUsize::new(0),
352 active: AtomicUsize::new(0),
353 state: Mutex::new(Trace {
354 schema_version: 2,
355 incidents: Vec::new(),
356 dropped_incidents: 0,
357 progress: None,
358 metadata: Vec::new(),
359 spans: Vec::new(),
360 dropped_spans: 0,
361 active_spans: 0,
362 cancelled_at: None,
363 observed_at: None,
364 returned_at: None,
365 }),
366 }),
367 }
368 }
369
370 pub fn set_report_timing(&self, enabled: bool) {
376 self.inner.report_timing.store(enabled, Ordering::Relaxed);
377 }
378
379 pub fn metadata(&self, key: impl Into<String>, value: impl Into<String>) {
382 let key = key.into();
383 let value = value.into();
384 let mut state = self.inner.state.lock();
385 if let Some((_, old)) = state.metadata.iter_mut().find(|(k, _)| *k == key) {
386 *old = value;
387 } else {
388 state.metadata.push((key, value));
389 }
390 }
391
392 pub fn span(
407 &self,
408 node: impl Into<Option<NodeId>>,
409 task: impl Into<String>,
410 kind: SpanKind,
411 ) -> Span {
412 self.start_span(None, node.into(), task.into(), kind, None)
413 }
414
415 pub fn measure_callback<R>(&self, task: impl Into<String>, callback: impl FnOnce() -> R) -> R {
418 let span = self.span(None, task, SpanKind::Callback);
419 let result = callback();
420 span.finish(Outcome::Succeeded);
421 result
422 }
423
424 pub(crate) fn now(&self) -> Duration {
426 self.inner.clock.now()
427 }
428
429 #[allow(deprecated)] pub(crate) fn start_span(
432 &self,
433 parent: Option<SpanId>,
434 node: Option<NodeId>,
435 task: String,
436 kind: SpanKind,
437 entered: Option<Duration>,
438 ) -> Span {
439 let id = self
442 .inner
443 .next_id
444 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
445 Some(n.saturating_add(1))
446 });
447 let (Ok(id) | Err(id)) = id;
448 let now = self.inner.clock.now();
449 let start = entered.unwrap_or(now).min(now);
450 self.inner.active.fetch_add(1, Ordering::Relaxed);
451 Span {
452 finished: false,
453 inner: Arc::new(SpanInner {
454 profiler: self.clone(),
455 id: SpanId(id),
456 parent,
457 node,
458 task,
459 kind,
460 start,
461 time_reports: self.inner.report_timing.load(Ordering::Relaxed),
462 checks_started: AtomicUsize::new(0),
463 state: Mutex::new(SpanState::new(nanos(start))),
464 }),
465 }
466 }
467
468 pub fn cancellation_requested(&self) {
471 let now = self.inner.clock.now();
472 let mut state = self.inner.state.lock();
473 state.cancelled_at = Some(state.cancelled_at.unwrap_or(now).min(now));
474 }
475
476 pub fn operation_returned(&self) {
479 let now = self.inner.clock.now();
480 self.inner.state.lock().returned_at = Some(now);
481 }
482
483 pub fn snapshot(&self) -> Trace {
485 let state = self.inner.state.lock();
486 self.snapshot_from(&state)
487 }
488
489 pub fn try_snapshot(&self) -> Option<Trace> {
492 let state = self.inner.state.try_lock()?;
493 Some(self.snapshot_from(&state))
494 }
495
496 fn snapshot_from(&self, state: &Trace) -> Trace {
497 let mut trace = state.clone();
498 trace.active_spans = self.inner.active.load(Ordering::Relaxed);
499 trace
500 }
501}
502
503fn nanos(at: Duration) -> u64 {
507 at.as_secs()
508 .saturating_mul(1_000_000_000)
509 .saturating_add(u64::from(at.subsec_nanos()))
510}
511
512struct SpanState {
513 stats: Stats,
514 timed_checks: u64,
516 check_time: u64,
517 max_check_gap: u64,
518 max_check_gap_start: u64,
519 max_report_gap: Option<u64>,
521 last_check: u64,
522 last_report: u64,
523 last_report_site: Option<SourceSite>,
524 start: u64,
527 samples: Vec<(u64, u64)>,
528 stride: u64,
529 until_sample: u64,
530 latest: Option<(u64, u64)>,
531 window: Window,
534 site_gaps: Vec<u64>,
536 check_site: usize,
539 report_site: usize,
540 closed: bool,
541}
542
543impl SpanState {
544 fn new(start: u64) -> Self {
545 Self {
546 stats: Stats::default(),
547 timed_checks: 0,
548 check_time: 0,
549 max_check_gap: 0,
550 max_check_gap_start: 0,
551 max_report_gap: None,
552 last_check: start,
553 last_report: start,
554 last_report_site: None,
555 start,
556 samples: Vec::new(),
557 stride: 1,
558 until_sample: 1,
559 latest: None,
560 window: Window::new(start),
561 site_gaps: Vec::new(),
562 check_site: 0,
563 report_site: 0,
564 closed: false,
565 }
566 }
567
568 fn close_report_gap(&mut self, end: u64, duration: u64, to: Option<SourceSite>) {
570 let (checks, max_check_gap) = self.window.close(end);
571 if self.max_report_gap.is_none_or(|old| duration > old) {
572 self.max_report_gap = Some(duration);
573 self.stats.max_report_gap = Some(ReportGap {
574 start: Duration::from_nanos(self.last_report),
575 duration: Duration::from_nanos(duration),
576 checks,
577 max_check_gap: Duration::from_nanos(max_check_gap),
578 from: self.last_report_site,
579 to,
580 });
581 }
582 }
583
584 fn late(&mut self, at: u64, units: u64) {
588 for sample in &mut self.samples {
589 if sample.0 > at {
590 sample.1 = sample.1.saturating_add(units);
591 }
592 }
593 if let Some(latest) = &mut self.latest {
594 latest.1 = latest.1.saturating_add(units);
595 }
596 }
597
598 fn sample(&mut self, now: u64, units: u64) {
600 self.latest = Some((now, units));
601 self.until_sample -= 1;
602 if self.until_sample != 0 {
603 return;
604 }
605 if self.samples.len() == PACE_SAMPLES {
606 let kept = PACE_SAMPLES / 2;
608 for index in 0..kept {
609 self.samples[index] = self.samples[2 * index + 1];
610 }
611 self.samples.truncate(kept);
612 self.stride = self.stride.saturating_mul(2);
613 }
614 self.until_sample = self.stride;
615 self.samples.push((now, units));
616 }
617
618 fn unit_pace(&self) -> Option<UnitPace> {
620 let latest = self.latest?;
621 let total = latest.1;
622 if self.stats.overflowed || self.stats.reports < 4 || total < 4 {
623 return None;
624 }
625 let samples = &self.samples;
627 let count = samples.len() + 2;
628 let point = |i: usize| match i {
629 0 => (self.start, 0),
630 i if i <= samples.len() => samples[i - 1],
631 _ => latest,
632 };
633 let (mut previous, mut i) = (self.start as f64, 1);
634 let mut quarter = |k: u32| {
635 let boundary = total as f64 * f64::from(k) / 4.0;
636 while i + 1 < count && (point(i).1 as f64) < boundary {
637 i += 1;
638 }
639 let ((t0, u0), (t1, u1)) = (point(i - 1), point(i));
640 let at = if u1 > u0 {
641 let part = ((boundary - u0 as f64) / (u1 - u0) as f64).min(1.0);
642 t0 as f64 + (t1 - t0) as f64 * part
643 } else {
644 t1 as f64
645 };
646 let quarter = Duration::from_nanos((at - previous).max(0.0) as u64);
647 previous = previous.max(at);
648 quarter
649 };
650 let quarters = [quarter(1), quarter(2), quarter(3), quarter(4)];
651 Some(UnitPace { quarters })
652 }
653
654 fn take_stats(&mut self) -> Stats {
657 let (timed, checks) = (self.timed_checks, self.stats.checks);
658 let check_time = if timed != 0 && timed < checks {
659 u128::from(self.check_time) * u128::from(checks) / u128::from(timed)
660 } else {
661 u128::from(self.check_time)
662 };
663 self.stats.check_time = Duration::from_nanos(u64::try_from(check_time).unwrap_or(u64::MAX));
664 self.stats.max_check_gap = Duration::from_nanos(self.max_check_gap);
665 self.stats.max_check_gap_start = Duration::from_nanos(self.max_check_gap_start);
666 for (site, &gap) in self.stats.sites.iter_mut().zip(&self.site_gaps) {
667 site.max_gap_before_check = Duration::from_nanos(gap);
668 }
669 core::mem::take(&mut self.stats)
670 }
671}
672
673struct Window {
674 last_check: u64,
675 checks: u64,
676 max_gap: u64,
677}
678
679impl Window {
680 fn new(at: u64) -> Self {
681 Self {
682 last_check: at,
683 checks: 0,
684 max_gap: 0,
685 }
686 }
687 fn check(&mut self, at: u64) {
688 self.max_gap = self.max_gap.max(at.saturating_sub(self.last_check));
689 self.last_check = self.last_check.max(at);
690 self.checks = self.checks.saturating_add(1);
691 }
692 fn close(&mut self, end: u64) -> (u64, u64) {
694 let gap = self.max_gap.max(end.saturating_sub(self.last_check));
695 let checks = self.checks;
696 *self = Self::new(end);
697 (checks, gap)
698 }
699}
700
701pub(crate) struct SpanInner {
702 profiler: Profiler,
703 id: SpanId,
704 parent: Option<SpanId>,
705 node: Option<NodeId>,
706 task: String,
707 kind: SpanKind,
708 start: Duration,
709 time_reports: bool,
710 checks_started: AtomicUsize,
713 state: Mutex<SpanState>,
714}
715
716const TIMED_CHECKS: usize = 16;
718const PACE_SAMPLES: usize = 32;
720
721impl SpanInner {
722 fn now(&self) -> Duration {
723 self.profiler.inner.clock.now()
724 }
725
726 fn time_next_check(&self) -> bool {
728 let n = self.checks_started.fetch_add(1, Ordering::Relaxed);
729 n < TIMED_CHECKS || n.is_multiple_of(TIMED_CHECKS)
730 }
731
732 fn record_check(
736 &self,
737 start: Duration,
738 end: Option<Duration>,
739 result: Result<(), StopReason>,
740 at: &'static Location<'static>,
741 ) {
742 let mut state = self.state.lock();
743 if state.closed {
744 drop(state);
746 self.observed(result, end);
747 return;
748 }
749 let start = nanos(start);
750 let gap = start.saturating_sub(state.last_check);
753 if gap > state.max_check_gap {
754 state.max_check_gap = gap;
755 state.max_check_gap_start = state.last_check;
756 }
757 state.window.check(start);
758 let interval = start.saturating_sub(state.last_check.max(state.last_report));
759 state.last_check = state.last_check.max(start);
760 if let Some(end) = end {
761 let elapsed = match nanos(end).checked_sub(start) {
762 Some(elapsed) => elapsed,
763 None => {
764 state.stats.clock_regressions += 1;
765 0
766 }
767 };
768 state.check_time = state.check_time.saturating_add(elapsed);
769 state.timed_checks += 1;
770 }
771 state.stats.overflowed |= add(&mut state.stats.checks, 1);
772 if let (Err(reason), Some(end)) = (result, end)
773 && state.stats.stopped_at.is_none()
774 {
775 state.stats.stopped_at = Some(end);
776 state.stats.stop_reason = Some(reason);
777 }
778 {
779 let state = &mut *state;
781 if let Some(i) = site(
782 &mut state.stats,
783 &mut state.site_gaps,
784 &mut state.check_site,
785 at,
786 ) {
787 let site = &mut state.stats.sites[i];
788 site.checks = site.checks.saturating_add(1);
789 site.hit(start, interval);
790 state.site_gaps[i] = state.site_gaps[i].max(gap);
791 }
792 }
793 drop(state);
794 self.observed(result, end);
795 }
796
797 fn observed(&self, result: Result<(), StopReason>, end: Option<Duration>) {
799 if let (Err(_), Some(end)) = (result, end) {
800 let mut trace = self.profiler.inner.state.lock();
801 trace.observed_at = Some(trace.observed_at.unwrap_or(end).min(end));
802 }
803 }
804
805 fn record_report(&self, completed: u64, at: &'static Location<'static>) {
808 let now = if self.time_reports {
809 Some(nanos(self.now()))
810 } else {
811 None
812 };
813 let mut state = self.state.lock();
814 if state.closed {
815 return;
816 }
817 let timed = match now {
818 Some(now) if now >= state.last_report => Some(now),
819 _ => None,
820 };
821 let mut interval = 0;
822 if let Some(now) = timed {
823 interval = now.saturating_sub(state.last_check.max(state.last_report));
824 let at = Some(SourceSite::from_location(at));
825 let duration = now - state.last_report;
826 state.close_report_gap(now, duration, at);
827 state.last_report = now;
828 state.last_report_site = at;
829 }
830 state.stats.overflowed |= add(&mut state.stats.reports, 1);
831 state.stats.overflowed |= add(&mut state.stats.units, completed);
832 match (timed, now) {
833 (Some(now), _) => {
834 let units = state.stats.units;
835 state.sample(now, units);
836 }
837 (None, Some(now)) => state.late(now, completed),
838 (None, None) => {}
839 }
840 let state = &mut *state;
842 if let Some(i) = site(
843 &mut state.stats,
844 &mut state.site_gaps,
845 &mut state.report_site,
846 at,
847 ) {
848 let site = &mut state.stats.sites[i];
849 site.reports = site.reports.saturating_add(1);
850 site.units = site.units.saturating_add(completed);
851 if let Some(now) = timed {
852 site.hit(now, interval);
853 }
854 }
855 }
856
857 fn finish(&self, outcome: Outcome) {
858 let end = self.profiler.inner.clock.now();
859 let stats = {
860 let mut state = self.state.lock();
861 if state.closed {
862 return;
863 }
864 state.closed = true;
865 let end = nanos(end);
866 let gap = end.checked_sub(state.last_check).unwrap_or_else(|| {
867 state.stats.clock_regressions += 1;
868 0
869 });
870 if gap > state.max_check_gap {
871 state.max_check_gap = gap;
872 state.max_check_gap_start = state.last_check;
873 }
874 if self.time_reports {
875 let duration = end.checked_sub(state.last_report).unwrap_or_else(|| {
876 state.stats.clock_regressions += 1;
877 0
878 });
879 state.close_report_gap(end, duration, None);
880 state.stats.unit_pace = state.unit_pace();
881 }
882 state.take_stats()
883 };
884 let record = SpanRecord {
885 id: self.id,
886 parent: self.parent,
887 node: self.node,
888 task: self.task.clone(),
889 kind: self.kind,
890 start: self.start,
891 end,
892 outcome,
893 stats,
894 };
895 let mut trace = self.profiler.inner.state.lock();
896 if self.id.0 != usize::MAX && trace.spans.len() < self.profiler.inner.capacity {
897 trace.spans.push(record);
898 } else {
899 trace.dropped_spans = trace.dropped_spans.saturating_add(1);
900 }
901 self.profiler.inner.active.fetch_sub(1, Ordering::Relaxed);
902 }
903}
904
905#[must_use = "finish the span after its instrumented calls return; dropping it unfinished records Abandoned"]
909pub struct Span {
910 inner: Arc<SpanInner>,
911 finished: bool,
912}
913
914impl fmt::Debug for Span {
915 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
916 f.debug_struct("Span")
917 .field("id", &self.inner.id)
918 .field("task", &self.inner.task)
919 .finish_non_exhaustive()
920 }
921}
922
923impl Span {
924 pub fn id(&self) -> SpanId {
926 self.inner.id
927 }
928 pub fn child(&self, task: impl Into<String>, kind: SpanKind) -> Span {
930 self.inner
931 .profiler
932 .start_span(Some(self.id()), self.inner.node, task.into(), kind, None)
933 }
934 pub fn instrument<T>(&self, value: T) -> Instrumented<T> {
937 Instrumented {
938 value,
939 span: self.shared(),
940 }
941 }
942 pub(crate) fn shared(&self) -> Arc<SpanInner> {
945 Arc::clone(&self.inner)
946 }
947 pub fn finish(mut self, outcome: Outcome) {
950 self.inner.finish(outcome);
951 self.finished = true;
952 }
953}
954
955impl Drop for Span {
956 fn drop(&mut self) {
957 if !self.finished {
958 self.inner.finish(Outcome::Abandoned);
959 }
960 }
961}
962
963#[derive(Clone)]
970pub struct Instrumented<T> {
971 value: T,
972 span: Arc<SpanInner>,
973}
974
975impl<T: fmt::Debug> fmt::Debug for Instrumented<T> {
976 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
977 f.debug_struct("Instrumented")
978 .field("value", &self.value)
979 .field("span", &self.span.id)
980 .finish()
981 }
982}
983
984impl<T> Instrumented<T> {
985 pub fn inner(&self) -> &T {
987 &self.value
988 }
989}
990
991pub(crate) fn sorted_indices(
998 len: usize,
999 compare: &dyn Fn(usize, usize) -> core::cmp::Ordering,
1000) -> Vec<usize> {
1001 let mut order = Vec::with_capacity(len);
1002 for index in 0..len {
1003 order.push(index);
1004 }
1005 let mut merged = order.clone();
1006 let mut width = 1;
1007 while width < len {
1008 let mut start = 0;
1009 while start < len {
1010 let middle = (start + width).min(len);
1011 let end = (start + 2 * width).min(len);
1012 let (mut left, mut right) = (start, middle);
1013 for slot in &mut merged[start..end] {
1014 let take_left =
1015 right == end || (left < middle && compare(order[left], order[right]).is_le());
1016 if take_left {
1017 *slot = order[left];
1018 left += 1;
1019 } else {
1020 *slot = order[right];
1021 right += 1;
1022 }
1023 }
1024 start = end;
1025 }
1026 (order, merged) = (merged, order);
1027 width *= 2;
1028 }
1029 order
1030}
1031
1032fn add(value: &mut u64, n: u64) -> bool {
1033 let overflow = value.checked_add(n).is_none();
1034 *value = value.saturating_add(n);
1035 overflow
1036}
1037
1038#[inline(always)]
1041fn is_site(site: &SiteStats, at: &'static Location<'static>) -> bool {
1042 site.line == at.line()
1043 && site.column == at.column()
1044 && (core::ptr::eq(site.file, at.file()) || site.file == at.file())
1045}
1046
1047#[inline(always)]
1050fn site(
1051 stats: &mut Stats,
1052 gaps: &mut Vec<u64>,
1053 hint: &mut usize,
1054 at: &'static Location<'static>,
1055) -> Option<usize> {
1056 if stats.sites.get(*hint).is_some_and(|s| is_site(s, at)) {
1057 return Some(*hint);
1058 }
1059 find_site(stats, gaps, hint, at)
1060}
1061
1062#[cold]
1063#[inline(never)]
1064fn find_site(
1065 stats: &mut Stats,
1066 gaps: &mut Vec<u64>,
1067 hint: &mut usize,
1068 at: &'static Location<'static>,
1069) -> Option<usize> {
1070 let sites = &mut stats.sites;
1071 let index = match sites.iter().position(|s| is_site(s, at)) {
1072 Some(index) => index,
1073 None if sites.len() == 64 => {
1074 stats.unattributed_calls = stats.unattributed_calls.saturating_add(1);
1075 return None;
1076 }
1077 None => {
1078 sites.push(SiteStats {
1079 file: at.file(),
1080 line: at.line(),
1081 column: at.column(),
1082 checks: 0,
1083 reports: 0,
1084 units: 0,
1085 max_gap_before_check: Duration::ZERO,
1086 time: Duration::ZERO,
1087 active: None,
1088 });
1089 gaps.push(0);
1090 sites.len() - 1
1091 }
1092 };
1093 *hint = index;
1094 Some(index)
1095}
1096
1097#[track_caller]
1101pub(crate) fn checked(span: &SpanInner, value: &dyn Stop) -> Result<(), StopReason> {
1102 let timed = span.time_next_check();
1103 let start = span.now();
1104 let result = value.check(); let end = (timed || result.is_err()).then(|| span.now());
1106 span.record_check(start, end, result, Location::caller());
1107 result
1108}
1109
1110#[track_caller]
1112pub(crate) fn advanced(span: &SpanInner, value: &dyn Report, completed: u64) {
1113 value.advance(completed);
1114 span.record_report(completed, Location::caller());
1115}
1116
1117impl<T: Stop> Stop for Instrumented<T> {
1118 #[track_caller]
1119 fn check(&self) -> Result<(), StopReason> {
1120 checked(&self.span, &self.value)
1121 }
1122}
1123
1124impl<T: Report> Report for Instrumented<T> {
1125 #[track_caller]
1126 fn advance(&self, completed: u64) {
1127 advanced(&self.span, &self.value, completed);
1128 }
1129}
1130
1131#[derive(Clone, Debug)]
1133#[non_exhaustive]
1134pub struct Trace {
1135 pub incidents: Vec<crate::diagnostics::Incident>,
1137 pub dropped_incidents: u64,
1139
1140 pub schema_version: u32,
1142 pub progress: Option<how_far_along::Snapshot>,
1144 pub metadata: Vec<(String, String)>,
1146 pub spans: Vec<SpanRecord>,
1148 pub dropped_spans: u64,
1150 pub active_spans: usize,
1152 pub cancelled_at: Option<Duration>,
1154 pub observed_at: Option<Duration>,
1156 pub returned_at: Option<Duration>,
1158}
1159
1160#[derive(Clone, Debug, PartialEq)]
1164#[non_exhaustive]
1165pub struct Overlap {
1166 pub wall: Duration,
1168 pub task_time: Duration,
1170 pub mean_active_tasks: f64,
1172 pub peak_active_tasks: usize,
1174 pub single_task_tail: Duration,
1176}
1177
1178impl Trace {
1179 pub fn with_progress(mut self, progress: how_far_along::Snapshot) -> Self {
1182 self.progress = Some(progress);
1183 self
1184 }
1185
1186 pub fn overlap(&self, ids: &[SpanId]) -> Option<Overlap> {
1192 if ids.is_empty() {
1193 return None;
1194 }
1195 let (mut first, mut last, mut task_time) = (Duration::MAX, Duration::ZERO, Duration::ZERO);
1196 let mut events: Vec<(Duration, isize)> = Vec::with_capacity(2 * ids.len());
1197 for (i, &id) in ids.iter().enumerate() {
1198 let span = self.span(id)?;
1199 if ids[..i].contains(&id)
1200 || span.kind != SpanKind::Work
1201 || span.end < span.start
1202 || span.stats.clock_regressions != 0
1203 {
1204 return None;
1205 }
1206 let mut parent = span.parent;
1207 while let Some(id) = parent {
1208 if ids.contains(&id) {
1209 return None;
1210 }
1211 parent = self.span(id)?.parent;
1212 }
1213 first = first.min(span.start);
1214 last = last.max(span.end);
1215 task_time = task_time.saturating_add(span.elapsed());
1216 events.push((span.start, 1));
1217 events.push((span.end, -1));
1218 }
1219 let order = sorted_indices(events.len(), &|a, b| events[a].0.cmp(&events[b].0));
1220 let mut active = 0_isize;
1221 let mut peak = 0;
1222 let mut tail_start = None;
1223 let mut i = 0;
1224 while i < order.len() {
1225 let at = events[order[i]].0;
1226 let before = active;
1227 while i < order.len() && events[order[i]].0 == at {
1228 active += events[order[i]].1;
1229 i += 1;
1230 }
1231 peak = peak.max(active as usize);
1232 if active == 1 && before != 1 {
1233 tail_start = Some(at);
1234 } else if active != 1 && at != last {
1235 tail_start = None;
1236 }
1237 }
1238 let wall = last.saturating_sub(first);
1239 Some(Overlap {
1240 wall,
1241 task_time,
1242 mean_active_tasks: if wall.is_zero() {
1243 0.0
1244 } else {
1245 task_time.as_secs_f64() / wall.as_secs_f64()
1246 },
1247 peak_active_tasks: peak,
1248 single_task_tail: last.saturating_sub(tail_start.unwrap_or(last)),
1249 })
1250 }
1251
1252 #[expect(
1254 clippy::manual_find,
1255 reason = "a loop compiles to less code than an adapter"
1256 )]
1257 pub(crate) fn span(&self, id: SpanId) -> Option<&SpanRecord> {
1258 for span in &self.spans {
1259 if span.id == id {
1260 return Some(span);
1261 }
1262 }
1263 None
1264 }
1265
1266 pub fn cancellation_observation_latency(&self) -> Option<Duration> {
1268 self.observed_at?.checked_sub(self.cancelled_at?)
1269 }
1270
1271 pub fn cancellation_return_latency(&self) -> Option<Duration> {
1274 self.returned_at?.checked_sub(self.cancelled_at?)
1275 }
1276
1277 pub fn write_json(&self, out: &mut impl fmt::Write) -> fmt::Result {
1283 write!(
1284 out,
1285 "{{\"schema_version\":{},\"time_unit\":\"ns\",\"dropped_spans\":{},\"active_spans\":{},\"metadata\":{{",
1286 self.schema_version, self.dropped_spans, self.active_spans
1287 )?;
1288 for (i, (key, value)) in self.metadata.iter().enumerate() {
1289 out.write_str(if i > 0 { "," } else { "" })?;
1290 quote(out, key)?;
1291 out.write_char(':')?;
1292 quote(out, value)?;
1293 }
1294 out.write_str("},\"progress\":")?;
1295 match &self.progress {
1296 Some(progress) => progress.write_json(out)?,
1297 None => out.write_str("null")?,
1298 }
1299 write!(
1300 out,
1301 ",\"dropped_incidents\":{},\"incidents\":[",
1302 self.dropped_incidents
1303 )?;
1304 for (i, incident) in self.incidents.iter().enumerate() {
1305 out.write_str(if i > 0 { "," } else { "" })?;
1306 write!(out, "{{\"node\":{},\"problem\":", incident.node)?;
1307 let name = match incident.problem {
1309 crate::diagnostics::Problem::Plan(_) => "Plan",
1310 crate::diagnostics::Problem::ReportAfterCompletion => "ReportAfterCompletion",
1311 crate::diagnostics::Problem::ReportToBranch => "ReportToBranch",
1312 };
1313 quote(out, name)?;
1314 out.write_str(",\"detail\":")?;
1315 quote(out, &alloc::format!("{:?}", incident.problem))?;
1316 out.write_str(",\"file\":")?;
1317 quote(out, incident.site.file)?;
1318 write!(
1319 out,
1320 ",\"line\":{},\"column\":{}}}",
1321 incident.site.line, incident.site.column
1322 )?;
1323 }
1324 out.write_char(']')?;
1325 optional_time(out, ",\"cancelled_at\":", self.cancelled_at)?;
1326 optional_time(out, ",\"observed_at\":", self.observed_at)?;
1327 optional_time(out, ",\"returned_at\":", self.returned_at)?;
1328 out.write_str(",\"spans\":[")?;
1329 for (i, span) in self.spans.iter().enumerate() {
1330 out.write_str(if i > 0 { "," } else { "" })?;
1331 write!(out, "{{\"id\":{},\"parent\":", span.id)?;
1332 optional_number(out, span.parent.map(SpanId::get))?;
1333 out.write_str(",\"node\":")?;
1334 optional_number(out, span.node.map(NodeId::get))?;
1335 out.write_str(",\"task\":")?;
1336 quote(out, &span.task)?;
1337 write!(
1338 out,
1339 ",\"kind\":\"{}\",\"outcome\":\"{}\",\"start\":\"{}\",\"end\":\"{}\",\"checks\":{},\"reports\":{},\"units\":\"{}\",\"overflowed\":{},\"max_check_gap\":\"{}\",\"max_check_gap_start\":\"{}\",\"check_time\":\"{}\",\"clock_regressions\":{},\"unattributed_calls\":{}",
1340 span.kind.name(),
1341 outcome_name(span.outcome),
1342 span.start.as_nanos(),
1343 span.end.as_nanos(),
1344 span.stats.checks,
1345 span.stats.reports,
1346 span.stats.units,
1347 span.stats.overflowed,
1348 span.stats.max_check_gap.as_nanos(),
1349 span.stats.max_check_gap_start.as_nanos(),
1350 span.stats.check_time.as_nanos(),
1351 span.stats.clock_regressions,
1352 span.stats.unattributed_calls
1353 )?;
1354 optional_time(out, ",\"stopped_at\":", span.stats.stopped_at)?;
1355 out.write_str(",\"stop_reason\":")?;
1356 match span.stats.stop_reason {
1357 Some(reason) => write!(out, "\"{}\"", stop_reason_name(reason))?,
1358 None => out.write_str("null")?,
1359 }
1360 out.write_str(",\"max_report_gap\":")?;
1361 match &span.stats.max_report_gap {
1362 Some(gap) => {
1363 write!(
1364 out,
1365 "{{\"start\":\"{}\",\"duration\":\"{}\",\"checks\":{},\"max_check_gap\":\"{}\",\"from\":",
1366 gap.start.as_nanos(),
1367 gap.duration.as_nanos(),
1368 gap.checks,
1369 gap.max_check_gap.as_nanos()
1370 )?;
1371 optional_site(out, gap.from)?;
1372 out.write_str(",\"to\":")?;
1373 optional_site(out, gap.to)?;
1374 out.write_char('}')?;
1375 }
1376 None => out.write_str("null")?,
1377 }
1378 out.write_str(",\"unit_pace\":")?;
1379 match &span.stats.unit_pace {
1380 Some(pace) => {
1381 let q = &pace.quarters;
1382 write!(
1383 out,
1384 "[\"{}\",\"{}\",\"{}\",\"{}\"]",
1385 q[0].as_nanos(),
1386 q[1].as_nanos(),
1387 q[2].as_nanos(),
1388 q[3].as_nanos()
1389 )?;
1390 }
1391 None => out.write_str("null")?,
1392 }
1393 out.write_str(",\"sites\":[")?;
1394 for (j, site) in span.stats.sites.iter().enumerate() {
1395 out.write_str(if j > 0 { "," } else { "" })?;
1396 out.write_str("{\"file\":")?;
1397 quote(out, site.file)?;
1398 write!(
1399 out,
1400 ",\"line\":{},\"column\":{},\"checks\":{},\"reports\":{},\"units\":\"{}\",\"max_gap_before_check\":\"{}\",\"time\":\"{}\",\"active\":",
1401 site.line,
1402 site.column,
1403 site.checks,
1404 site.reports,
1405 site.units,
1406 site.max_gap_before_check.as_nanos(),
1407 site.time.as_nanos()
1408 )?;
1409 match site.active {
1410 Some((first, last)) => {
1411 write!(out, "[\"{}\",\"{}\"]}}", first.as_nanos(), last.as_nanos())?
1412 }
1413 None => out.write_str("null}")?,
1414 }
1415 }
1416 out.write_str("]}")?;
1417 }
1418 out.write_str("]}")
1419 }
1420}
1421
1422impl fmt::Display for Trace {
1423 fn fmt(&self, out: &mut fmt::Formatter<'_>) -> fmt::Result {
1424 writeln!(
1425 out,
1426 "how-far trace v{}: {} retained, {} active, {} dropped",
1427 self.schema_version,
1428 self.spans.len(),
1429 self.active_spans,
1430 self.dropped_spans
1431 )?;
1432 writeln!(
1433 out,
1434 "id\ttask\tkind\twall ms\tchecks\treports\tunits\tmax check gap ms\toutcome"
1435 )?;
1436 for span in &self.spans {
1437 writeln!(
1438 out,
1439 "{}\t{:?}\t{}\t{:.3}\t{}\t{}\t{}\t{:.3}\t{}",
1440 span.id,
1441 span.task,
1442 span.kind.name(),
1443 span.elapsed().as_secs_f64() * 1000.0,
1444 span.stats.checks,
1445 span.stats.reports,
1446 span.stats.units,
1447 span.stats.max_check_gap.as_secs_f64() * 1000.0,
1448 outcome_name(span.outcome)
1449 )?;
1450 }
1451 writeln!(out, "Times are wall-clock elapsed time, not CPU time.")
1452 }
1453}
1454
1455fn optional_time(out: &mut impl fmt::Write, key: &str, value: Option<Duration>) -> fmt::Result {
1456 out.write_str(key)?;
1457 match value {
1458 Some(time) => write!(out, "\"{}\"", time.as_nanos()),
1459 None => out.write_str("null"),
1460 }
1461}
1462
1463fn optional_number(out: &mut impl fmt::Write, value: Option<usize>) -> fmt::Result {
1464 match value {
1465 Some(number) => write!(out, "{number}"),
1466 None => out.write_str("null"),
1467 }
1468}
1469
1470fn optional_site(out: &mut impl fmt::Write, value: Option<SourceSite>) -> fmt::Result {
1471 match value {
1472 Some(site) => {
1473 out.write_str("{\"file\":")?;
1474 quote(out, site.file)?;
1475 write!(out, ",\"line\":{},\"column\":{}}}", site.line, site.column)
1476 }
1477 None => out.write_str("null"),
1478 }
1479}
1480
1481#[cfg(test)]
1482mod tests {
1483 use super::sorted_indices;
1484 use alloc::vec::Vec;
1485
1486 #[test]
1487 fn exhausted_span_identifiers_are_dropped_not_a_panic() {
1488 use super::{Profiler, SpanKind, StdClock};
1489 use core::sync::atomic::Ordering;
1490 use how_far::Outcome;
1491 let profiler = Profiler::new(StdClock::new(), 8);
1492 profiler
1493 .inner
1494 .next_id
1495 .store(usize::MAX - 1, Ordering::Relaxed);
1496 for _ in 0..3 {
1497 profiler
1498 .span(None, "work", SpanKind::Work)
1499 .finish(Outcome::Succeeded);
1500 }
1501 let trace = profiler.snapshot();
1502 assert_eq!(trace.spans.len(), 1);
1503 assert_eq!(trace.dropped_spans, 2);
1504 assert_eq!(trace.active_spans, 0);
1505 }
1506
1507 #[test]
1508 fn sorted_indices_is_a_stable_sort() {
1509 let mut seed = 0x2545_f491_4f6c_dd1d_u64;
1511 for len in [0, 1, 2, 3, 4, 5, 7, 8, 9, 16, 31, 33, 100, 257] {
1512 for keys in [2, 7, 1_000] {
1513 let values: Vec<u64> = (0..len)
1514 .map(|_| {
1515 seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
1516 (seed >> 33) % keys
1517 })
1518 .collect();
1519 let mut expected: Vec<usize> = (0..len).collect();
1520 expected.sort_by_key(|&i| values[i]); assert_eq!(
1522 sorted_indices(len, &|a, b| values[a].cmp(&values[b])),
1523 expected,
1524 "{len} values from {keys} keys"
1525 );
1526 }
1527 }
1528 }
1529}