From 6a80b56a66281b2fe0a97b0df85addfe3aa325fc Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 18:48:20 -0600 Subject: [PATCH 1/3] resolve: memoize bare_call_receiver + generated-path check per key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bare_call_receiver answers the same question once per same-file candidate — file LRU hit, memmem scan, and up to an 8-line join each time. Memoize by ref address (stable within a chunk Vec) and clear at every chunk-loop head so a reallocated Vec can never hit stale entries. The >256-pool generated-share scan ran is_generated_file's ~25 regexes per candidate; memoize per unique file path. --- codegraph-kernel/src/resolve/mod.rs | 25 +++++++++++++++++++ codegraph-kernel/src/resolve/names.rs | 4 +-- .../src/resolve/overloads_upstream.rs | 13 ++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index 4fe542b1a6..d7652cde4f 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -776,6 +776,14 @@ pub struct KernelResolver { /// 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)>, + /// bare_call_receiver's answer per ref (the same `&r` reaches it once per + /// same-file candidate). Keyed by the ref's address, which is stable for + /// a chunk's Vec; cleared at every chunk loop so a reallocated Vec can + /// never hit a previous chunk's entry. + bare_receiver_memo: HashMap, String)>>, + /// is_generated_file per path — the >256-pool scans run its ~25 + /// regexes once per candidate; pools share few unique paths. + gen_path_memo: HashMap, language_type_scope_memo: HashMap>, scala_package_object_membership_memo: HashMap>, cpp_default_declaration_memo: HashMap, @@ -987,6 +995,8 @@ impl KernelResolver { lexical_scope_memo: HashMap::new(), js_decl_scope_memo: HashMap::new(), bare_name_memo: None, + bare_receiver_memo: HashMap::new(), + gen_path_memo: HashMap::new(), language_type_scope_memo: HashMap::new(), scala_package_object_membership_memo: HashMap::new(), cpp_default_declaration_memo: HashMap::new(), @@ -1179,6 +1189,18 @@ impl KernelResolver { Ok(out) } + /// is_generated_file, memoized per path — the >256-pool generated-share + /// scans run the ~25-regex check once per candidate; pools share few + /// unique file paths. + pub(super) fn is_gen_path(&mut self, path: &str) -> bool { + if let Some(&b) = self.gen_path_memo.get(path) { + return b; + } + let b = crate::textutil::is_generated_file(path); + self.gen_path_memo.insert(path.to_string(), b); + b + } + /// `SELECT generated FROM files WHERE path = ?` — the join's fallback /// when a ref's stored file_path was empty (see read_pending_batch). fn file_generated(&mut self, path: &str) -> Result> { @@ -1197,6 +1219,7 @@ impl KernelResolver { #[cfg_attr(feature = "addon", napi)] pub fn resolve_chunk(&mut self, refs: Vec) -> Result> { let mut out = Vec::with_capacity(refs.len()); + self.bare_receiver_memo.clear(); for r in refs { let t0 = PROF_ON.then(std::time::Instant::now); let mut o = match per_ref(&r, ResolveOutcome::unresolved(), || self.resolve_ref(&r)) { @@ -1257,6 +1280,7 @@ impl KernelResolver { #[cfg_attr(feature = "addon", napi)] pub fn resolve_deferred_this_members(&mut self, refs: Vec) -> Result> { let mut out = Vec::with_capacity(refs.len()); + self.bare_receiver_memo.clear(); for r in refs { if !is_migrated_language(&r.language) { out.push(ResolveOutcome::unresolved()); @@ -1278,6 +1302,7 @@ impl KernelResolver { #[cfg_attr(feature = "addon", napi)] pub fn resolve_deferred_chains(&mut self, refs: Vec) -> Result> { let mut out = Vec::with_capacity(refs.len()); + self.bare_receiver_memo.clear(); for r in refs { if !is_migrated_language(&r.language) { out.push(ResolveOutcome::unresolved()); diff --git a/codegraph-kernel/src/resolve/names.rs b/codegraph-kernel/src/resolve/names.rs index cc96a70919..46e0c77b6b 100644 --- a/codegraph-kernel/src/resolve/names.rs +++ b/codegraph-kernel/src/resolve/names.rs @@ -723,7 +723,7 @@ impl KernelResolver { // pools (Migration, __init__) proceed untouched. if named.len() > GENERATED_NAME_POOL_CAP && (r.generated == Some(true) - || named.iter().filter(|n| crate::textutil::is_generated_file(&n.file_path)).count() * 4 + || named.iter().filter(|n| self.is_gen_path(&n.file_path)).count() * 4 >= named.len() * 3) { return Ok(None); @@ -1019,7 +1019,7 @@ impl KernelResolver { // at least as wide, so decline before the retain chain. if candidates.len() > GENERATED_NAME_POOL_CAP && (r.generated == Some(true) - || candidates.iter().filter(|n| crate::textutil::is_generated_file(&n.file_path)).count() * 4 + || candidates.iter().filter(|n| self.is_gen_path(&n.file_path)).count() * 4 >= candidates.len() * 3) { return Ok(None); diff --git a/codegraph-kernel/src/resolve/overloads_upstream.rs b/codegraph-kernel/src/resolve/overloads_upstream.rs index 54837b25ba..737f990e9d 100644 --- a/codegraph-kernel/src/resolve/overloads_upstream.rs +++ b/codegraph-kernel/src/resolve/overloads_upstream.rs @@ -917,6 +917,19 @@ impl KernelResolver { /// `this.container.classList.toggle()`); None for a call written bare or /// not found. pub(super) fn bare_call_receiver(&mut self, r: &ResolveRefIn) -> Res, String)>> { + // The answer depends only on the ref, but is asked once per same-file + // candidate — memoize by the ref's address (cleared per chunk loop, + // so a reallocated Vec can never reuse an entry). + let key = r as *const ResolveRefIn as usize; + if let Some(hit) = self.bare_receiver_memo.get(&key) { + return Ok(hit.clone()); + } + let out = self.bare_call_receiver_uncached(r)?; + self.bare_receiver_memo.insert(key, out.clone()); + Ok(out) + } + + fn bare_call_receiver_uncached(&mut self, r: &ResolveRefIn) -> Res, String)>> { if !re!(r"^[A-Za-z_$][\w$]*$").is_match(&r.reference_name) { return Ok(None); } From 7a2f240662c8f22d831aaa7eb43c44804bb4946d Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 18:50:03 -0600 Subject: [PATCH 2/3] resolve: memoize bare_call_receiver + generated-path scan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bare_call_receiver answers the same question once per same-file candidate — file LRU hit, memmem scan, possible 8-line join each time. Memoize on a content hash of the ref's identifying fields (no alloc, no pointer aliasing, dedupes identical refs). - The >256-pool generated-share scan ran is_generated_file's ~25 regexes per candidate; pools share few unique paths — memoize per path. --- codegraph-kernel/src/resolve/mod.rs | 12 ++++-------- .../src/resolve/overloads_upstream.rs | 15 +++++++++++---- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index d7652cde4f..2aa2efee2a 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -776,11 +776,10 @@ pub struct KernelResolver { /// 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)>, - /// bare_call_receiver's answer per ref (the same `&r` reaches it once per - /// same-file candidate). Keyed by the ref's address, which is stable for - /// a chunk's Vec; cleared at every chunk loop so a reallocated Vec can - /// never hit a previous chunk's entry. - bare_receiver_memo: HashMap, String)>>, + /// bare_call_receiver's answer per ref (the same question is asked once + /// per same-file candidate). Keyed on a content hash of the ref's + /// fields — identical key means identical input, so no clearing needed. + bare_receiver_memo: HashMap, String)>>, /// is_generated_file per path — the >256-pool scans run its ~25 /// regexes once per candidate; pools share few unique paths. gen_path_memo: HashMap, @@ -1219,7 +1218,6 @@ impl KernelResolver { #[cfg_attr(feature = "addon", napi)] pub fn resolve_chunk(&mut self, refs: Vec) -> Result> { let mut out = Vec::with_capacity(refs.len()); - self.bare_receiver_memo.clear(); for r in refs { let t0 = PROF_ON.then(std::time::Instant::now); let mut o = match per_ref(&r, ResolveOutcome::unresolved(), || self.resolve_ref(&r)) { @@ -1280,7 +1278,6 @@ impl KernelResolver { #[cfg_attr(feature = "addon", napi)] pub fn resolve_deferred_this_members(&mut self, refs: Vec) -> Result> { let mut out = Vec::with_capacity(refs.len()); - self.bare_receiver_memo.clear(); for r in refs { if !is_migrated_language(&r.language) { out.push(ResolveOutcome::unresolved()); @@ -1302,7 +1299,6 @@ impl KernelResolver { #[cfg_attr(feature = "addon", napi)] pub fn resolve_deferred_chains(&mut self, refs: Vec) -> Result> { let mut out = Vec::with_capacity(refs.len()); - self.bare_receiver_memo.clear(); for r in refs { if !is_migrated_language(&r.language) { out.push(ResolveOutcome::unresolved()); diff --git a/codegraph-kernel/src/resolve/overloads_upstream.rs b/codegraph-kernel/src/resolve/overloads_upstream.rs index 737f990e9d..7f52847462 100644 --- a/codegraph-kernel/src/resolve/overloads_upstream.rs +++ b/codegraph-kernel/src/resolve/overloads_upstream.rs @@ -917,10 +917,17 @@ impl KernelResolver { /// `this.container.classList.toggle()`); None for a call written bare or /// not found. pub(super) fn bare_call_receiver(&mut self, r: &ResolveRefIn) -> Res, String)>> { - // The answer depends only on the ref, but is asked once per same-file - // candidate — memoize by the ref's address (cleared per chunk loop, - // so a reallocated Vec can never reuse an entry). - let key = r as *const ResolveRefIn as usize; + // The answer depends only on the ref's fields, but is asked once per + // same-file candidate — memoize on a content hash of them (no alloc, + // no pointer aliasing, dedupes identical refs across a chunk too). + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + r.file_path.hash(&mut h); + r.from_node_id.hash(&mut h); + r.reference_name.hash(&mut h); + r.line.hash(&mut h); + r.column.hash(&mut h); + let key = h.finish(); if let Some(hit) = self.bare_receiver_memo.get(&key) { return Ok(hit.clone()); } From 1eedb6175e8ded8e1968dfe37c9105dd692d6557 Mon Sep 17 00:00:00 2001 From: bompus Date: Tue, 27 Oct 2026 19:15:24 -0600 Subject: [PATCH 3/3] resolve: bound the batch memos at each entry point The bare-receiver and generated-path memos only need intra-batch reuse; clearing per resolve_chunk/deferred call also drops pinned SourceFiles. (CodeRabbit #570) --- codegraph-kernel/src/resolve/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/codegraph-kernel/src/resolve/mod.rs b/codegraph-kernel/src/resolve/mod.rs index 2aa2efee2a..2df6788378 100644 --- a/codegraph-kernel/src/resolve/mod.rs +++ b/codegraph-kernel/src/resolve/mod.rs @@ -1217,6 +1217,10 @@ impl KernelResolver { /// The `settle` stage — one verdict per ref in input order. #[cfg_attr(feature = "addon", napi)] pub fn resolve_chunk(&mut self, refs: Vec) -> Result> { + // Memos carry only intra-batch value; bounding them per entry point + // also drops the Rcs bare_receiver_memo pins. + self.bare_receiver_memo.clear(); + self.gen_path_memo.clear(); let mut out = Vec::with_capacity(refs.len()); for r in refs { let t0 = PROF_ON.then(std::time::Instant::now); @@ -1277,6 +1281,8 @@ impl KernelResolver { /// run after the main pass wrote every implements/extends edge. #[cfg_attr(feature = "addon", napi)] pub fn resolve_deferred_this_members(&mut self, refs: Vec) -> Result> { + self.bare_receiver_memo.clear(); + self.gen_path_memo.clear(); let mut out = Vec::with_capacity(refs.len()); for r in refs { if !is_migrated_language(&r.language) { @@ -1298,6 +1304,8 @@ impl KernelResolver { /// every other chain through the dotted arm, then the language gate. #[cfg_attr(feature = "addon", napi)] pub fn resolve_deferred_chains(&mut self, refs: Vec) -> Result> { + self.bare_receiver_memo.clear(); + self.gen_path_memo.clear(); let mut out = Vec::with_capacity(refs.len()); for r in refs { if !is_migrated_language(&r.language) {