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}
390
391impl Fault {
392    /// An error at `top_level` of `program`, unwound through `trace`.
393    #[must_use]
394    pub fn of(
395        program: &ResolvedProgram,
396        erased: &[Spanned],
397        top_level: usize,
398        e: &EvalError,
399        trace: &Trace,
400    ) -> Self {
401        let defs = Definitions::of(program, erased);
402        let message = crate::pipeline::display_keys(&crate::messages::describe(e, erased));
403        // `span()` is `None` for the arms that genuinely have no position
404        // (`Reader`, `Halted`, `NotImplemented`); synthetic is the honest
405        // stand-in, rendered as a file with no line.
406        let raised = e.span().unwrap_or_else(Span::synthetic);
407        // The innermost frame of the program's own owns the error's offsets,
408        // which is what lets a raise inside a callee from another file name
409        // that file; inside a builtin, the position is the call into it.
410        let (own, at) = defs.first_own(trace, raised);
411        let site = own
412            .and_then(|k| trace.frames[k..].iter().find_map(|f| defs.of_frame(f)))
413            .and_then(|d| defs.get(d))
414            .map_or(top_level, |d| d.top_level);
415        Self {
416            located: program.locate(site, at, &message).to_string(),
417            place: Place::of(program, site, at),
418            stack: Stack::of(program, &defs, trace, raised, top_level),
419            message,
420        }
421    }
422
423    /// The JSON form: one object, the stack innermost first.
424    #[must_use]
425    pub fn json(&self) -> serde_json::Value {
426        let mut v = self.place.json();
427        v["stage"] = "eval".into();
428        v["severity"] = "error".into();
429        v["message"] = self.message.clone().into();
430        v["stack"] = self.stack.json();
431        v["stack_depth"] = self.stack.depth.into();
432        v
433    }
434}
435
436impl From<String> for Fault {
437    fn from(message: String) -> Self {
438        Self {
439            located: message.clone(),
440            message,
441            ..Self::default()
442        }
443    }
444}
445
446impl std::fmt::Display for Fault {
447    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
448        f.write_str(&self.located)?;
449        if !self.stack.is_empty() {
450            write!(f, "\n{}", self.stack)?;
451        }
452        Ok(())
453    }
454}
455
456/// What one definition, or one unnamed function, cost.
457#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
458pub struct Counts {
459    /// Activations, a tail call counting as one.
460    pub calls: u64,
461    /// Time inside it and everything it called, counted once across recursion.
462    pub inclusive: Duration,
463    /// Time inside it alone.
464    pub exclusive: Duration,
465    /// Allocations made inside it and everything it called, counted once
466    /// across recursion. Zero when the binary does not count allocations;
467    /// [`Recorder::counts_allocations`] says which.
468    pub allocated: Allocations,
469    /// Allocations made inside it alone.
470    pub allocated_exclusive: Allocations,
471}
472
473/// A recorded function: a definition, or a lambda no top-level name binds.
474#[derive(Clone, Debug, PartialEq, Eq, Hash)]
475enum Slot {
476    Def(usize),
477    /// Keyed by its runtime name when it has one (a macro's `define`), else
478    /// by its span and the definition it ran inside.
479    Other {
480        name: Option<Arc<str>>,
481        within: Option<usize>,
482        source: Span,
483    },
484}
485
486/// One live activation on the recorder's shadow stack.
487struct Active {
488    slot: Slot,
489    start: Option<Instant>,
490    children: Duration,
491    /// This thread's allocation totals when the activation entered, read
492    /// after the recorder's own bookkeeping for the entry.
493    allocated_at: Option<Allocations>,
494    allocated_children: Allocations,
495    /// The arguments of a traced activation, kept for its exit line.
496    traced: Option<Vec<Value>>,
497}
498
499#[derive(Default)]
500struct State {
501    stack: Vec<Active>,
502    counts: HashMap<Slot, Counts>,
503    live: HashMap<Slot, usize>,
504    trace_depth: usize,
505}
506
507/// One line of `blue trace`: a call of the traced definition, or its exit.
508pub enum TraceEvent<'a> {
509    Call {
510        depth: usize,
511        name: &'a str,
512        args: &'a [Value],
513    },
514    Exit {
515        depth: usize,
516        name: &'a str,
517        args: &'a [Value],
518        exit: Exit<'a>,
519    },
520}
521
522/// Which definitions to trace, and where their lines go.
523pub struct Tracer {
524    pub targets: Vec<usize>,
525    pub sink: Box<dyn Fn(&TraceEvent<'_>) + Send + Sync>,
526}
527
528/// The one observer: per-definition counts, optionally timed, optionally
529/// tracing some definitions.
530pub struct Recorder {
531    defs: Arc<Definitions>,
532    timed: bool,
533    tracer: Option<Tracer>,
534    state: Mutex<State>,
535}
536
537impl Recorder {
538    /// `timed` reads the clock twice per activation; coverage leaves it off.
539    #[must_use]
540    pub fn new(defs: Arc<Definitions>, timed: bool, tracer: Option<Tracer>) -> Self {
541        Self {
542            defs,
543            timed,
544            tracer,
545            state: Mutex::new(State::default()),
546        }
547    }
548
549    fn state(&self) -> std::sync::MutexGuard<'_, State> {
550        self.state.lock().unwrap_or_else(PoisonError::into_inner)
551    }
552
553    fn slot(&self, call: &Call<'_>, stack: &[Active]) -> Slot {
554        if let Some(d) = self.defs.of_call(call.name, call.source) {
555            return Slot::Def(d);
556        }
557        let within = stack.iter().rev().find_map(|a| match a.slot {
558            Slot::Def(d) => Some(d),
559            Slot::Other { within, .. } => within,
560        });
561        Slot::Other {
562            name: call.name.cloned(),
563            within,
564            source: call.source,
565        }
566    }
567
568    fn label(&self, slot: &Slot) -> String {
569        match slot {
570            Slot::Def(d) => self
571                .defs
572                .get(*d)
573                .map_or_else(String::new, |d| d.name.clone()),
574            Slot::Other { name: Some(n), .. } => display_key(n),
575            Slot::Other {
576                name: None, within, ..
577            } => match within.and_then(|d| self.defs.get(d)) {
578                Some(d) => format!("fn in {}", d.name),
579                None => "fn".to_string(),
580            },
581        }
582    }
583
584    /// Every function that ran, with what it cost, and every definition that
585    /// did not run with zero calls: the profile and the coverage in one table.
586    #[must_use]
587    pub fn report(&self) -> Vec<Measured> {
588        let state = self.state();
589        let mut out: Vec<Measured> = self
590            .defs
591            .iter()
592            .enumerate()
593            .map(|(i, d)| Measured {
594                name: d.name.clone(),
595                definition: Some(i),
596                place: d.place.clone(),
597                entry: d.entry,
598                counts: state.counts.get(&Slot::Def(i)).copied().unwrap_or_default(),
599            })
600            .collect();
601        for (slot, counts) in &state.counts {
602            let Slot::Other { within, .. } = slot else {
603                continue;
604            };
605            let place = within
606                .and_then(|d| self.defs.get(d))
607                .map(|d| d.place.clone())
608                .unwrap_or_default();
609            out.push(Measured {
610                name: self.label(slot),
611                definition: None,
612                place,
613                entry: within
614                    .and_then(|d| self.defs.get(d))
615                    .is_some_and(|d| d.entry),
616                counts: *counts,
617            });
618        }
619        out
620    }
621
622    #[must_use]
623    pub fn definitions(&self) -> &Definitions {
624        &self.defs
625    }
626
627    /// Whether the allocation columns of [`Counts`] were measured: true only
628    /// in a binary that installs [`crate::alloc::Counting`]. Elsewhere they
629    /// read zero, which means unmeasured, never "allocated nothing".
630    #[must_use]
631    pub fn counts_allocations(&self) -> bool {
632        crate::alloc::allocations().is_some()
633    }
634}
635
636/// One row of [`Recorder::report`].
637#[derive(Clone, Debug)]
638pub struct Measured {
639    pub name: String,
640    /// Its index in [`Definitions`]; `None` for an unnamed function.
641    pub definition: Option<usize>,
642    pub place: Place,
643    pub entry: bool,
644    pub counts: Counts,
645}
646
647impl CallObserver for Recorder {
648    fn enter(&self, call: &Call<'_>, args: &[Value]) -> Option<Value> {
649        let mut state = self.state();
650        let slot = self.slot(call, &state.stack);
651        state.counts.entry(slot.clone()).or_default().calls += 1;
652        *state.live.entry(slot.clone()).or_default() += 1;
653        let traced = match (&self.tracer, &slot) {
654            (Some(t), Slot::Def(d)) if t.targets.contains(d) => {
655                let depth = state.trace_depth;
656                state.trace_depth += 1;
657                (t.sink)(&TraceEvent::Call {
658                    depth,
659                    name: &self.label(&slot),
660                    args,
661                });
662                Some(args.to_vec())
663            }
664            _ => None,
665        };
666        state.stack.push(Active {
667            slot,
668            start: self.timed.then(Instant::now),
669            children: Duration::ZERO,
670            allocated_at: None,
671            allocated_children: Allocations::default(),
672            traced,
673        });
674        // Last, so the entry's own map and stack growth falls outside the
675        // window the activation is charged for.
676        if let Some(active) = state.stack.last_mut() {
677            active.allocated_at = crate::alloc::allocations();
678        }
679        None
680    }
681
682    fn leave(&self, _call: &Call<'_>, exit: Exit<'_>) {
683        // First, for the same reason `enter` reads it last.
684        let allocated_now = crate::alloc::allocations();
685        let mut state = self.state();
686        let Some(active) = state.stack.pop() else {
687            return;
688        };
689        let elapsed = active.start.map_or(Duration::ZERO, |s| s.elapsed());
690        let allocated = match (allocated_now, active.allocated_at) {
691            (Some(now), Some(at)) => now.since(at),
692            _ => Allocations::default(),
693        };
694        if let Some(parent) = state.stack.last_mut() {
695            parent.children += elapsed;
696            parent.allocated_children += allocated;
697        }
698        let live = state.live.entry(active.slot.clone()).or_default();
699        *live = live.saturating_sub(1);
700        let outermost = *live == 0;
701        let counts = state.counts.entry(active.slot.clone()).or_default();
702        counts.exclusive += elapsed.saturating_sub(active.children);
703        counts.allocated_exclusive += allocated.since(active.allocated_children);
704        if outermost {
705            counts.inclusive += elapsed;
706            counts.allocated += allocated;
707        }
708        if let (Some(t), Some(args)) = (&self.tracer, &active.traced) {
709            state.trace_depth = state.trace_depth.saturating_sub(1);
710            (t.sink)(&TraceEvent::Exit {
711                depth: state.trace_depth,
712                name: &self.label(&active.slot),
713                args,
714                exit,
715            });
716        }
717    }
718}