From 502c51ed5ea0c8d8e27799f645aa7c1d66ec2b8a Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 18:28:11 -0600 Subject: [PATCH] resolve: check js_decl_scope_memo before the file/tree loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The #566 memo sat behind read_file, js_binding_tree (format! + tree LRU + Rc clone) and two js_unit_to_byte scans — the preamble was the residual cost the memo couldn't see. A hit now needs no tree; a missing or global scope needs no source either. Two adjacent same-loop wins: is_lexically_reachable's bare regex gains a one-entry memo keyed on reference_name, and prefer_adjacent_implementations skips its HashSet build unless a .d.ts candidate exists. pretix parity exact (32799/73404/165700); resolution 4.73s -> 4.57s. --- .../src/resolve/js_scope_upstream.rs | 38 +++++++++++++------ codegraph-kernel/src/resolve/mod.rs | 4 ++ codegraph-kernel/src/resolve/names.rs | 16 +++++++- 3 files changed, 46 insertions(+), 12 deletions(-) diff --git a/codegraph-kernel/src/resolve/js_scope_upstream.rs b/codegraph-kernel/src/resolve/js_scope_upstream.rs index 2adcc0637..104ffffa6 100644 --- a/codegraph-kernel/src/resolve/js_scope_upstream.rs +++ b/codegraph-kernel/src/resolve/js_scope_upstream.rs @@ -309,6 +309,33 @@ impl KernelResolver { { return None; } + // The declarator walk below depends only on the candidate — the ref + // enters only through `visible(scope, point)`. Memoize the walk so a + // name with hundreds of same-file candidates pays the parent-chain + // climb once, not once per ref (minified bundles are the hot case). + // Keyed by ref language too: parsed_tree picks its parser from + // r.language, so the same candidate walks a different tree under + // typescript vs javascript refs (review on #566). + // Check BEFORE the file/tree loads: a memo hit needs no tree at all, + // and a missing or global scope needs no source either — the loads + // were the residual cost that kept the memo from helping (#566 + // follow-up). + let memo_key = (n.id.clone(), r.language.clone()); + if let Some(&hit) = self.js_decl_scope_memo.get(&memo_key) { + return match hit { + Some(None) => Some(true), + None => None, + Some(Some(scope)) => { + let source = self.read_file(&n.file_path)?; + let line = source.get((r.line - 1).max(0) as usize)?; + let point = Point::new( + (r.line - 1).max(0) as usize, + super::names::js_unit_to_byte(line, r.column.max(0) as usize), + ); + Some(visible(Some(scope), point)) + } + }; + } let source = self.read_file(&n.file_path)?; let tree = self.js_binding_tree(&source, r)?; let line = source.get((r.line - 1).max(0) as usize)?; @@ -321,17 +348,6 @@ impl KernelResolver { (n.start_line - 1).max(0) as usize, super::names::js_unit_to_byte(line, n.start_column.max(0) as usize), ); - // The declarator walk below depends only on the candidate — the ref - // enters only through `visible(scope, point)`. Memoize the walk so a - // name with hundreds of same-file candidates pays the parent-chain - // climb once, not once per ref (minified bundles are the hot case). - // Keyed by ref language too: parsed_tree picks its parser from - // r.language, so the same candidate walks a different tree under - // typescript vs javascript refs (review on #566). - let memo_key = (n.id.clone(), r.language.clone()); - if let Some(&hit) = self.js_decl_scope_memo.get(&memo_key) { - return hit.map(|scope| visible(scope, point)); - } let mut current = tree .root_node() .descendant_for_point_range(declaration, declaration); diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index f2babf951..4fe542b1a 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -773,6 +773,9 @@ pub struct KernelResolver { /// ref language) — parsed_tree selects its parser from the ref's /// language, so the same candidate can walk a different tree. js_decl_scope_memo: HashMap<(String, String), js_scope_upstream::JsDeclScope>, + /// is_lexically_reachable's `bare` regex, by reference_name — refs are + /// evaluated in per-ref loops, so one entry covers a whole loop. + bare_name_memo: Option<(String, bool)>, language_type_scope_memo: HashMap>, scala_package_object_membership_memo: HashMap>, cpp_default_declaration_memo: HashMap, @@ -983,6 +986,7 @@ impl KernelResolver { manifest_external_memo: HashMap::new(), lexical_scope_memo: HashMap::new(), js_decl_scope_memo: HashMap::new(), + bare_name_memo: None, language_type_scope_memo: HashMap::new(), scala_package_object_membership_memo: HashMap::new(), cpp_default_declaration_memo: HashMap::new(), diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index d1ad0b09d..cc96a7091 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -32,6 +32,11 @@ fn objc_file_local_elsewhere(n: &KNode, r: &ResolveRefIn) -> bool { fn prefer_adjacent_implementations(candidates: &mut Vec>, r: &ResolveRefIn) { if r.reference_kind != "calls" || !is_js_family(&r.language) { return; } + // Nothing to demote without a .d.ts candidate — skip the HashSet build + // and its two String allocations per candidate. + if !candidates.iter().any(|n| [".d.ts", ".d.mts", ".d.cts"].iter().any(|s| n.file_path.ends_with(s))) { + return; + } let implementations: HashSet<_> = candidates.iter() .filter(|n| !re!(r"\.d\.[cm]?ts$").is_match(&n.file_path)) .filter_map(|n| n.file_path.rsplit_once('.').map(|(stem, _)| (stem.to_string(), n.qualified_name.clone()))) @@ -137,7 +142,16 @@ impl KernelResolver { /// (`case class B()` in a test method), or a method of such a type — /// declared inside a function is reachable only from inside it. pub(super) fn is_lexically_reachable(&mut self, candidate: &KNode, r: &ResolveRefIn) -> Res { - let bare = re!(r"^[A-Za-z_$][\w$]*$").is_match(&r.reference_name); + // Same reference_name for every candidate in a ref's loop — one-entry + // memo instead of the thread-local re! per candidate. + let bare = match &self.bare_name_memo { + Some((name, b)) if name == &r.reference_name => *b, + _ => { + let b = re!(r"^[A-Za-z_$][\w$]*$").is_match(&r.reference_name); + self.bare_name_memo = Some((r.reference_name.clone(), b)); + b + } + }; if candidate.kind == "function" && bare && !self.object_member_reachable_by_name(candidate,r)? { return Ok(false); } if bare && js_objects_upstream::path_holder(candidate) && js_objects_upstream::literal_owner(candidate) { let last = js_objects_upstream::last_segment(candidate);