Skip to main content

turbopack_trace_utils/
raw_trace.rs

1use std::{
2    borrow::Cow,
3    cell::{Cell, RefCell, RefMut},
4    fmt::Write,
5    marker::PhantomData,
6    sync::atomic::{AtomicU64, Ordering},
7    thread,
8    time::Instant,
9};
10
11use thread_local::ThreadLocal;
12use tracing::{
13    Subscriber,
14    field::{Visit, display},
15    span,
16};
17use tracing_subscriber::{Layer, registry::LookupSpan};
18use turbo_tasks_malloc::TurboMalloc;
19
20use crate::{
21    flavor::WriteGuardFlavor,
22    tokio_workers::active_worker_threads,
23    trace_writer::{TraceWriter, WriteGuard},
24    tracing::{Allocations, TraceRow, TraceValue},
25};
26
27/// 10ms in microseconds
28const MEMORY_SAMPLE_INTERVAL_US: u64 = 10_000;
29
30/// 1000ms in microseconds. Reading the process memory footprint is a syscall
31/// (or a `/proc` read on Linux), so it is refreshed less often than the other
32/// memory sample values; samples in between repeat the last reading.
33const MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US: u64 = 1_000_000;
34
35static GLOBAL_LAST_MEMORY_SAMPLE: AtomicU64 = AtomicU64::new(0);
36
37static MEMORY_FOOTPRINT_THROTTLE: FootprintThrottle = FootprintThrottle::new();
38
39/// Caches the process memory footprint and refreshes it at most once per
40/// [`MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US`].
41struct FootprintThrottle {
42    /// Timestamp of the last claimed read, or [`FootprintThrottle::NEVER`].
43    last_read_ts: AtomicU64,
44    /// The most recently read value (`0` until the first read completes).
45    value: AtomicU64,
46}
47
48impl FootprintThrottle {
49    const NEVER: u64 = u64::MAX;
50
51    const fn new() -> Self {
52        Self {
53            last_read_ts: AtomicU64::new(Self::NEVER),
54            value: AtomicU64::new(0),
55        }
56    }
57
58    /// Returns the footprint for a sample at `ts`. Calls `read` only when no
59    /// read happened yet or the last one is at least
60    /// [`MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US`] old, and only if this caller
61    /// wins the claim on `last_read_ts`. All other callers (including
62    /// concurrent ones while a read is in progress) get the cached value.
63    fn get(&self, ts: u64, read: impl FnOnce() -> u64) -> u64 {
64        let last = self.last_read_ts.load(Ordering::Relaxed);
65        if last != Self::NEVER && ts.saturating_sub(last) < MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US {
66            return self.value.load(Ordering::Relaxed);
67        }
68        if self
69            .last_read_ts
70            .compare_exchange(last, ts, Ordering::Relaxed, Ordering::Relaxed)
71            .is_err()
72        {
73            // Another caller claimed this read.
74            return self.value.load(Ordering::Relaxed);
75        }
76        let value = read();
77        self.value.store(value, Ordering::Relaxed);
78        value
79    }
80}
81
82thread_local! {
83    static THREAD_LOCAL_LAST_MEMORY_SAMPLE: Cell<u64> = const { Cell::new(0) };
84}
85
86/// Options for [`RawTraceLayer`].
87#[derive(Clone, Debug)]
88pub struct RawTraceLayerOptions {
89    /// Track memory: attach the per-thread allocation counters to every span enter and exit
90    /// and write periodic process memory samples (`MemorySample` rows).
91    /// These make up a large part of the trace size, so disabling them significantly reduces
92    /// trace file size, at the cost of losing all memory information. Defaults to `true`.
93    pub memory: bool,
94}
95
96impl Default for RawTraceLayerOptions {
97    fn default() -> Self {
98        Self { memory: true }
99    }
100}
101
102struct RawTraceLayerExtension {
103    id: u64,
104}
105
106fn get_id<S: Subscriber + for<'a> LookupSpan<'a>>(
107    ctx: tracing_subscriber::layer::Context<'_, S>,
108    id: &span::Id,
109) -> u64 {
110    ctx.span(id)
111        .unwrap()
112        .extensions()
113        .get::<RawTraceLayerExtension>()
114        .unwrap()
115        .id
116}
117
118/// The maximum gap between an `Exit` and the next `Enter` of the same span for which both rows
119/// are omitted. `entered` is the time between the last `Enter` row written on the thread (of any
120/// span, omitted `Enter` rows don't count) and the `Exit`.
121///
122/// Omitting the rows makes the trace show the span as entered during the gap, so its self time
123/// gains the gap. This is bounded to 1µs (the timestamp resolution) or 0.1% of the preceding
124/// entered interval (which includes time spent in nested spans and earlier omitted gaps). For
125/// nested spans, the last `Enter` on the thread can be of a nested span, which is later than the
126/// span's own `Enter`, so that only makes the allowed gap smaller.
127fn max_exit_enter_gap(entered: u64) -> u64 {
128    (entered / 1000).max(1)
129}
130
131/// A tracing layer that writes raw trace data to a writer. We store data using the [`TraceRow`],
132/// serialized with [`postcard`], with timestamps encoded as described in the docs of
133/// [`TraceRow`].
134pub struct RawTraceLayer<S: Subscriber + for<'a> LookupSpan<'a>> {
135    trace_writer: TraceWriter,
136    start: Instant,
137    next_id: AtomicU64,
138    memory: bool,
139    /// Reads the allocation counters of the current thread. Only replaced in tests.
140    allocation_counters: fn() -> Allocations,
141    /// Returns the current timestamp in microseconds since `start`. Only replaced in tests.
142    clock: fn(Instant) -> u64,
143    threads: ThreadLocal<RefCell<ThreadState>>,
144    _phantom: PhantomData<fn(S)>,
145}
146
147/// Per-thread state of the [`RawTraceLayer`].
148#[derive(Default)]
149struct ThreadState {
150    /// The thread this state belongs to. Slots of [`ThreadLocal`] are reused by new threads
151    /// after a thread exited, so the state is reset when this doesn't match.
152    thread_id: u64,
153    /// The allocation counters of the thread at the time they were last reported.
154    reported_allocations: Option<Allocations>,
155    /// The timestamp of the last `Enter` row written on this thread, of any span. Omitted `Enter`
156    /// rows don't update it. See [`max_exit_enter_gap`].
157    last_enter_ts: Option<u64>,
158    /// The last `Exit` row written on this thread. It can be removed again when it's followed
159    /// by an `Enter` of the same span shortly after, see [`max_exit_enter_gap`]. The row is
160    /// marked with the span id, see [`crate::trace_writer::WriteGuard::mark`].
161    last_exit: Option<LastExit>,
162}
163
164#[derive(Clone, Copy)]
165struct LastExit {
166    id: u64,
167    ts: u64,
168    /// [`ThreadState::reported_allocations`] before the `Exit` row was written.
169    reported_allocations_before: Option<Allocations>,
170}
171
172fn thread_allocation_counters() -> Allocations {
173    let counters = TurboMalloc::allocation_counters();
174    Allocations {
175        allocations: counters.allocations as u64,
176        allocation_count: counters.allocation_count as u64,
177        deallocations: counters.deallocations as u64,
178        deallocation_count: counters.deallocation_count as u64,
179    }
180}
181
182impl<S: Subscriber + for<'a> LookupSpan<'a>> RawTraceLayer<S> {
183    /// Creates a layer with the default [`RawTraceLayerOptions`].
184    pub fn new(trace_writer: TraceWriter) -> Self {
185        Self::with_options(trace_writer, RawTraceLayerOptions::default())
186    }
187
188    pub fn with_options(trace_writer: TraceWriter, options: RawTraceLayerOptions) -> Self {
189        let RawTraceLayerOptions { memory } = options;
190        Self {
191            trace_writer,
192            start: Instant::now(),
193            next_id: AtomicU64::new(1),
194            memory,
195            allocation_counters: thread_allocation_counters,
196            clock: |start| start.elapsed().as_micros() as u64,
197            threads: ThreadLocal::new(),
198            _phantom: PhantomData,
199        }
200    }
201
202    fn now(&self) -> u64 {
203        (self.clock)(self.start)
204    }
205
206    /// Returns the state of the current thread, which has the id `thread_id`.
207    fn thread_state(&self, thread_id: u64) -> RefMut<'_, ThreadState> {
208        let mut state = self.threads.get_or_default().borrow_mut();
209        if state.thread_id != thread_id {
210            *state = ThreadState {
211                thread_id,
212                ..Default::default()
213            };
214        }
215        state
216    }
217
218    /// Writes a row with an absolute timestamp (if any). The timestamp gets encoded here, see
219    /// "Timestamps" in the docs of [`TraceRow`].
220    fn write(&self, data: TraceRow<'_, u64>) {
221        let start = TurboMalloc::allocation_counters();
222        let mut guard = self.trace_writer.start_write();
223        let data = Self::encode_timestamp(&mut guard, data);
224        postcard::serialize_with_flavor(&data, WriteGuardFlavor { guard: &mut guard }).unwrap();
225        drop(guard);
226        TurboMalloc::reset_allocation_counters(start);
227    }
228
229    /// Writes a row and marks it with `marker`, so that it can be removed by the next write on
230    /// this thread. See [`crate::trace_writer::WriteGuard::mark`].
231    fn write_marked(&self, data: TraceRow<'_, u64>, marker: u64) {
232        let start = TurboMalloc::allocation_counters();
233        let mut guard = self.trace_writer.start_write();
234        // The `TimestampBase` row written for the row is part of the mark
235        guard.mark(marker, |guard| {
236            let data = Self::encode_timestamp(guard, data);
237            postcard::serialize_with_flavor(&data, WriteGuardFlavor { guard }).unwrap()
238        });
239        drop(guard);
240        TurboMalloc::reset_allocation_counters(start);
241    }
242
243    /// Converts `data` into its serialized form, writing a [`TraceRow::TimestampBase`] before it
244    /// when one is needed, see [`WriteGuard::encode_timestamp`].
245    fn encode_timestamp<'a>(guard: &mut WriteGuard<'_>, data: TraceRow<'a, u64>) -> TraceRow<'a> {
246        data.map_timestamp(|ts| guard.encode_timestamp(ts))
247    }
248
249    /// Removes the last row written on this thread, if it is still in the thread local buffer
250    /// and is marked with `marker`. Returns `true` when it was removed.
251    fn remove_last_row(&self, marker: u64) -> bool {
252        let start = TurboMalloc::allocation_counters();
253        let mut guard = self.trace_writer.start_write();
254        let removed = guard.remove_last_row(marker);
255        drop(guard);
256        TurboMalloc::reset_allocation_counters(start);
257        removed
258    }
259
260    fn maybe_report_memory_sample(&self, ts: u64) {
261        // Fast thread-local check
262        let skip = THREAD_LOCAL_LAST_MEMORY_SAMPLE
263            .with(|tl| ts.saturating_sub(tl.get()) < MEMORY_SAMPLE_INTERVAL_US);
264        if skip {
265            return;
266        }
267
268        // Check global atomic
269        let global_last = GLOBAL_LAST_MEMORY_SAMPLE.load(Ordering::Relaxed);
270        if ts.saturating_sub(global_last) < MEMORY_SAMPLE_INTERVAL_US {
271            // Another thread sampled recently; update thread-local cache
272            THREAD_LOCAL_LAST_MEMORY_SAMPLE.with(|tl| tl.set(global_last));
273            return;
274        }
275
276        // Try to atomically claim the sample
277        match GLOBAL_LAST_MEMORY_SAMPLE.compare_exchange(
278            global_last,
279            ts,
280            Ordering::Relaxed,
281            Ordering::Relaxed,
282        ) {
283            Ok(_) => {
284                // We won the race — write the sample
285                THREAD_LOCAL_LAST_MEMORY_SAMPLE.with(|tl| tl.set(ts));
286                let memory = TurboMalloc::memory_usage() as u64;
287                let memory_pressure = TurboMalloc::memory_pressure().unwrap_or(0);
288                let memory_footprint = MEMORY_FOOTPRINT_THROTTLE.get(ts, || {
289                    TurboMalloc::memory_footprint()
290                        .map(|v| v as u64)
291                        .unwrap_or(0)
292                });
293                self.write(TraceRow::MemorySample {
294                    ts,
295                    memory,
296                    memory_pressure,
297                    memory_footprint,
298                    active_worker_threads: active_worker_threads() as u64,
299                });
300            }
301            Err(actual) => {
302                // Lost the race; update thread-local with the winner's timestamp
303                THREAD_LOCAL_LAST_MEMORY_SAMPLE.with(|tl| tl.set(actual));
304            }
305        }
306    }
307
308    /// Returns the allocations to attach to an Enter/Exit row: what the current thread
309    /// (de)allocated since its previous report, given the `current` counters of the thread.
310    /// Returns `None` when memory isn't tracked, on the first report of a thread (which only
311    /// establishes the baseline), and when nothing was (de)allocated since the previous report.
312    fn allocations(state: &mut ThreadState, current: Option<Allocations>) -> Option<Allocations> {
313        let current = current?;
314        let previous = state.reported_allocations.replace(current)?;
315        let delta = Allocations {
316            allocations: current.allocations.saturating_sub(previous.allocations),
317            allocation_count: current
318                .allocation_count
319                .saturating_sub(previous.allocation_count),
320            deallocations: current.deallocations.saturating_sub(previous.deallocations),
321            deallocation_count: current
322                .deallocation_count
323                .saturating_sub(previous.deallocation_count),
324        };
325        (delta != Allocations::default()).then_some(delta)
326    }
327}
328
329impl<S: Subscriber + for<'a> LookupSpan<'a>> Layer<S> for RawTraceLayer<S> {
330    fn on_new_span(
331        &self,
332        attrs: &span::Attributes<'_>,
333        id: &span::Id,
334        ctx: tracing_subscriber::layer::Context<'_, S>,
335    ) {
336        let ts = self.now();
337        let mut values = ValuesVisitor::new();
338        attrs.values().record(&mut values);
339        let external_id = self
340            .next_id
341            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
342        ctx.span(id)
343            .unwrap()
344            .extensions_mut()
345            .insert(RawTraceLayerExtension { id: external_id });
346        self.write(TraceRow::Start {
347            ts,
348            id: external_id,
349            parent: if attrs.is_contextual() {
350                ctx.current_span().id().map(|p| get_id(ctx, p))
351            } else {
352                attrs.parent().map(|p| get_id(ctx, p))
353            },
354            name: attrs.metadata().name().into(),
355            target: attrs.metadata().target().into(),
356            values: values.values,
357        });
358    }
359
360    fn on_close(&self, id: span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
361        let ts = self.now();
362        self.write(TraceRow::End {
363            ts,
364            id: get_id(ctx, &id),
365        });
366    }
367
368    fn on_enter(&self, id: &span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
369        // Allocations made by the tracing itself are excluded from the counters.
370        let malloc_counters = TurboMalloc::allocation_counters();
371        let current = self.memory.then(self.allocation_counters);
372        let ts = self.now();
373        let thread_id = thread::current().id().as_u64().into();
374        let id = get_id(ctx, id);
375        let enter_allocations = {
376            let mut state = self.thread_state(thread_id);
377            // An `Exit` directly followed by an `Enter` of the same span (almost) at the same
378            // time has (almost) no effect on the trace data, so both rows can be omitted. This is
379            // common for async spans, which are exited and entered again on every poll.
380            if let Some(last_exit) = state.last_exit.take()
381                && last_exit.id == id
382                && last_exit.ts <= ts
383                && ts
384                    <= last_exit.ts.saturating_add(max_exit_enter_gap(
385                        last_exit
386                            .ts
387                            .saturating_sub(state.last_enter_ts.unwrap_or(last_exit.ts)),
388                    ))
389                // Nothing (de)allocated since the exit, otherwise the `Enter` would need to
390                // report it for the span that was running in between
391                && state.reported_allocations == current
392                // The `Exit` row is marked with the span id
393                && self.remove_last_row(id)
394            {
395                // What the span (de)allocated before the removed `Exit` is reported with the next
396                // row of this thread instead. The span is on top of the stack again at that
397                // point, so it's still attributed to it.
398                state.reported_allocations = last_exit.reported_allocations_before;
399                None
400            } else {
401                state.last_enter_ts = Some(ts);
402                Some(Self::allocations(&mut state, current))
403            }
404        };
405        // After the `Exit` row was removed, as the sample would be the last row otherwise
406        if self.memory {
407            self.maybe_report_memory_sample(ts);
408        }
409        // `None` when the rows were omitted
410        if let Some(allocations) = enter_allocations {
411            self.write(TraceRow::Enter {
412                ts,
413                id,
414                thread_id,
415                allocations,
416            });
417        }
418        TurboMalloc::reset_allocation_counters(malloc_counters);
419    }
420
421    fn on_exit(&self, id: &span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) {
422        // Allocations made by the tracing itself are excluded from the counters.
423        let malloc_counters = TurboMalloc::allocation_counters();
424        let current = self.memory.then(self.allocation_counters);
425        let ts = self.now();
426        let thread_id = thread::current().id().as_u64().into();
427        let id = get_id(ctx, id);
428        let allocations = {
429            let mut state = self.thread_state(thread_id);
430            let reported_allocations_before = state.reported_allocations;
431            let allocations = Self::allocations(&mut state, current);
432            state.last_exit = Some(LastExit {
433                id,
434                ts,
435                reported_allocations_before,
436            });
437            allocations
438        };
439        self.write_marked(
440            TraceRow::Exit {
441                ts,
442                id,
443                thread_id,
444                allocations,
445            },
446            id,
447        );
448        TurboMalloc::reset_allocation_counters(malloc_counters);
449    }
450
451    fn on_event(&self, event: &tracing::Event<'_>, ctx: tracing_subscriber::layer::Context<'_, S>) {
452        let ts = self.now();
453        let mut values = ValuesVisitor::new();
454        event.record(&mut values);
455        self.write(TraceRow::Event {
456            ts,
457            parent: if event.is_contextual() {
458                ctx.current_span().id().map(|p| get_id(ctx, p))
459            } else {
460                event.parent().map(|p| get_id(ctx, p))
461            },
462            values: values.values,
463        });
464    }
465
466    fn on_record(
467        &self,
468        id: &span::Id,
469        record: &span::Record<'_>,
470        ctx: tracing_subscriber::layer::Context<'_, S>,
471    ) {
472        let mut values = ValuesVisitor::new();
473        record.record(&mut values);
474        self.write(TraceRow::Record {
475            id: get_id(ctx, id),
476            values: values.values,
477        });
478    }
479}
480
481struct ValuesVisitor {
482    values: Vec<(Cow<'static, str>, TraceValue<'static>)>,
483}
484
485impl ValuesVisitor {
486    fn new() -> Self {
487        Self { values: Vec::new() }
488    }
489}
490
491impl Visit for ValuesVisitor {
492    fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
493        let mut str = String::new();
494        let _ = write!(str, "{value:?}");
495        self.values
496            .push((field.name().into(), TraceValue::String(str.into())));
497    }
498
499    fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
500        self.values
501            .push((field.name().into(), TraceValue::Float(value)));
502    }
503
504    fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
505        self.values
506            .push((field.name().into(), TraceValue::Int(value)));
507    }
508
509    fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
510        self.values
511            .push((field.name().into(), TraceValue::UInt(value)));
512    }
513
514    fn record_i128(&mut self, field: &tracing::field::Field, value: i128) {
515        self.record_debug(field, &value)
516    }
517
518    fn record_u128(&mut self, field: &tracing::field::Field, value: u128) {
519        self.record_debug(field, &value)
520    }
521
522    fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
523        self.values
524            .push((field.name().into(), TraceValue::Bool(value)));
525    }
526
527    fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
528        self.values.push((
529            field.name().into(),
530            TraceValue::String(value.to_string().into()),
531        ));
532    }
533
534    fn record_error(
535        &mut self,
536        field: &tracing::field::Field,
537        value: &(dyn std::error::Error + 'static),
538    ) {
539        self.record_debug(field, &display(value))
540    }
541}
542
543#[cfg(test)]
544pub(crate) mod tests {
545    use std::{
546        cell::Cell,
547        io::{self, Write},
548        sync::{
549            Arc, Barrier, Mutex,
550            atomic::{AtomicUsize, Ordering},
551        },
552        thread,
553        time::Duration,
554    };
555
556    use tracing_subscriber::{Registry, layer::SubscriberExt};
557
558    use crate::{
559        raw_trace::{
560            FootprintThrottle, MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US, RawTraceLayer,
561            RawTraceLayerOptions,
562        },
563        trace_writer::TraceWriter,
564        tracing::{Allocations, TRACE_HEADER, TimestampDecoder, TraceRow},
565    };
566
567    thread_local! {
568        /// Fake allocation counters of the current thread, controlled by the test.
569        static FAKE_ALLOCATIONS: Cell<Allocations> = const {
570            Cell::new(Allocations {
571                allocations: 0,
572                allocation_count: 0,
573                deallocations: 0,
574                deallocation_count: 0,
575            })
576        };
577        /// Fake time of the current thread, controlled by the test.
578        static FAKE_TIME: Cell<u64> = const { Cell::new(0) };
579        /// How much the fake time advances on every read. Tests can set it to 0 to freeze it.
580        static FAKE_TIME_STEP: Cell<u64> = const { Cell::new(1) };
581    }
582
583    fn fake_clock(_start: std::time::Instant) -> u64 {
584        let step = FAKE_TIME_STEP.with(|s| s.get());
585        FAKE_TIME.with(|t| {
586            let time = t.get();
587            t.set(time + step);
588            time
589        })
590    }
591
592    /// Freezes the fake time of the current thread at `time`.
593    fn freeze_time(time: u64) {
594        FAKE_TIME.with(|t| t.set(time));
595        FAKE_TIME_STEP.with(|s| s.set(0));
596    }
597
598    fn fake_allocation_counters() -> Allocations {
599        FAKE_ALLOCATIONS.with(|c| c.get())
600    }
601
602    /// Simulates `count` allocations of `bytes` bytes each on the current thread.
603    pub(crate) fn fake_allocate(bytes: u64, count: u64) {
604        FAKE_ALLOCATIONS.with(|c| {
605            let mut counters = c.get();
606            counters.allocations += bytes * count;
607            counters.allocation_count += count;
608            c.set(counters);
609        });
610    }
611
612    /// Simulates `count` deallocations of `bytes` bytes each on the current thread.
613    pub(crate) fn fake_deallocate(bytes: u64, count: u64) {
614        FAKE_ALLOCATIONS.with(|c| {
615            let mut counters = c.get();
616            counters.deallocations += bytes * count;
617            counters.deallocation_count += count;
618            c.set(counters);
619        });
620    }
621
622    #[derive(Clone, Default)]
623    struct SharedBuffer(Arc<Mutex<Vec<u8>>>);
624
625    impl Write for SharedBuffer {
626        fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
627            self.0.lock().unwrap().extend_from_slice(buf);
628            Ok(buf.len())
629        }
630
631        fn flush(&mut self) -> io::Result<()> {
632            Ok(())
633        }
634    }
635
636    /// Runs `f` with a [`RawTraceLayer`] installed as the thread's default subscriber and
637    /// returns the complete trace file content. Allocation counters are read from the fake
638    /// counters controlled by [`fake_allocate`] and [`fake_deallocate`].
639    pub(crate) fn capture(options: RawTraceLayerOptions, f: impl FnOnce()) -> Vec<u8> {
640        FAKE_TIME_STEP.with(|s| s.set(1));
641        let buffer = SharedBuffer::default();
642        let (trace_writer, guard) = TraceWriter::new(buffer.clone());
643        let mut layer = RawTraceLayer::with_options(trace_writer, options);
644        layer.allocation_counters = fake_allocation_counters;
645        layer.clock = fake_clock;
646        let subscriber = Registry::default().with(layer);
647        tracing::subscriber::with_default(subscriber, f);
648        // Flushes all buffers and waits for the writer thread to finish.
649        drop(guard);
650        Arc::try_unwrap(buffer.0).unwrap().into_inner().unwrap()
651    }
652
653    /// Decodes the rows of a trace file, skipping the header. Timestamps are resolved to absolute
654    /// values and the [`TraceRow::TimestampBase`] rows are omitted.
655    pub(crate) fn decode(data: &[u8]) -> Vec<TraceRow<'_, u64>> {
656        decode_all(data)
657            .into_iter()
658            .filter(|row| !matches!(row, TraceRow::TimestampBase { .. }))
659            .collect()
660    }
661
662    /// Decodes all rows of a trace file including [`TraceRow::TimestampBase`] rows, skipping the
663    /// header. Timestamps are resolved to absolute values.
664    pub(crate) fn decode_all(data: &[u8]) -> Vec<TraceRow<'_, u64>> {
665        let header = TRACE_HEADER;
666        assert!(data.starts_with(header), "missing trace header");
667        let mut remaining = &data[header.len()..];
668        let mut decoder = TimestampDecoder::default();
669        let mut rows = Vec::new();
670        while !remaining.is_empty() {
671            let (row, rest) = postcard::take_from_bytes(remaining).unwrap();
672            rows.push(decoder.decode(row).unwrap());
673            remaining = rest;
674        }
675        rows
676    }
677
678    fn enter_exit_span() {
679        let span = tracing::info_span!("test span");
680        let _guard = span.enter();
681    }
682
683    fn count(rows: &[TraceRow<'_, u64>], predicate: impl Fn(&TraceRow<'_, u64>) -> bool) -> usize {
684        rows.iter().filter(|row| predicate(row)).count()
685    }
686
687    /// Returns the allocation counters attached to each Enter/Exit row, in order.
688    pub(crate) fn enter_exit_allocations(
689        rows: &[TraceRow<'_, u64>],
690    ) -> Vec<(&'static str, Option<Allocations>)> {
691        rows.iter()
692            .filter_map(|row| match row {
693                TraceRow::Enter { allocations, .. } => Some(("enter", *allocations)),
694                TraceRow::Exit { allocations, .. } => Some(("exit", *allocations)),
695                _ => None,
696            })
697            .collect()
698    }
699
700    #[test]
701    fn timestamps_round_trip_across_threads_and_buffers() {
702        let data = capture(RawTraceLayerOptions::default(), || {
703            // Time going backwards produces negative deltas
704            freeze_time(1000);
705            let span = tracing::info_span!("backwards");
706            let guard = span.enter();
707            freeze_time(400);
708            drop(guard);
709            drop(span);
710            let dispatch = tracing::dispatcher::get_default(|d| d.clone());
711            std::thread::scope(|scope| {
712                for _ in 0..2 {
713                    scope.spawn(|| {
714                        tracing::dispatcher::with_default(&dispatch, || {
715                            // Enough rows to fill and send several thread local buffers
716                            for i in 0..40_000u64 {
717                                let span = tracing::info_span!("loop", i);
718                                let _guard = span.enter();
719                            }
720                        })
721                    });
722                }
723            });
724        });
725        let all_rows = decode_all(&data);
726        let bases = count(&all_rows, |r| matches!(r, TraceRow::TimestampBase { .. }));
727        assert!(bases > 3, "expected multiple buffers, got {bases} bases");
728        let rows = decode(&data);
729
730        // The span on the main thread has exactly the frozen timestamps
731        let backwards_id = rows
732            .iter()
733            .find_map(|row| match row {
734                TraceRow::Start { id, name, ts, .. } if name == "backwards" => {
735                    assert_eq!(*ts, 1000);
736                    Some(*id)
737                }
738                _ => None,
739            })
740            .unwrap();
741        let backwards: Vec<_> = rows
742            .iter()
743            .filter_map(|row| match row {
744                TraceRow::Enter { id, ts, .. } if *id == backwards_id => Some(("enter", *ts)),
745                TraceRow::Exit { id, ts, .. } if *id == backwards_id => Some(("exit", *ts)),
746                TraceRow::End { id, ts } if *id == backwards_id => Some(("end", *ts)),
747                _ => None,
748            })
749            .collect();
750        assert_eq!(backwards, [("enter", 1000), ("exit", 400), ("end", 400)]);
751
752        // The fake clock of each spawned thread starts at 0 and advances by 1 on every read. Each
753        // loop iteration reads it once for each of its Start, Enter, Exit and End rows, so the
754        // Enter and Exit rows of iteration `i` have the timestamps `4 * i + 1` and `4 * i + 2`.
755        let mut per_thread: std::collections::HashMap<u64, (Vec<u64>, Vec<u64>)> =
756            Default::default();
757        for row in &rows {
758            match row {
759                TraceRow::Enter {
760                    thread_id, ts, id, ..
761                } if *id != backwards_id => per_thread.entry(*thread_id).or_default().0.push(*ts),
762                TraceRow::Exit {
763                    thread_id, ts, id, ..
764                } if *id != backwards_id => per_thread.entry(*thread_id).or_default().1.push(*ts),
765                _ => {}
766            }
767        }
768        assert_eq!(per_thread.len(), 2);
769        let expected_enter: Vec<u64> = (0..40_000).map(|i| 4 * i + 1).collect();
770        let expected_exit: Vec<u64> = (0..40_000).map(|i| 4 * i + 2).collect();
771        for (mut enter, mut exit) in per_thread.into_values() {
772            // Buffers of a thread can end up in the file in any order
773            enter.sort_unstable();
774            exit.sort_unstable();
775            assert!(enter == expected_enter, "unexpected Enter timestamps");
776            assert!(exit == expected_exit, "unexpected Exit timestamps");
777        }
778    }
779
780    #[test]
781    fn buffer_starting_with_a_record_gets_a_base_before_its_first_timestamp() {
782        // Just below the threshold where the buffer is sent to the writer thread
783        let fill = crate::trace_writer::THREAD_LOCAL_INITIAL_BUFFER_SIZE * 2 / 3;
784        let data = capture(RawTraceLayerOptions::default(), || {
785            let span = tracing::info_span!("recorded", value = tracing::field::Empty);
786            // This Record exceeds the threshold, so the buffer is sent after it
787            span.record("value", "x".repeat(fill));
788            // The next Record is the first row of a new buffer
789            span.record("value", "small");
790        });
791        let rows = decode_all(&data);
792        let kinds: Vec<_> = rows
793            .iter()
794            .map(|row| match row {
795                TraceRow::TimestampBase { .. } => "base",
796                TraceRow::Start { .. } => "start",
797                TraceRow::Record { .. } => "record",
798                TraceRow::End { .. } => "end",
799                _ => "other",
800            })
801            .collect();
802        // The second buffer starts with the Record, which has no timestamp, so its base is only
803        // written before the End row
804        assert_eq!(kinds, ["base", "start", "record", "record", "base", "end"]);
805    }
806
807    #[test]
808    fn attaches_allocation_deltas_to_enter_and_exit() {
809        let data = capture(RawTraceLayerOptions::default(), || {
810            fake_allocate(10, 1);
811            let span = tracing::info_span!("first");
812            let guard = span.enter();
813            fake_allocate(100, 2);
814            fake_deallocate(5, 1);
815            drop(guard);
816            fake_deallocate(1, 1);
817            let span = tracing::info_span!("second");
818            let guard = span.enter();
819            drop(guard);
820            fake_allocate(3, 1);
821            let guard = span.enter();
822            drop(guard);
823        });
824        let rows = decode(&data);
825        assert_eq!(
826            enter_exit_allocations(&rows),
827            vec![
828                // The first report of the thread is the baseline
829                ("enter", None),
830                (
831                    "exit",
832                    Some(Allocations {
833                        allocations: 200,
834                        allocation_count: 2,
835                        deallocations: 5,
836                        deallocation_count: 1,
837                    })
838                ),
839                (
840                    "enter",
841                    Some(Allocations {
842                        allocations: 0,
843                        allocation_count: 0,
844                        deallocations: 1,
845                        deallocation_count: 1,
846                    })
847                ),
848                // Nothing (de)allocated in between
849                ("exit", None),
850                (
851                    "enter",
852                    Some(Allocations {
853                        allocations: 3,
854                        allocation_count: 1,
855                        deallocations: 0,
856                        deallocation_count: 0,
857                    })
858                ),
859                ("exit", None),
860            ]
861        );
862    }
863
864    #[test]
865    fn memory_tracking_can_be_disabled() {
866        let data = capture(RawTraceLayerOptions { memory: false }, enter_exit_span);
867        let rows = decode(&data);
868        assert_eq!(count(&rows, |r| matches!(r, TraceRow::Start { .. })), 1);
869        assert_eq!(
870            enter_exit_allocations(&rows),
871            vec![("enter", None), ("exit", None)]
872        );
873        assert_eq!(count(&rows, |r| matches!(r, TraceRow::End { .. })), 1);
874        assert_eq!(
875            count(&rows, |r| matches!(r, TraceRow::MemorySample { .. })),
876            0
877        );
878        assert_eq!(rows.len(), 4);
879    }
880
881    /// Returns the Enter/Exit rows as `(kind, span id, ts)` tuples.
882    fn enter_exit_rows(rows: &[TraceRow<'_, u64>]) -> Vec<(&'static str, u64, u64)> {
883        rows.iter()
884            .filter_map(|row| match row {
885                TraceRow::Enter { id, ts, .. } => Some(("enter", *id, *ts)),
886                TraceRow::Exit { id, ts, .. } => Some(("exit", *id, *ts)),
887                _ => None,
888            })
889            .collect()
890    }
891
892    /// Enters and exits the same span twice.
893    fn reenter_span(between: impl FnOnce()) {
894        let span = tracing::info_span!("span");
895        drop(span.enter());
896        between();
897        drop(span.enter());
898    }
899
900    fn assert_not_elided(rows: &[TraceRow<'_, u64>]) {
901        let kinds: Vec<_> = enter_exit_rows(rows).iter().map(|r| r.0).collect();
902        assert_eq!(kinds, vec!["enter", "exit", "enter", "exit"]);
903    }
904
905    #[test]
906    fn elides_exit_enter_pair_of_same_span_at_same_timestamp() {
907        for memory in [true, false] {
908            let data = capture(RawTraceLayerOptions { memory }, || {
909                freeze_time(10);
910                reenter_span(|| {});
911            });
912            let rows = decode(&data);
913            assert_eq!(
914                enter_exit_rows(&rows),
915                vec![("enter", 1, 10), ("exit", 1, 10)],
916                "memory: {memory}"
917            );
918            assert_eq!(rows.len(), 4, "memory: {memory}");
919        }
920    }
921
922    #[test]
923    fn keeps_exit_enter_pair_with_larger_gap() {
924        let data = capture(RawTraceLayerOptions::default(), || {
925            freeze_time(10);
926            // Entered for 0µs, so only a gap of up to 1µs is allowed
927            reenter_span(|| freeze_time(12));
928        });
929        assert_not_elided(&decode(&data));
930    }
931
932    #[test]
933    fn elides_exit_enter_pair_with_1us_gap() {
934        let data = capture(RawTraceLayerOptions::default(), || {
935            freeze_time(10);
936            reenter_span(|| freeze_time(11));
937        });
938        assert_eq!(
939            enter_exit_rows(&decode(&data)),
940            vec![("enter", 1, 10), ("exit", 1, 11)]
941        );
942    }
943
944    /// Enters a span at `enter`, exits it at `exit`, enters it again at `reenter` and exits it
945    /// at `reenter + 1000`.
946    fn reenter_at(enter: u64, exit: u64, reenter: u64) -> Vec<(&'static str, u64, u64)> {
947        let data = capture(RawTraceLayerOptions::default(), || {
948            freeze_time(enter);
949            let span = tracing::info_span!("span");
950            let guard = span.enter();
951            freeze_time(exit);
952            drop(guard);
953            freeze_time(reenter);
954            let guard = span.enter();
955            freeze_time(reenter + 1000);
956            drop(guard);
957        });
958        enter_exit_rows(&decode(&data))
959    }
960
961    #[test]
962    fn elides_exit_enter_pair_with_gap_up_to_a_thousandth_of_the_entered_duration() {
963        assert_eq!(
964            reenter_at(0, 3000, 3003),
965            vec![("enter", 1, 0), ("exit", 1, 4003)]
966        );
967        assert_eq!(
968            reenter_at(0, 3000, 3004),
969            vec![
970                ("enter", 1, 0),
971                ("exit", 1, 3000),
972                ("enter", 1, 3004),
973                ("exit", 1, 4004)
974            ]
975        );
976    }
977
978    /// An omitted `Enter` doesn't count as the last `Enter` on the thread, so the entered duration
979    /// for the allowed gap keeps growing over chained elisions.
980    #[test]
981    fn chained_elisions_measure_from_the_last_written_enter() {
982        let data = capture(RawTraceLayerOptions::default(), || {
983            freeze_time(0);
984            let span = tracing::info_span!("span");
985            for (enter, exit) in [(0, 2000), (2002, 3000), (3002, 4000)] {
986                freeze_time(enter);
987                let guard = span.enter();
988                freeze_time(exit);
989                drop(guard);
990            }
991        });
992        assert_eq!(
993            enter_exit_rows(&decode(&data)),
994            vec![
995                // The first gap of 2µs is allowed after 2000µs, the second one after 3000µs
996                // (measured from the `Enter` at 0, not the omitted one at 2002)
997                ("enter", 1, 0),
998                ("exit", 1, 4000)
999            ]
1000        );
1001    }
1002
1003    /// Memory samples are taken on every `Enter`, even when its row is omitted.
1004    #[test]
1005    fn reports_memory_samples_when_the_enter_is_omitted() {
1006        // Far beyond the timestamps of the other tests, as the last sample time is global
1007        const START: u64 = 1 << 50;
1008        let data = capture(RawTraceLayerOptions::default(), || {
1009            let span = tracing::info_span!("span");
1010            freeze_time(START);
1011            let guard = span.enter();
1012            freeze_time(START + 100_000);
1013            drop(guard);
1014            // An omitted `Enter`, long after the sample on the first `Enter`
1015            drop(span.enter());
1016        });
1017        let rows = decode(&data);
1018        assert_eq!(
1019            enter_exit_rows(&rows),
1020            vec![("enter", 1, START), ("exit", 1, START + 100_000)]
1021        );
1022        let samples: Vec<_> = rows
1023            .iter()
1024            .filter_map(|row| match row {
1025                TraceRow::MemorySample { ts, .. } => Some(*ts),
1026                _ => None,
1027            })
1028            .collect();
1029        assert_eq!(samples, vec![START, START + 100_000]);
1030    }
1031
1032    /// The entered duration for the allowed gap is measured from the last `Enter` on the thread,
1033    /// regardless of the span. For nested spans that's shorter than the span's own entered time,
1034    /// so the allowed gap only shrinks.
1035    #[test]
1036    fn measures_the_gap_from_the_last_enter_on_the_thread() {
1037        let data = capture(RawTraceLayerOptions::default(), || {
1038            let outer = tracing::info_span!("outer");
1039            let inner = tracing::info_span!("inner");
1040            freeze_time(0);
1041            let outer_guard = outer.enter();
1042            freeze_time(2000);
1043            let inner_guard = inner.enter();
1044            freeze_time(2500);
1045            drop(inner_guard);
1046            freeze_time(3000);
1047            drop(outer_guard);
1048            // Entered for 3000µs since its own `Enter`, but only 1000µs since the last `Enter` on
1049            // this thread, so the gap of 2µs is not allowed
1050            freeze_time(3002);
1051            let outer_guard = outer.enter();
1052            freeze_time(4002);
1053            drop(outer_guard);
1054        });
1055        assert_eq!(
1056            enter_exit_rows(&decode(&data)),
1057            vec![
1058                ("enter", 1, 0),
1059                ("enter", 2, 2000),
1060                ("exit", 2, 2500),
1061                ("exit", 1, 3000),
1062                ("enter", 1, 3002),
1063                ("exit", 1, 4002)
1064            ]
1065        );
1066    }
1067
1068    #[test]
1069    fn keeps_exit_enter_pair_of_different_spans() {
1070        let data = capture(RawTraceLayerOptions::default(), || {
1071            freeze_time(10);
1072            let a = tracing::info_span!("a");
1073            let b = tracing::info_span!("b");
1074            drop(a.enter());
1075            drop(b.enter());
1076        });
1077        assert_not_elided(&decode(&data));
1078    }
1079
1080    #[test]
1081    fn keeps_exit_enter_pair_with_allocations_in_between() {
1082        let data = capture(RawTraceLayerOptions::default(), || {
1083            freeze_time(10);
1084            reenter_span(|| fake_allocate(8, 1));
1085        });
1086        let rows = decode(&data);
1087        assert_not_elided(&rows);
1088        // The allocation is reported with the `Enter`, for whatever ran before
1089        assert_eq!(
1090            enter_exit_allocations(&rows)[2],
1091            (
1092                "enter",
1093                Some(Allocations {
1094                    allocations: 8,
1095                    allocation_count: 1,
1096                    deallocations: 0,
1097                    deallocation_count: 0,
1098                })
1099            )
1100        );
1101    }
1102
1103    #[test]
1104    fn keeps_exit_enter_pair_with_row_in_between() {
1105        let data = capture(RawTraceLayerOptions::default(), || {
1106            freeze_time(10);
1107            reenter_span(|| tracing::info!("event"));
1108        });
1109        let rows = decode(&data);
1110        assert_not_elided(&rows);
1111        assert_eq!(count(&rows, |r| matches!(r, TraceRow::Event { .. })), 1);
1112    }
1113
1114    #[test]
1115    fn keeps_exit_enter_pair_on_different_threads() {
1116        let data = capture(RawTraceLayerOptions::default(), || {
1117            freeze_time(10);
1118            let span = tracing::info_span!("span");
1119            drop(span.enter());
1120            let dispatch = tracing::dispatcher::get_default(|d| d.clone());
1121            std::thread::scope(|scope| {
1122                scope.spawn(|| {
1123                    freeze_time(10);
1124                    tracing::dispatcher::with_default(&dispatch, || drop(span.enter()));
1125                });
1126            });
1127        });
1128        assert_not_elided(&decode(&data));
1129    }
1130
1131    #[test]
1132    fn resets_state_when_a_thread_slot_is_reused() {
1133        let data = capture(RawTraceLayerOptions::default(), || {
1134            let span = tracing::info_span!("span");
1135            let dispatch = tracing::dispatcher::get_default(|d| d.clone());
1136            // Sequential threads reuse the same thread local slot
1137            for i in 1..=2 {
1138                std::thread::scope(|scope| {
1139                    scope.spawn(|| {
1140                        freeze_time(10);
1141                        fake_allocate(1000 * i, 1);
1142                        tracing::dispatcher::with_default(&dispatch, || drop(span.enter()));
1143                    });
1144                });
1145            }
1146        });
1147        let rows = decode(&data);
1148        // Neither elided across threads nor diffed against the other thread's counters: each
1149        // thread reports its own baseline (`None`) first.
1150        assert_eq!(
1151            enter_exit_allocations(&rows),
1152            vec![
1153                ("enter", None),
1154                ("exit", None),
1155                ("enter", None),
1156                ("exit", None)
1157            ]
1158        );
1159        let threads: Vec<_> = rows
1160            .iter()
1161            .filter_map(|row| match row {
1162                TraceRow::Enter { thread_id, .. } => Some(*thread_id),
1163                _ => None,
1164            })
1165            .collect();
1166        assert_ne!(threads[0], threads[1]);
1167    }
1168
1169    #[test]
1170    fn uses_the_enter_of_the_same_thread_for_the_entered_duration() {
1171        let data = capture(RawTraceLayerOptions::default(), || {
1172            let span = tracing::info_span!("span");
1173            let dispatch = tracing::dispatcher::get_default(|d| d.clone());
1174            // The span is entered at 0 on this thread and at 2900 on another thread
1175            freeze_time(0);
1176            let guard = span.enter();
1177            std::thread::scope(|scope| {
1178                scope.spawn(|| {
1179                    freeze_time(2900);
1180                    tracing::dispatcher::with_default(&dispatch, || {
1181                        let _guard = span.enter();
1182                        freeze_time(2950);
1183                    });
1184                });
1185            });
1186            freeze_time(3000);
1187            drop(guard);
1188            // A gap of 3µs is allowed after 3000µs entered on this thread
1189            freeze_time(3003);
1190            drop(span.enter());
1191        });
1192        let rows = decode(&data);
1193        let mut by_thread: Vec<(u64, Vec<(&str, u64)>)> = Vec::new();
1194        for row in &rows {
1195            let (kind, thread, ts) = match row {
1196                TraceRow::Enter { thread_id, ts, .. } => ("enter", *thread_id, *ts),
1197                TraceRow::Exit { thread_id, ts, .. } => ("exit", *thread_id, *ts),
1198                _ => continue,
1199            };
1200            match by_thread.iter_mut().find(|(t, _)| *t == thread) {
1201                Some((_, rows)) => rows.push((kind, ts)),
1202                None => by_thread.push((thread, vec![(kind, ts)])),
1203            }
1204        }
1205        let mut by_thread: Vec<_> = by_thread.into_iter().map(|(_, rows)| rows).collect();
1206        by_thread.sort();
1207        assert_eq!(
1208            by_thread,
1209            vec![
1210                // This thread: the re-enter is omitted
1211                vec![("enter", 0), ("exit", 3003)],
1212                // The other thread
1213                vec![("enter", 2900), ("exit", 2950)],
1214            ]
1215        );
1216    }
1217
1218    #[test]
1219    fn elision_preserves_allocations_of_the_exited_span() {
1220        let run = |freeze: bool| {
1221            let data = capture(RawTraceLayerOptions::default(), || {
1222                freeze_time(10);
1223                let span = tracing::info_span!("span");
1224                let guard = span.enter();
1225                fake_allocate(50, 1);
1226                drop(guard);
1227                if !freeze {
1228                    // Too large to be elided
1229                    freeze_time(12);
1230                }
1231                let guard = span.enter();
1232                fake_allocate(7, 1);
1233                fake_deallocate(3, 1);
1234                drop(guard);
1235            });
1236            let rows = decode(&data);
1237            let rows_count = enter_exit_rows(&rows).len();
1238            // All allocations happen inside of the span, so the total of all reports is what
1239            // is attributed to the span.
1240            let total = enter_exit_allocations(&rows)
1241                .into_iter()
1242                .filter_map(|(_, allocations)| allocations)
1243                .fold(Allocations::default(), |a, b| Allocations {
1244                    allocations: a.allocations + b.allocations,
1245                    allocation_count: a.allocation_count + b.allocation_count,
1246                    deallocations: a.deallocations + b.deallocations,
1247                    deallocation_count: a.deallocation_count + b.deallocation_count,
1248                });
1249            (rows_count, total)
1250        };
1251        let (elided_rows, elided_total) = run(true);
1252        let (rows, total) = run(false);
1253        assert_eq!((elided_rows, rows), (2, 4));
1254        assert_eq!(
1255            elided_total,
1256            Allocations {
1257                allocations: 57,
1258                allocation_count: 2,
1259                deallocations: 3,
1260                deallocation_count: 1,
1261            }
1262        );
1263        assert_eq!(elided_total, total);
1264    }
1265
1266    #[test]
1267    fn footprint_is_read_at_most_once_per_interval() {
1268        let throttle = FootprintThrottle::new();
1269        let reads = Cell::new(0);
1270        let read = |value: u64| {
1271            reads.set(reads.get() + 1);
1272            value
1273        };
1274
1275        // The very first sample reads, even at ts 0.
1276        assert_eq!(throttle.get(0, || read(100)), 100);
1277        assert_eq!(reads.get(), 1);
1278
1279        // Within the interval the cached value is reused.
1280        assert_eq!(throttle.get(10_000, || read(200)), 100);
1281        assert_eq!(
1282            throttle.get(MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US - 1, || read(300)),
1283            100
1284        );
1285        assert_eq!(reads.get(), 1);
1286
1287        // Once the interval has elapsed it reads again.
1288        assert_eq!(
1289            throttle.get(MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US, || read(400)),
1290            400
1291        );
1292        assert_eq!(reads.get(), 2);
1293        assert_eq!(
1294            throttle.get(MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US + 10_000, || read(500)),
1295            400
1296        );
1297        assert_eq!(reads.get(), 2);
1298    }
1299
1300    #[test]
1301    fn concurrent_caller_during_a_read_gets_cached_value() {
1302        let throttle = FootprintThrottle::new();
1303        assert_eq!(throttle.get(0, || 100), 100);
1304
1305        // While the claimed read for the next interval is in progress, another
1306        // caller (simulated by a nested call) must not read and gets the
1307        // previous value.
1308        let ts = MEMORY_FOOTPRINT_SAMPLE_INTERVAL_US;
1309        let value = throttle.get(ts, || {
1310            let nested = throttle.get(ts + 10_000, || panic!("must not read concurrently"));
1311            assert_eq!(nested, 100);
1312            200
1313        });
1314        assert_eq!(value, 200);
1315        assert_eq!(throttle.get(ts + 20_000, || panic!("cached")), 200);
1316    }
1317
1318    #[test]
1319    fn simultaneous_first_calls_read_once() {
1320        const THREADS: usize = 8;
1321        let throttle = FootprintThrottle::new();
1322        let reads = AtomicUsize::new(0);
1323        let barrier = Barrier::new(THREADS);
1324
1325        let values: Vec<u64> = thread::scope(|scope| {
1326            let handles: Vec<_> = (0..THREADS)
1327                .map(|_| {
1328                    scope.spawn(|| {
1329                        barrier.wait();
1330                        throttle.get(0, || {
1331                            reads.fetch_add(1, Ordering::Relaxed);
1332                            // Keep the read in progress while the others race.
1333                            thread::sleep(Duration::from_millis(20));
1334                            100
1335                        })
1336                    })
1337                })
1338                .collect();
1339            handles.into_iter().map(|h| h.join().unwrap()).collect()
1340        });
1341
1342        assert_eq!(reads.load(Ordering::Relaxed), 1);
1343        // The winner gets the fresh value; losers get the (initially 0) cache.
1344        assert!(values.iter().all(|&v| v == 0 || v == 100), "{values:?}");
1345        assert!(values.contains(&100), "{values:?}");
1346        assert_eq!(throttle.get(10_000, || panic!("cached")), 100);
1347    }
1348}