Skip to main content

fallow_graph/resolve/
work.rs

1//! Deterministic work counts for one import-resolution run.
2//!
3//! Resolution runs one module per rayon task, and a module never spawns nested
4//! rayon work, so each module's counts live in a thread-local scope while the
5//! module resolves. The scope closes with the module and its counts travel back
6//! in the module's output, where the sequential merge sums them. The hot path
7//! therefore never touches a shared atomic.
8//!
9//! The counts are exact for a given project and commit. They do not depend on
10//! the thread count or on scheduling, which makes them usable as a regression
11//! metric where wall-clock time is too noisy.
12
13use std::cell::RefCell;
14use std::hash::{Hash, Hasher};
15use std::ops::AddAssign;
16
17use rustc_hash::{FxHashSet, FxHasher};
18
19/// Work counts for import resolution.
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
21pub struct ResolveWork {
22    /// Specifier resolutions that the import sites asked for: one for each
23    /// static import binding, re-export, `require()`, `import()` and module
24    /// mock. Internal retries inside the resolver are not counted here.
25    pub specifier_calls: u64,
26    /// Distinct `(specifier, from_style)` pairs for each importing file,
27    /// summed over all files. `specifier_calls / unique_specifiers` above 1.0
28    /// shows bindings that share a specifier. The resolver runs at most once
29    /// for each of these pairs. A pair that returns before the resolver, such
30    /// as an external URL, makes no resolver call.
31    pub unique_specifiers: u64,
32    /// Calls into `oxc_resolver`, including fallback retries.
33    pub oxc_resolve_calls: u64,
34    /// Path canonicalize calls that the run needs: each direct call, plus one
35    /// for each distinct path that goes through the canonicalize cache.
36    pub canonicalize_calls: u64,
37}
38
39impl AddAssign for ResolveWork {
40    fn add_assign(&mut self, other: Self) {
41        self.specifier_calls += other.specifier_calls;
42        self.unique_specifiers += other.unique_specifiers;
43        self.oxc_resolve_calls += other.oxc_resolve_calls;
44        self.canonicalize_calls += other.canonicalize_calls;
45    }
46}
47
48struct ModuleScope {
49    work: ResolveWork,
50    seen_specifiers: FxHashSet<u64>,
51}
52
53thread_local! {
54    static SCOPE: RefCell<Option<ModuleScope>> = const { RefCell::new(None) };
55    /// The specifier set of the last closed scope, kept so that the next
56    /// module on this worker reuses its capacity instead of allocating.
57    static SPARE_SET: RefCell<FxHashSet<u64>> = RefCell::new(FxHashSet::default());
58}
59
60/// Restores the previous scope when the module scope ends, also on unwind.
61struct ScopeGuard {
62    previous: Option<ModuleScope>,
63    restored: bool,
64}
65
66impl ScopeGuard {
67    fn finish(mut self) -> ResolveWork {
68        self.restored = true;
69        let previous = self.previous.take();
70        let Some(closed) = SCOPE.with(|scope| scope.replace(previous)) else {
71            return ResolveWork::default();
72        };
73        let mut seen = closed.seen_specifiers;
74        seen.clear();
75        SPARE_SET.with(|spare| *spare.borrow_mut() = seen);
76        closed.work
77    }
78}
79
80impl Drop for ScopeGuard {
81    fn drop(&mut self) {
82        if !self.restored {
83            let previous = self.previous.take();
84            SCOPE.with(|scope| scope.replace(previous));
85        }
86    }
87}
88
89/// Run `resolve` inside a fresh work scope and return its counts.
90pub(super) fn in_module_scope<T>(resolve: impl FnOnce() -> T) -> (T, ResolveWork) {
91    let fresh = ModuleScope {
92        work: ResolveWork::default(),
93        seen_specifiers: SPARE_SET.with(|spare| std::mem::take(&mut *spare.borrow_mut())),
94    };
95    let guard = ScopeGuard {
96        previous: SCOPE.with(|scope| scope.replace(Some(fresh))),
97        restored: false,
98    };
99    let output = resolve();
100    (output, guard.finish())
101}
102
103fn with_scope(record: impl FnOnce(&mut ModuleScope)) {
104    SCOPE.with(|scope| {
105        if let Some(scope) = scope.borrow_mut().as_mut() {
106            record(scope);
107        }
108    });
109}
110
111/// Record one specifier resolution that an import site asked for.
112pub(super) fn note_specifier(specifier: &str, from_style: bool) {
113    with_scope(|scope| {
114        scope.work.specifier_calls += 1;
115        let mut hasher = FxHasher::default();
116        specifier.hash(&mut hasher);
117        from_style.hash(&mut hasher);
118        if scope.seen_specifiers.insert(hasher.finish()) {
119            scope.work.unique_specifiers += 1;
120        }
121    });
122}
123
124/// Record one call into `oxc_resolver`.
125pub(super) fn note_oxc_resolve() {
126    with_scope(|scope| scope.work.oxc_resolve_calls += 1);
127}
128
129/// Record `count` direct path canonicalize calls.
130pub(super) fn note_canonicalize(count: u64) {
131    with_scope(|scope| scope.work.canonicalize_calls += count);
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn a_module_scope_counts_calls_and_distinct_specifiers() {
140        let ((), work) = in_module_scope(|| {
141            note_specifier("./a", false);
142            note_specifier("./a", false);
143            note_specifier("./a", true);
144            note_oxc_resolve();
145            note_canonicalize(2);
146        });
147        assert_eq!(
148            work,
149            ResolveWork {
150                specifier_calls: 3,
151                unique_specifiers: 2,
152                oxc_resolve_calls: 1,
153                canonicalize_calls: 2,
154            }
155        );
156    }
157
158    #[test]
159    fn notes_outside_a_scope_are_dropped() {
160        note_specifier("./a", false);
161        let ((), work) = in_module_scope(|| note_specifier("./b", false));
162        assert_eq!(work.specifier_calls, 1);
163    }
164
165    #[test]
166    fn each_scope_starts_with_an_empty_specifier_set() {
167        let ((), first) = in_module_scope(|| note_specifier("./a", false));
168        let ((), second) = in_module_scope(|| note_specifier("./a", false));
169        assert_eq!(first.unique_specifiers, 1);
170        assert_eq!(second.unique_specifiers, 1);
171    }
172
173    #[test]
174    fn a_nested_scope_keeps_its_counts_apart_from_the_outer_scope() {
175        let (inner, outer) = in_module_scope(|| {
176            note_specifier("./outer", false);
177            let ((), inner) = in_module_scope(|| note_specifier("./inner", false));
178            note_specifier("./outer-2", false);
179            inner
180        });
181        assert_eq!(inner.specifier_calls, 1);
182        assert_eq!(outer.specifier_calls, 2);
183    }
184}