Skip to main content

how_far_really/
profile.rs

1//! Measure how often work checks for cancellation and reports progress.
2//!
3//! Create one [`Span`] per logical task or chunk, then wrap that task's stop
4//! policy or progress sink with [`Span::instrument`]. The wrapper records every
5//! check and report at its original call site. One span per task matters: a
6//! single span shared by a pool would let one busy worker hide another
7//! worker's long silence. Separate span kinds record queueing, joins, host
8//! suspensions, and callbacks.
9//!
10//! Durations are wall time, **not CPU time**, read from the [`Clock`] you
11//! supply. Retention is bounded, and anything dropped is counted in every
12//! export. The collector uses OS mutexes, so it requires `std`; it belongs on
13//! native threads or browser workers. Browser UI readers use
14//! [`Profiler::try_snapshot`].
15//!
16//! # Stability
17//!
18//! The record types ([`Trace`], [`SpanRecord`], [`Stats`], [`SiteStats`],
19//! [`ReportGap`], ...) are `#[non_exhaustive]` with public fields. Later
20//! releases may add fields but will not rename, retype or remove them. JSON
21//! from [`Trace::write_json`] follows the same rule: readers must ignore keys
22//! they do not know, and `schema_version` changes only if a key is removed or
23//! its meaning changes.
24
25use 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
37/// A monotonic clock with one shared epoch. Only instrumented calls read it.
38///
39/// Browser hosts implement it over `performance.now()`. Methods added in
40/// later compatible releases will have default bodies.
41pub trait Clock: Send + Sync {
42    /// Time elapsed since the clock's epoch.
43    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/// The host's monotonic clock, with its epoch at construction.
52#[derive(Clone, Copy, Debug)]
53pub struct StdClock(std::time::Instant);
54impl StdClock {
55    /// Start an epoch now.
56    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/// What a span measures.
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
73#[non_exhaustive]
74pub enum SpanKind {
75    /// A logical task's work, including any nested waits or callbacks.
76    Work,
77    /// Work waiting in a queue before a worker claims it.
78    Queued,
79    /// Waiting, such as a coordinator's join.
80    Wait,
81    /// A host suspension, such as a JSPI await.
82    Yield,
83    /// An observer or application callback.
84    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/// A span's identity within one profiler.
100#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
101pub struct SpanId(usize);
102
103impl SpanId {
104    /// The identifier as a number, as written in JSON exports.
105    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/// A source location captured by an instrumented call.
117#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
118#[non_exhaustive]
119pub struct SourceSite {
120    /// Source file.
121    pub file: &'static str,
122    /// Line.
123    pub line: u32,
124    /// Column.
125    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/// Counts for one call site within one span.
145#[derive(Clone, Debug)]
146#[non_exhaustive]
147pub struct SiteStats {
148    /// Source file, captured through `#[track_caller]`.
149    pub file: &'static str,
150    /// Line.
151    pub line: u32,
152    /// Column.
153    pub column: u32,
154    /// Cancellation checks.
155    pub checks: u64,
156    /// Report calls.
157    pub reports: u64,
158    /// Units those reports added (saturating).
159    pub units: u64,
160    /// Longest interval that ended at a check at this site.
161    pub max_gap_before_check: Duration,
162    /// Wall time of the intervals that ended at this site: from the span's
163    /// previous check or timed report, or its entry, to this site's call.
164    /// Reports count only with [`Profiler::set_report_timing`] on.
165    pub time: Duration,
166    /// When this site's first and last timed calls were made, in the clock's
167    /// epoch; `None` if none were timed.
168    pub active: Option<(Duration, Duration)>,
169}
170
171impl SiteStats {
172    /// A timed call at `at`, ending an interval of `interval` since the
173    /// span's previous checkpoint (both in nanoseconds).
174    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/// The longest interval between reports in one span, recorded only when
185/// [`Profiler::set_report_timing`] is on.
186#[derive(Clone, Debug)]
187#[non_exhaustive]
188pub struct ReportGap {
189    /// Start of the interval, in the clock's epoch.
190    pub start: Duration,
191    /// Length of the interval.
192    pub duration: Duration,
193    /// Cancellation checks this span made inside the interval.
194    pub checks: u64,
195    /// Longest interval without a check inside it, boundaries included.
196    pub max_check_gap: Duration,
197    /// The report that opened the interval; `None` for span entry.
198    pub from: Option<SourceSite>,
199    /// The report that closed the interval; `None` for span exit.
200    pub to: Option<SourceSite>,
201}
202
203/// How long each quarter of one span's reported units took, recorded only
204/// when [`Profiler::set_report_timing`] is on and the span reported at least
205/// four times. An estimate: each quarter ends where its last unit was
206/// reported, interpolated between up to 32 sampled reports. The first starts
207/// at the span's start, so work before the first report counts toward it,
208/// and a report that read the clock before reports already recorded counts
209/// from its own reading on.
210#[derive(Clone, Debug)]
211#[non_exhaustive]
212pub struct UnitPace {
213    /// Wall time of each quarter of the units, in order.
214    pub quarters: [Duration; 4],
215}
216
217/// One span's checkpoint evidence. Gaps include entry and exit, so a span
218/// with no checks still records how long it ran unchecked.
219#[derive(Clone, Debug, Default)]
220#[non_exhaustive]
221pub struct Stats {
222    /// Cancellation checks (saturating).
223    pub checks: u64,
224    /// Report calls (saturating).
225    pub reports: u64,
226    /// Units reported (saturating).
227    pub units: u64,
228    /// Whether any count saturated; rates then lose their meaning.
229    pub overflowed: bool,
230    /// Longest interval without a check: entry to first check, check to
231    /// check, or last check to exit.
232    pub max_check_gap: Duration,
233    /// Start of that interval, in the clock's epoch.
234    pub max_check_gap_start: Duration,
235    /// Longest interval between reports, if report timing was on.
236    pub max_report_gap: Option<ReportGap>,
237    /// How long each quarter of the reported units took, if report timing
238    /// was on.
239    pub unit_pace: Option<UnitPace>,
240    /// Time spent inside instrumented checks, including any work they ran.
241    ///
242    /// Timing a check takes a second clock read, so a span times its first
243    /// 16 checks, every 16th after them, and every check that returned a
244    /// stop, and scales the timed total to all its checks. A slow check
245    /// that was not timed still shows in [`max_check_gap`](Self::max_check_gap),
246    /// which runs from one check's start to the next.
247    pub check_time: Duration,
248    /// When the first check that returned a stop error finished.
249    pub stopped_at: Option<Duration>,
250    /// That check's reason, which keeps cancellation apart from a timeout.
251    pub stop_reason: Option<StopReason>,
252    /// Times the clock went backwards within one call, or at finish. Workers
253    /// sharing a span can deliver readings out of order; that is expected,
254    /// counts as a zero gap, and is not a regression.
255    pub clock_regressions: u64,
256    /// Up to 64 distinct call sites.
257    pub sites: Vec<SiteStats>,
258    /// Calls from sites beyond the first 64. The totals above still count them.
259    pub unattributed_calls: u64,
260}
261
262/// One finished span.
263#[derive(Clone, Debug)]
264#[non_exhaustive]
265pub struct SpanRecord {
266    /// This span's identity.
267    pub id: SpanId,
268    /// The enclosing span, for spans made with [`Span::child`].
269    pub parent: Option<SpanId>,
270    /// The progress-tree phase this span belongs to, if any.
271    pub node: Option<NodeId>,
272    /// The task's label: a logical task, chunk or attempt, not an OS thread.
273    pub task: String,
274    /// What the span measures.
275    pub kind: SpanKind,
276    /// Start, in the clock's epoch.
277    pub start: Duration,
278    /// End, in the clock's epoch.
279    pub end: Duration,
280    /// How the span ended; a dropped span records `Abandoned`.
281    pub outcome: Outcome,
282    /// Checkpoint evidence.
283    pub stats: Stats,
284}
285
286impl SpanRecord {
287    /// Wall time from start to end, including nested spans. Do not add a
288    /// parent's time to its children's.
289    pub fn elapsed(&self) -> Duration {
290        self.end.saturating_sub(self.start)
291    }
292    /// Checks per second of this span's wall time.
293    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    /// Whether this span made at least `minimum_checks` checks at more than
301    /// `checks_per_second`. A high rate alone does not prove waste.
302    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/// A bounded, cloneable span collector. It starts no threads, keeps no global
319/// state, and reads only the clock you give it.
320#[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    /// Keep up to `capacity` finished spans. Later spans are dropped and
344    /// counted, so every export shows incomplete coverage.
345    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    /// Time reports as well as checks, in spans started from now on.
371    ///
372    /// Off by default because it reads the clock at every instrumented report.
373    /// With it on, each span records its longest [`ReportGap`].
374    /// [`DiagnosticPulse`](crate::diagnostics::DiagnosticPulse) turns it on.
375    pub fn set_report_timing(&self, enabled: bool) {
376        self.inner.report_timing.store(enabled, Ordering::Relaxed);
377    }
378
379    /// Record a key/value pair, such as a configuration or build identity.
380    /// A repeated key replaces the earlier value.
381    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    /// Start a span for one task. `node` ties it to a progress-tree phase; pass
393    /// `None` for work outside a tree.
394    ///
395    /// ```
396    /// use how_far_along::{Outcome, Unstoppable};
397    /// use how_far_really::profile::{Profiler, SpanKind, StdClock};
398    ///
399    /// let profiler = Profiler::new(StdClock::new(), 16);
400    /// let span = profiler.span(None, "decode", SpanKind::Work);
401    /// let stop = span.instrument(Unstoppable);
402    /// // decode(&input, &stop)?;
403    /// span.finish(Outcome::Succeeded);
404    /// assert_eq!(profiler.snapshot().spans.len(), 1);
405    /// ```
406    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    /// Run one callback inside a `Callback` span. A panic records the span as
416    /// abandoned and keeps unwinding.
417    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    /// The clock's current reading.
425    pub(crate) fn now(&self) -> Duration {
426        self.inner.clock.now()
427    }
428
429    /// `entered`, if known, is when the span began, before its first checkpoint.
430    #[allow(deprecated)] // Atomic::try_update is newer than the Rust 1.88 MSRV.
431    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        // Saturates: a span past the last identifier is counted as dropped,
440        // never a panic inside an instrumented check.
441        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    /// Record when cancellation was actually requested. Call it next to the
469    /// application's cancel call, not when a callback notices.
470    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    /// Record when the operation returned, after every worker joined and
477    /// cleanup finished.
478    pub fn operation_returned(&self) {
479        let now = self.inner.clock.now();
480        self.inner.state.lock().returned_at = Some(now);
481    }
482
483    /// Copy the finished spans. Active and dropped counts show what is missing.
484    pub fn snapshot(&self) -> Trace {
485        let state = self.inner.state.lock();
486        self.snapshot_from(&state)
487    }
488
489    /// [`snapshot`](Self::snapshot) without waiting: `None` if another thread
490    /// holds the collector's lock. UI threads keep their last trace and retry.
491    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
503/// A reading in nanoseconds of the clock's epoch: hot state is kept in
504/// these, which compare and subtract in one instruction, and `finish` turns
505/// them into the public `Duration`s. Exact below 584 years.
506fn 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    /// Checks whose duration `check_time` sums so far.
515    timed_checks: u64,
516    check_time: u64,
517    max_check_gap: u64,
518    max_check_gap_start: u64,
519    /// `stats.max_report_gap`'s duration.
520    max_report_gap: Option<u64>,
521    last_check: u64,
522    last_report: u64,
523    last_report_site: Option<SourceSite>,
524    /// Timed reports as (time, units so far): every `stride`th one, at most
525    /// `PACE_SAMPLES`, and the latest. `stats.unit_pace` is read from them.
526    start: u64,
527    samples: Vec<(u64, u64)>,
528    stride: u64,
529    until_sample: u64,
530    latest: Option<(u64, u64)>,
531    /// Checks since the last report, to tell a coarse reporting unit from
532    /// missing cancellation checks.
533    window: Window,
534    /// `stats.sites[i].max_gap_before_check`.
535    site_gaps: Vec<u64>,
536    /// Indices in `stats.sites` of the last check's and the last report's
537    /// sites, tried first: a loop that steps alternates between the two.
538    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    /// End the interval since the last report at `end`, keeping it if longest.
569    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    /// A timed report that read the clock at `at`, before reports already
585    /// recorded (a worker preempted between reading the clock and locking):
586    /// its `units` count in every sample taken after it.
587    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    /// Keep a timed report reaching `units` at `now` for `stats.unit_pace`.
599    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            // Keep every other sample, and sample half as often from now on.
607            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    /// How long each quarter of the sampled units took; see [`UnitPace`].
619    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        // Entry, the samples, then the latest report.
626        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    /// The statistics as of now, with the timed checks' total scaled to all
655    /// of them; see [`Stats::check_time`].
656    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    /// Close the interval at `end` and start the next one there.
693    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, to choose which ones to time without the lock. A wrap
711    /// only times a few more.
712    checks_started: AtomicUsize,
713    state: Mutex<SpanState>,
714}
715
716/// A span times its first this-many checks, then every this-many-th.
717const TIMED_CHECKS: usize = 16;
718/// Timed reports a span keeps for [`UnitPace`].
719const PACE_SAMPLES: usize = 32;
720
721impl SpanInner {
722    fn now(&self) -> Duration {
723        self.profiler.inner.clock.now()
724    }
725
726    /// Whether to time the check about to start: see [`Stats::check_time`].
727    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    /// Record a check that started at `start` and just returned `result`,
733    /// at `end` if it was timed. Shared by every [`Instrumented`] type, so it
734    /// is compiled once.
735    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            // The span's statistics are final, but the stop was still observed.
745            drop(state);
746            self.observed(result, end);
747            return;
748        }
749        let start = nanos(start);
750        // A worker sharing this span may have recorded a later reading
751        // first; an out-of-order arrival is a zero gap, not a regression.
752        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            // Borrow the guard's fields apart.
780            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    /// Record when an instrumented check first returned a stop.
798    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    /// Record a report of `completed` units. Compiled once, like
806    /// [`record_check`](Self::record_check).
807    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        // Borrow the guard's fields apart.
841        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/// The owner of one running span. Finish it after its instrumented calls
906/// return; dropping it unfinished, including during unwinding, records
907/// `Abandoned`.
908#[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    /// This span's identity.
925    pub fn id(&self) -> SpanId {
926        self.inner.id
927    }
928    /// Start a nested span, such as a callback, queue wait, or host suspension.
929    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    /// Wrap a stop policy or progress sink so that its calls are recorded in
935    /// this span, at their original call sites.
936    pub fn instrument<T>(&self, value: T) -> Instrumented<T> {
937        Instrumented {
938            value,
939            span: self.shared(),
940        }
941    }
942    /// The recording state, for wrappers that record through
943    /// [`checked`] and [`advanced`].
944    pub(crate) fn shared(&self) -> Arc<SpanInner> {
945        Arc::clone(&self.inner)
946    }
947    /// Record how the span ended. Wrappers keep working afterwards but stop
948    /// recording.
949    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/// A stop policy or progress sink whose calls are recorded in a span.
964///
965/// It is `'static` whenever the wrapped value is, so it can go wherever the
966/// original went: into a codec context, a spawned thread, or an
967/// `Arc<dyn Stop>`. `may_stop` and `may_report` always return `true`, so
968/// callers that skip no-op policies still make the calls being measured.
969#[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    /// The wrapped value. Calls made through it are not recorded.
986    pub fn inner(&self) -> &T {
987        &self.value
988    }
989}
990
991/// The indices `0..len`, stably ordered by `compare`: equal elements keep
992/// their index order.
993///
994/// Every sort in this crate's analysis code goes through this one bottom-up
995/// merge sort. The standard library's sorts compile to thousands of lines per
996/// element type and comparator, which this cold code does not need.
997pub(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/// Whether `site` is the call site at `at`: cheap fields first, and one
1039/// file's sites share its path's address.
1040#[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/// The index in `stats.sites` of `at`, trying `hint` first and leaving it
1048/// there; `None` past 64 sites.
1049#[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/// `value.check()`, recorded in `span` at the caller's location. Every
1098/// instrumented type calls this one body, so instrumenting another type adds
1099/// a few lines, not a copy of the bookkeeping.
1100#[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(); // User policy never runs under our locks.
1105    let end = (timed || result.is_err()).then(|| span.now());
1106    span.record_check(start, end, result, Location::caller());
1107    result
1108}
1109
1110/// `value.advance(completed)`, recorded in `span` like [`checked`].
1111#[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/// A finished run's spans, optionally with its progress tree attached.
1132#[derive(Clone, Debug)]
1133#[non_exhaustive]
1134pub struct Trace {
1135    /// Bounded reporting-protocol incidents, independent from work outcomes.
1136    pub incidents: Vec<crate::diagnostics::Incident>,
1137    /// Incidents omitted because their separate retention budget was exhausted.
1138    pub dropped_incidents: u64,
1139
1140    /// Version of the JSON format.
1141    pub schema_version: u32,
1142    /// The progress tree, attached with [`with_progress`](Self::with_progress).
1143    pub progress: Option<how_far_along::Snapshot>,
1144    /// Key/value pairs from [`Profiler::metadata`].
1145    pub metadata: Vec<(String, String)>,
1146    /// Retained finished spans, in the order they finished.
1147    pub spans: Vec<SpanRecord>,
1148    /// Finished spans beyond the profiler's capacity.
1149    pub dropped_spans: u64,
1150    /// Spans still running when the trace was taken.
1151    pub active_spans: usize,
1152    /// When cancellation was requested, if recorded.
1153    pub cancelled_at: Option<Duration>,
1154    /// When an instrumented check first returned a stop error.
1155    pub observed_at: Option<Duration>,
1156    /// When the operation returned, if recorded.
1157    pub returned_at: Option<Duration>,
1158}
1159
1160/// How a chosen set of independent work spans overlapped in time.
1161///
1162/// It measures concurrency, not CPU utilization.
1163#[derive(Clone, Debug, PartialEq)]
1164#[non_exhaustive]
1165pub struct Overlap {
1166    /// First start to last end, gaps included.
1167    pub wall: Duration,
1168    /// Sum of the spans' own durations.
1169    pub task_time: Duration,
1170    /// `task_time / wall`: the average number of spans running.
1171    pub mean_active_tasks: f64,
1172    /// The most spans running at once.
1173    pub peak_active_tasks: usize,
1174    /// The final stretch during which exactly one span was running.
1175    pub single_task_tail: Duration,
1176}
1177
1178impl Trace {
1179    /// Attach the progress tree, usually a final snapshot taken after every
1180    /// worker joined.
1181    pub fn with_progress(mut self, progress: how_far_along::Snapshot) -> Self {
1182        self.progress = Some(progress);
1183        self
1184    }
1185
1186    /// Measure how the given work spans overlapped.
1187    ///
1188    /// Returns `None` for an empty, missing or repeated ID, a span that is not
1189    /// `Work`, a span with clock regressions, or a span together with one of
1190    /// its ancestors (which would count time twice).
1191    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    /// The retained span with this identity.
1253    #[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    /// Time from the cancellation request to the first check that saw it.
1267    pub fn cancellation_observation_latency(&self) -> Option<Duration> {
1268        self.observed_at?.checked_sub(self.cancelled_at?)
1269    }
1270
1271    /// Time from the cancellation request to the operation's return,
1272    /// including stragglers, joins, and cleanup.
1273    pub fn cancellation_return_latency(&self) -> Option<Duration> {
1274        self.returned_at?.checked_sub(self.cancelled_at?)
1275    }
1276
1277    /// Write the trace as JSON.
1278    ///
1279    /// Times are nanoseconds since the clock's epoch, written as decimal
1280    /// strings so 64-bit values survive JavaScript. Labels are escaped. Later
1281    /// versions may add keys; readers should ignore keys they do not know.
1282    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            // Stable wire names; Debug text remains human-readable evidence only.
1308            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        // A small LCG, so the test needs no dependencies and is reproducible.
1510        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]); // The standard stable sort.
1521                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}