Skip to main content

blue_lang_runtime/
observe.rs

1//! Watching a blue program run: the call stack a runtime error reports, and
2//! the per-definition counters `blue profile`, `blue test --coverage` and
3//! `blue trace` read.
4//!
5//! One mechanism under all of them: tatara-lisp-eval's call hook
6//! (`tatara_lisp_eval::observe`), which every closure activation passes once,
7//! tail calls included. Its unwind arm is always on and costs nothing until an
8//! error, so every runtime error carries the frames it left ([`Fault`]). Its
9//! observer arm is installed only when a tool asks, and [`Recorder`] is the one
10//! observer here: counts, inclusive and exclusive time and allocations
11//! ([`crate::alloc`]), and the trace of one definition. The hook's `enter` may also answer a call without running its
12//! body, which is where a memo table keyed on a definition and its arguments
13//! plugs in (`theory/BLUE-TOOLING.md` T-M) without a second hook.
14//!
15//! Every count is keyed by the DEFINITION the runtime binds (`retsu/first`,
16//! `%root/f`), the key the check stage resolved. That is the key the engine's
17//! per-definition queries carry (`theory/BLUE-ENGINE.md` §3), so when
18//! `def_hash` replaces the name, the counters move with it.
19//!
20//! The tree-walker only: the VM reports neither arm.
21
22use std::collections::HashMap;
23use std::sync::{Arc, Mutex, PoisonError};
24use std::time::{Duration, Instant};
25
26use tatara_lisp::{Span, Spanned};
27use tatara_lisp_eval::{EvalError, Value};
28
29pub use tatara_lisp_eval::observe::{Call, CallObserver, Exit, Trace, TraceFrame};
30
31use crate::alloc::Allocations;
32use crate::uses::ResolvedProgram;
33
34/// A function the program defines at the top level, under its runtime key.
35#[derive(Clone, Debug)]
36pub struct Definition {
37    /// What the runtime binds: `retsu/first`, `%root/f`.
38    pub key: Arc<str>,
39    /// What the author writes: `retsu::first`, `f`.
40    pub name: String,
41    /// The top-level form that defines it.
42    pub top_level: usize,
43    /// The defining form's span, in its own file.
44    pub span: Span,
45    /// Defined in the entry file rather than an imported one.
46    pub entry: bool,
47    /// Where it is defined.
48    pub place: Place,
49}
50
51/// A position as a reader follows it. Each part is absent when it cannot be
52/// justified, never guessed.
53#[derive(Clone, Debug, Default, PartialEq, Eq)]
54pub struct Place {
55    pub file: Option<String>,
56    pub line: Option<usize>,
57    pub column: Option<usize>,
58}
59
60impl Place {
61    fn of(program: &ResolvedProgram, top_level: usize, span: Span) -> Self {
62        let at = program.locate(top_level, span, "");
63        Self {
64            file: at.file_name(),
65            line: at.line_col().map(|(l, _)| l),
66            column: at.line_col().map(|(_, c)| c),
67        }
68    }
69
70    fn json(&self) -> serde_json::Value {
71        serde_json::json!({ "file": self.file, "line": self.line, "column": self.column })
72    }
73}
74
75impl std::fmt::Display for Place {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        f.write_str(self.file.as_deref().unwrap_or("<unknown file>"))?;
78        if let Some(line) = self.line {
79            write!(f, ":{line}")?;
80            if let Some(col) = self.column {
81                write!(f, ":{col}")?;
82            }
83        }
84        Ok(())
85    }
86}
87
88/// Every function a program defines at its top level: the denominator of
89/// coverage, and the table that turns a closure's runtime key back into a
90/// name and a place.
91#[derive(Debug, Default)]
92pub struct Definitions {
93    defs: Vec<Definition>,
94    by_key: HashMap<Arc<str>, usize>,
95    /// `f = fn(x) … end` binds an unnamed lambda: found by its span. `None`
96    /// when two definitions share one (two files, same offsets), so no
97    /// activation is credited to the wrong one.
98    by_lambda: HashMap<Span, Option<usize>>,
99}
100
101impl Definitions {
102    /// Read off the tree the evaluator runs (`erased`, one form per top-level
103    /// form of `program`).
104    #[must_use]
105    pub fn of(program: &ResolvedProgram, erased: &[Spanned]) -> Self {
106        let mut out = Self::default();
107        for (i, form) in erased.iter().enumerate() {
108            let Some(items) = form.as_list() else {
109                continue;
110            };
111            if items.first().and_then(Spanned::as_symbol) != Some("define") || items.len() < 3 {
112                continue;
113            }
114            let (key, lambda) = match items[1].as_list() {
115                Some(head) => match head.first().and_then(Spanned::as_symbol) {
116                    Some(k) => (k, None),
117                    None => continue,
118                },
119                None => match (items[1].as_symbol(), items[2].as_list()) {
120                    (Some(k), Some(value))
121                        if value.first().and_then(Spanned::as_symbol) == Some("lambda") =>
122                    {
123                        (k, Some(items[2].span))
124                    }
125                    _ => continue,
126                },
127            };
128            let key: Arc<str> = Arc::from(key);
129            let index = out.defs.len();
130            out.defs.push(Definition {
131                name: display_key(&key),
132                key: Arc::clone(&key),
133                top_level: i,
134                span: form.span,
135                entry: program.owner_of(i) == Some(ResolvedProgram::ENTRY),
136                place: Place::of(program, i, form.span),
137            });
138            out.by_key.insert(key, index);
139            if let Some(span) = lambda {
140                out.by_lambda
141                    .entry(span)
142                    .and_modify(|slot| *slot = None)
143                    .or_insert(Some(index));
144            }
145        }
146        out
147    }
148
149    pub fn iter(&self) -> impl Iterator<Item = &Definition> {
150        self.defs.iter()
151    }
152
153    #[must_use]
154    pub fn len(&self) -> usize {
155        self.defs.len()
156    }
157
158    #[must_use]
159    pub fn is_empty(&self) -> bool {
160        self.defs.is_empty()
161    }
162
163    #[must_use]
164    pub fn get(&self, index: usize) -> Option<&Definition> {
165        self.defs.get(index)
166    }
167
168    /// The definitions an author's `name` can mean: the exact name
169    /// (`retsu::first`, `f`), else every definition whose own name is it.
170    #[must_use]
171    pub fn named(&self, name: &str) -> Vec<usize> {
172        let exact: Vec<usize> = (0..self.defs.len())
173            .filter(|&i| self.defs[i].name == name || &*self.defs[i].key == name)
174            .collect();
175        if !exact.is_empty() {
176            return exact;
177        }
178        (0..self.defs.len())
179            .filter(|&i| self.defs[i].name.rsplit("::").next() == Some(name))
180            .collect()
181    }
182
183    /// Which definition an activation is a call of: by its name, or for a
184    /// lambda bound at the top level, by its span.
185    fn of_call(&self, name: Option<&Arc<str>>, source: Span) -> Option<usize> {
186        match name {
187            Some(n) => self.by_key.get(n).copied(),
188            None => self.by_lambda.get(&source).copied().flatten(),
189        }
190    }
191
192    fn of_frame(&self, frame: &TraceFrame) -> Option<usize> {
193        self.of_call(frame.name.as_ref(), frame.source)
194    }
195
196    /// A named closure the program did not define: one of the runtime's own
197    /// tatara-lisp builtins. Every program definition runs under a qualified
198    /// key (`%root/f`, `retsu/first`), so a bare name is never the author's.
199    fn is_builtin(&self, frame: &TraceFrame) -> bool {
200        frame.name.as_ref().is_some_and(|n| {
201            !self.by_key.contains_key(n) && blue_lang_syntax::qualified(n).is_none()
202        })
203    }
204
205    /// Where the program's own code was when an error left it: the innermost
206    /// frame that is not a builtin, and the span inside it — the error's own
207    /// span, or the call into the builtin that raised.
208    fn first_own(&self, trace: &Trace, error: Span) -> (Option<usize>, Span) {
209        let frames = &trace.frames;
210        let own = frames.iter().position(|f| !self.is_builtin(f));
211        match own {
212            Some(0) | None => (own, error),
213            Some(k) => (Some(k), frames[k - 1].at),
214        }
215    }
216}
217
218/// A runtime key as the author writes it: `%root/f` is `f`, `retsu/first` is
219/// `retsu::first`, a builtin stays bare.
220#[must_use]
221pub fn display_key(key: &str) -> String {
222    let root = format!(
223        "{}{}",
224        blue_lang_check::names::ROOT_QUALIFIER,
225        blue_lang_syntax::QUALIFIER
226    );
227    if let Some(rest) = key.strip_prefix(&root) {
228        rest.to_string()
229    } else if let Some((pkg, name)) = blue_lang_syntax::qualified(key) {
230        format!("{pkg}::{name}")
231    } else {
232        key.to_string()
233    }
234}
235
236/// One frame of a blue call stack: the definition that was running and where
237/// in it.
238#[derive(Clone, Debug, PartialEq, Eq)]
239pub struct StackFrame {
240    pub function: String,
241    /// `None` for a builtin the runtime defines in tatara-lisp (`first`,
242    /// `map`): its offsets index the runtime's own source, not the program's.
243    pub place: Option<Place>,
244}
245
246/// The frames a runtime error unwound through, innermost first, ending with
247/// the top-level form that started the chain when it was recorded.
248#[derive(Clone, Debug, Default, PartialEq, Eq)]
249pub struct Stack {
250    pub frames: Vec<StackFrame>,
251    /// Activations unwound, recorded or not. Above `frames.len()` when the
252    /// recursion was deeper than `tatara_lisp_eval::observe::TRACE_FRAMES`.
253    pub depth: usize,
254}
255
256/// Distinct lines a rendered stack shows before it summarises the rest.
257pub const STACK_LINES: usize = 24;
258
259impl Stack {
260    #[must_use]
261    pub fn is_empty(&self) -> bool {
262        self.frames.is_empty()
263    }
264
265    /// Read a [`Trace`] against the program that ran. `error` is where the
266    /// innermost frame raised; `top_level` the form that was executing.
267    #[must_use]
268    pub fn of(
269        program: &ResolvedProgram,
270        defs: &Definitions,
271        trace: &Trace,
272        error: Span,
273        top_level: usize,
274    ) -> Self {
275        let frames = &trace.frames;
276        let mut out = Vec::with_capacity(frames.len() + 1);
277        for (k, frame) in frames.iter().enumerate() {
278            let def = defs.of_frame(frame);
279            // A lambda is defined inside whatever named frame is outward of
280            // it, which is the file its body's offsets index.
281            let outer = frames[k + 1..].iter().find_map(|f| defs.of_frame(f));
282            let site = def
283                .or(outer)
284                .and_then(|d| defs.get(d))
285                .map_or(top_level, |d| d.top_level);
286            if defs.is_builtin(frame) {
287                out.push(StackFrame {
288                    function: frame.name.as_deref().map_or_else(String::new, display_key),
289                    place: None,
290                });
291                continue;
292            }
293            let at = if k == 0 { error } else { frames[k - 1].at };
294            let function = match (def.and_then(|d| defs.get(d)), &frame.name) {
295                (Some(d), _) => d.name.clone(),
296                (None, Some(n)) => display_key(n),
297                (None, None) => match outer.and_then(|d| defs.get(d)) {
298                    Some(d) => format!("fn in {}", d.name),
299                    None => "fn".to_string(),
300                },
301            };
302            out.push(StackFrame {
303                function,
304                place: Some(Place::of(program, site, at)),
305            });
306        }
307        if let Some(last) = frames.last() {
308            if trace.depth == frames.len() {
309                out.push(StackFrame {
310                    function: "<top level>".to_string(),
311                    place: Some(Place::of(program, top_level, last.at)),
312                });
313            }
314        }
315        Self {
316            frames: out,
317            depth: trace.depth,
318        }
319    }
320
321    #[must_use]
322    pub fn json(&self) -> serde_json::Value {
323        serde_json::Value::Array(
324            self.frames
325                .iter()
326                .map(|f| {
327                    let mut v = f
328                        .place
329                        .as_ref()
330                        .map_or_else(|| serde_json::json!({ "builtin": true }), Place::json);
331                    v["function"] = serde_json::Value::String(f.function.clone());
332                    v
333                })
334                .collect(),
335        )
336    }
337}
338
339impl std::fmt::Display for Stack {
340    /// `  at f (file:line:col)`, one line per frame, a run of identical frames
341    /// folded into one, at most [`STACK_LINES`] lines.
342    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
343        let mut runs: Vec<(&StackFrame, usize)> = Vec::new();
344        for frame in &self.frames {
345            match runs.last_mut() {
346                Some((last, n)) if *last == frame => *n += 1,
347                _ => runs.push((frame, 1)),
348            }
349        }
350        let mut first = true;
351        let mut line = |f: &mut std::fmt::Formatter<'_>, text: String| {
352            let sep = if first { "" } else { "\n" };
353            first = false;
354            write!(f, "{sep}  {text}")
355        };
356        for (shown, (frame, n)) in runs.iter().enumerate() {
357            if shown == STACK_LINES {
358                let rest: usize = runs[shown..].iter().map(|(_, n)| n).sum();
359                line(f, format!("… {rest} more frame(s)"))?;
360                break;
361            }
362            match &frame.place {
363                Some(place) => line(f, format!("at {} ({place})", frame.function))?,
364                None => line(f, format!("at {} (builtin)", frame.function))?,
365            }
366            if *n > 1 {
367                line(f, format!("… repeated {} more time(s)", n - 1))?;
368            }
369        }
370        let unrecorded = self.depth.saturating_sub(self.frames.len());
371        if unrecorded > 0 {
372            line(f, format!("… {unrecorded} outer frame(s) not recorded"))?;
373        }
374        Ok(())
375    }
376}
377
378/// A runtime error: what went wrong, where, and the blue call stack that led
379/// there. [`crate::RunError::Eval`] carries one.
380#[derive(Clone, Debug, Default)]
381pub struct Fault {
382    /// The message, names as the author writes them, without a position.
383    pub message: String,
384    /// Where it was raised.
385    pub place: Place,
386    /// `file:line:col: message`, the one-line form.
387    pub located: String,
388    pub stack: Stack,
389    /// The run ended on a capability breach ([`crate::breach`]).
390    pub breach: bool,
391}
392
393impl Fault {
394    /// An error at `top_level` of `program`, unwound through `trace`.
395    #[must_use]
396    pub fn of(
397        program: &ResolvedProgram,
398        erased: &[Spanned],
399        top_level: usize,
400        e: &EvalError,
401        trace: &Trace,
402    ) -> Self {
403        let defs = Definitions::of(program, erased);
404        let message = crate::pipeline::display_keys(&crate::messages::describe(e, erased));
405        // `span()` is `None` for the arms that genuinely have no position
406        // (`Reader`, `Halted`, `NotImplemented`); synthetic is the honest
407        // stand-in, rendered as a file with no line.
408        let raised = e.span().unwrap_or_else(Span::synthetic);
409        // The innermost frame of the program's own owns the error's offsets,
410        // which is what lets a raise inside a callee from another file name
411        // that file; inside a builtin, the position is the call into it.
412        let (own, at) = defs.first_own(trace, raised);
413        let site = own
414            .and_then(|k| trace.frames[k..].iter().find_map(|f| defs.of_frame(f)))
415            .and_then(|d| defs.get(d))
416            .map_or(top_level, |d| d.top_level);
417        Self {
418            located: program.locate(site, at, &message).to_string(),
419            place: Place::of(program, site, at),
420            stack: Stack::of(program, &defs, trace, raised, top_level),
421            message,
422            breach: crate::is_breach(e),
423        }
424    }
425
426    /// The JSON form: one object, the stack innermost first.
427    #[must_use]
428    pub fn json(&self) -> serde_json::Value {
429        let mut v = self.place.json();
430        v["stage"] = "eval".into();
431        v["severity"] = "error".into();
432        v["message"] = self.message.clone().into();
433        v["stack"] = self.stack.json();
434        v["stack_depth"] = self.stack.depth.into();
435        if self.breach {
436            v["kind"] = crate::BREACH.into();
437        }
438        v
439    }
440}
441
442impl From<String> for Fault {
443    fn from(message: String) -> Self {
444        Self {
445            located: message.clone(),
446            message,
447            ..Self::default()
448        }
449    }
450}
451
452impl std::fmt::Display for Fault {
453    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
454        f.write_str(&self.located)?;
455        if !self.stack.is_empty() {
456            write!(f, "\n{}", self.stack)?;
457        }
458        Ok(())
459    }
460}
461
462/// What one definition, or one unnamed function, cost.
463#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
464pub struct Counts {
465    /// Activations, a tail call counting as one.
466    pub calls: u64,
467    /// Time inside it and everything it called, counted once across recursion.
468    pub inclusive: Duration,
469    /// Time inside it alone.
470    pub exclusive: Duration,
471    /// Allocations made inside it and everything it called, counted once
472    /// across recursion. Zero when the binary does not count allocations;
473    /// [`Recorder::counts_allocations`] says which.
474    pub allocated: Allocations,
475    /// Allocations made inside it alone.
476    pub allocated_exclusive: Allocations,
477}
478
479/// A recorded function: a definition, or a lambda no top-level name binds.
480#[derive(Clone, Debug, PartialEq, Eq, Hash)]
481enum Slot {
482    Def(usize),
483    /// Keyed by its runtime name when it has one (a macro's `define`), else
484    /// by its span and the definition it ran inside.
485    Other {
486        name: Option<Arc<str>>,
487        within: Option<usize>,
488        source: Span,
489    },
490}
491
492/// One live activation on the recorder's shadow stack.
493struct Active {
494    slot: Slot,
495    start: Option<Instant>,
496    children: Duration,
497    /// This thread's allocation totals when the activation entered, read
498    /// after the recorder's own bookkeeping for the entry.
499    allocated_at: Option<Allocations>,
500    allocated_children: Allocations,
501    /// The arguments of a traced activation, kept for its exit line.
502    traced: Option<Vec<Value>>,
503}
504
505#[derive(Default)]
506struct State {
507    stack: Vec<Active>,
508    counts: HashMap<Slot, Counts>,
509    live: HashMap<Slot, usize>,
510    trace_depth: usize,
511}
512
513/// One line of `blue trace`: a call of the traced definition, or its exit.
514pub enum TraceEvent<'a> {
515    Call {
516        depth: usize,
517        name: &'a str,
518        args: &'a [Value],
519    },
520    Exit {
521        depth: usize,
522        name: &'a str,
523        args: &'a [Value],
524        exit: Exit<'a>,
525    },
526}
527
528/// Which definitions to trace, and where their lines go.
529pub struct Tracer {
530    pub targets: Vec<usize>,
531    pub sink: Box<dyn Fn(&TraceEvent<'_>) + Send + Sync>,
532}
533
534/// The one observer: per-definition counts, optionally timed, optionally
535/// tracing some definitions.
536pub struct Recorder {
537    defs: Arc<Definitions>,
538    timed: bool,
539    tracer: Option<Tracer>,
540    state: Mutex<State>,
541}
542
543impl Recorder {
544    /// `timed` reads the clock twice per activation; coverage leaves it off.
545    #[must_use]
546    pub fn new(defs: Arc<Definitions>, timed: bool, tracer: Option<Tracer>) -> Self {
547        Self {
548            defs,
549            timed,
550            tracer,
551            state: Mutex::new(State::default()),
552        }
553    }
554
555    fn state(&self) -> std::sync::MutexGuard<'_, State> {
556        self.state.lock().unwrap_or_else(PoisonError::into_inner)
557    }
558
559    fn slot(&self, call: &Call<'_>, stack: &[Active]) -> Slot {
560        if let Some(d) = self.defs.of_call(call.name, call.source) {
561            return Slot::Def(d);
562        }
563        let within = stack.iter().rev().find_map(|a| match a.slot {
564            Slot::Def(d) => Some(d),
565            Slot::Other { within, .. } => within,
566        });
567        Slot::Other {
568            name: call.name.cloned(),
569            within,
570            source: call.source,
571        }
572    }
573
574    fn label(&self, slot: &Slot) -> String {
575        match slot {
576            Slot::Def(d) => self
577                .defs
578                .get(*d)
579                .map_or_else(String::new, |d| d.name.clone()),
580            Slot::Other { name: Some(n), .. } => display_key(n),
581            Slot::Other {
582                name: None, within, ..
583            } => match within.and_then(|d| self.defs.get(d)) {
584                Some(d) => format!("fn in {}", d.name),
585                None => "fn".to_string(),
586            },
587        }
588    }
589
590    /// Every function that ran, with what it cost, and every definition that
591    /// did not run with zero calls: the profile and the coverage in one table.
592    #[must_use]
593    pub fn report(&self) -> Vec<Measured> {
594        let state = self.state();
595        let mut out: Vec<Measured> = self
596            .defs
597            .iter()
598            .enumerate()
599            .map(|(i, d)| Measured {
600                name: d.name.clone(),
601                definition: Some(i),
602                place: d.place.clone(),
603                entry: d.entry,
604                counts: state.counts.get(&Slot::Def(i)).copied().unwrap_or_default(),
605            })
606            .collect();
607        for (slot, counts) in &state.counts {
608            let Slot::Other { within, .. } = slot else {
609                continue;
610            };
611            let place = within
612                .and_then(|d| self.defs.get(d))
613                .map(|d| d.place.clone())
614                .unwrap_or_default();
615            out.push(Measured {
616                name: self.label(slot),
617                definition: None,
618                place,
619                entry: within
620                    .and_then(|d| self.defs.get(d))
621                    .is_some_and(|d| d.entry),
622                counts: *counts,
623            });
624        }
625        out
626    }
627
628    #[must_use]
629    pub fn definitions(&self) -> &Definitions {
630        &self.defs
631    }
632
633    /// Whether the allocation columns of [`Counts`] were measured: true only
634    /// in a binary that installs [`crate::alloc::Counting`]. Elsewhere they
635    /// read zero, which means unmeasured, never "allocated nothing".
636    #[must_use]
637    pub fn counts_allocations(&self) -> bool {
638        crate::alloc::allocations().is_some()
639    }
640}
641
642/// One row of [`Recorder::report`].
643#[derive(Clone, Debug)]
644pub struct Measured {
645    pub name: String,
646    /// Its index in [`Definitions`]; `None` for an unnamed function.
647    pub definition: Option<usize>,
648    pub place: Place,
649    pub entry: bool,
650    pub counts: Counts,
651}
652
653impl CallObserver for Recorder {
654    fn enter(&self, call: &Call<'_>, args: &[Value]) -> Option<Value> {
655        let mut state = self.state();
656        let slot = self.slot(call, &state.stack);
657        state.counts.entry(slot.clone()).or_default().calls += 1;
658        *state.live.entry(slot.clone()).or_default() += 1;
659        let traced = match (&self.tracer, &slot) {
660            (Some(t), Slot::Def(d)) if t.targets.contains(d) => {
661                let depth = state.trace_depth;
662                state.trace_depth += 1;
663                (t.sink)(&TraceEvent::Call {
664                    depth,
665                    name: &self.label(&slot),
666                    args,
667                });
668                Some(args.to_vec())
669            }
670            _ => None,
671        };
672        state.stack.push(Active {
673            slot,
674            start: self.timed.then(Instant::now),
675            children: Duration::ZERO,
676            allocated_at: None,
677            allocated_children: Allocations::default(),
678            traced,
679        });
680        // Last, so the entry's own map and stack growth falls outside the
681        // window the activation is charged for.
682        if let Some(active) = state.stack.last_mut() {
683            active.allocated_at = crate::alloc::allocations();
684        }
685        None
686    }
687
688    fn leave(&self, _call: &Call<'_>, exit: Exit<'_>) {
689        // First, for the same reason `enter` reads it last.
690        let allocated_now = crate::alloc::allocations();
691        let mut state = self.state();
692        let Some(active) = state.stack.pop() else {
693            return;
694        };
695        let elapsed = active.start.map_or(Duration::ZERO, |s| s.elapsed());
696        let allocated = match (allocated_now, active.allocated_at) {
697            (Some(now), Some(at)) => now.since(at),
698            _ => Allocations::default(),
699        };
700        if let Some(parent) = state.stack.last_mut() {
701            parent.children += elapsed;
702            parent.allocated_children += allocated;
703        }
704        let live = state.live.entry(active.slot.clone()).or_default();
705        *live = live.saturating_sub(1);
706        let outermost = *live == 0;
707        let counts = state.counts.entry(active.slot.clone()).or_default();
708        counts.exclusive += elapsed.saturating_sub(active.children);
709        counts.allocated_exclusive += allocated.since(active.allocated_children);
710        if outermost {
711            counts.inclusive += elapsed;
712            counts.allocated += allocated;
713        }
714        if let (Some(t), Some(args)) = (&self.tracer, &active.traced) {
715            state.trace_depth = state.trace_depth.saturating_sub(1);
716            (t.sink)(&TraceEvent::Exit {
717                depth: state.trace_depth,
718                name: &self.label(&active.slot),
719                args,
720                exit,
721            });
722        }
723    }
724}