big_code_analysis_ast/observation.rs
1//! Thread-local counters that make an invisible optimization testable.
2//!
3//! Several optimizations change no output at all: reusing one
4//! `tree_sitter::Parser` per thread (#1118), skipping a space-kind
5//! lookup on a node that opens no space (#1110), serializing `Ops`
6//! through a borrowed projection rather than an owned clone (#1110),
7//! deferring the modeline scan behind a resolving extension (#1111).
8//! Every assertion on the *result* of those paths holds just as well
9//! once the optimization is reverted, so a revert is silent unless
10//! something counts the work.
11//!
12//! # The invariant
13//!
14//! The counter and the function that bumps it are **unconditional**;
15//! only the accessor is gated. Gating the counter itself would leave the
16//! test observing a build production never ships — the counted branch
17//! compiled under `cfg(test)` and the shipped one under nothing — which
18//! is the exact failure these counters exist to catch. Four sites grew
19//! that rule independently and each stated it in prose; `counter!`
20//! makes it structural instead, emitting the three items together so the
21//! wrong one cannot be gated.
22//!
23//! The cost is one `Cell` increment on a path that already does far more
24//! (building a parser, classifying a node, projecting a whole tree),
25//! which is why it is affordable to leave in the shipped build.
26//!
27//! # Two crates, one macro
28//!
29//! `big-code-analysis` declares counters of its own (in its `ops`,
30//! `wire` and `spaces` walks) with the same macro, which is why it is
31//! `#[macro_export]`ed. The accessor's gate is a parameter because the
32//! two crates need different ones: a counter declared *here* must also be
33//! readable by the root crate's tests, which see this crate as an
34//! ordinary dependency (no `cfg(test)`), so it opens under the
35//! `test-support` feature as well.
36
37/// Declares a thread-local observation counter as a module: a private
38/// `Cell`, an unconditional `record()`, and a gated `observed()`.
39///
40/// Takes the module's name and, optionally, the `cfg` predicate that
41/// gates the accessor (`cfg(test)` when omitted). It deliberately
42/// carries no narrative — *which* optimization a counter observes, and
43/// why no assertion on the output can distinguish it, belongs in a
44/// comment above each invocation.
45///
46/// A module rather than three free items so a call site names the
47/// counter once (`parsers_built::record()`), which keeps the invocation
48/// to one line and leaves no room for the recorder and the accessor to
49/// drift apart.
50#[macro_export]
51#[doc(hidden)]
52macro_rules! observation_counter {
53 ($name:ident) => {
54 $crate::observation_counter!($name, cfg(test));
55 };
56 ($name:ident, $gate:meta) => {
57 #[doc(hidden)]
58 pub mod $name {
59 thread_local! {
60 static COUNT: ::std::cell::Cell<usize> = const { ::std::cell::Cell::new(0) };
61 }
62
63 /// Records one occurrence on this thread.
64 ///
65 /// `pub(super)` on purpose: only the module that owns the
66 /// counted path may bump it, so a counter cannot drift into
67 /// meaning "whatever any caller felt like recording".
68 #[inline]
69 pub(super) fn record() {
70 COUNT.with(|count| count.set(count.get() + 1));
71 }
72
73 /// Occurrences recorded on this thread. Only this accessor
74 /// is gated; see the `observation` module docs.
75 ///
76 /// Wider than `record` because the guarded path and the test
77 /// that guards it need not share a module: `child_scan_cursors`
78 /// counts a cursor hoist in `node`, but the metric dump in
79 /// `big-code-analysis` is one of the walks that has to hold
80 /// one, and a counter only reachable from its own module
81 /// silently leaves such a caller unguarded.
82 #[$gate]
83 pub fn observed() -> usize {
84 COUNT.with(::std::cell::Cell::get)
85 }
86 }
87 };
88}
89
90pub use observation_counter as counter;