Skip to content

Commit 4bf195a

Browse files
committed
perf(native): replace O(n²) type-map dedup with O(n) write-then-dedup
set_type_map_entry previously scanned the entire Vec on every write to enforce first-write-wins semantics, giving O(n²) construction for files with many field definitions (e.g. 3 entries per field × N fields × N scan per write). push_return_type_entry in javascript.rs had the same pattern. Replace with a two-phase approach: all writes are plain Vec pushes (O(1) each), and a single dedup_type_map() pass collapses duplicates at the end of each extractor's extract() call (O(n) total). The new helper uses a HashMap drain to keep the highest-confidence entry per key, with first-write-wins on ties — identical semantics to before. Adds dedup_type_map() call to all 13 extractors that populate type_map, and to return_type_map in javascript.rs. Includes unit tests covering empty, no-duplicate, highest-confidence-wins, and tie-breaking cases.
1 parent 4a1329f commit 4bf195a

14 files changed

Lines changed: 132 additions & 23 deletions

File tree

crates/codegraph-core/src/extractors/c.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ impl SymbolExtractor for CExtractor {
1313
walk_tree(&tree.root_node(), source, &mut symbols, match_c_node);
1414
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &C_AST_CONFIG);
1515
walk_tree(&tree.root_node(), source, &mut symbols, match_c_type_map);
16+
dedup_type_map(&mut symbols.type_map);
1617
symbols
1718
}
1819
}

crates/codegraph-core/src/extractors/cpp.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ impl SymbolExtractor for CppExtractor {
1313
walk_tree(&tree.root_node(), source, &mut symbols, match_cpp_node);
1414
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &CPP_AST_CONFIG);
1515
walk_tree(&tree.root_node(), source, &mut symbols, match_cpp_type_map);
16+
dedup_type_map(&mut symbols.type_map);
1617
symbols
1718
}
1819
}

crates/codegraph-core/src/extractors/csharp.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ impl SymbolExtractor for CSharpExtractor {
1515
reclassify_csharp_implements(&mut symbols);
1616
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &CSHARP_AST_CONFIG);
1717
walk_tree(&tree.root_node(), source, &mut symbols, match_csharp_type_map);
18+
dedup_type_map(&mut symbols.type_map);
1819
symbols
1920
}
2021
}

crates/codegraph-core/src/extractors/cuda.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ impl SymbolExtractor for CudaExtractor {
3434
// fires for CUDA files just like it does for C++ files. Mirrors the
3535
// third walk in `cpp.rs`.
3636
walk_tree(&tree.root_node(), source, &mut symbols, match_cuda_type_map);
37+
dedup_type_map(&mut symbols.type_map);
3738
symbols
3839
}
3940
}

crates/codegraph-core/src/extractors/go.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ impl SymbolExtractor for GoExtractor {
1313
walk_tree(&tree.root_node(), source, &mut symbols, match_go_node);
1414
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &GO_AST_CONFIG);
1515
walk_tree(&tree.root_node(), source, &mut symbols, match_go_type_map);
16+
dedup_type_map(&mut symbols.type_map);
1617
symbols
1718
}
1819
}

crates/codegraph-core/src/extractors/helpers.rs

Lines changed: 114 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -958,14 +958,14 @@ pub fn extract_simple_parameters(
958958

959959
// ── Type-map helpers ───────────────────────────────────────────────────────
960960

961-
/// Merge a type-map entry into the sink with highest-confidence-wins semantics,
962-
/// matching `setTypeMapEntry` in `src/extractors/helpers.ts`.
961+
/// Append a raw type-map entry (name → type_name at the given confidence)
962+
/// without deduplication. Call [`dedup_type_map`] once after all entries
963+
/// have been pushed to collapse duplicates with highest-confidence-wins
964+
/// semantics.
963965
///
964-
/// If the key already exists with equal-or-higher confidence the new entry is
965-
/// silently dropped; otherwise the old entry is replaced. This prevents
966-
/// duplicate entries from accumulating when the same bare key is written
967-
/// multiple times (e.g. two class expressions in one file that both define a
968-
/// field with the same name).
966+
/// This is the write half of a two-phase approach: accumulate all entries
967+
/// cheaply (O(1) per write), then deduplicate once in O(n) at the end of
968+
/// extraction — avoiding the previous O(n²) scan-per-write pattern.
969969
///
970970
/// Mirrors `setTypeMapEntry` in `src/extractors/helpers.ts`.
971971
pub fn set_type_map_entry(
@@ -978,14 +978,6 @@ pub fn set_type_map_entry(
978978
if name.is_empty() {
979979
return;
980980
}
981-
// Scan for an existing entry with the same key.
982-
if let Some(existing) = symbols.type_map.iter_mut().find(|e| e.name == name) {
983-
if confidence > existing.confidence {
984-
existing.type_name = type_name.into();
985-
existing.confidence = confidence;
986-
}
987-
return;
988-
}
989981
symbols.type_map.push(TypeMapEntry {
990982
name,
991983
type_name: type_name.into(),
@@ -996,9 +988,8 @@ pub fn set_type_map_entry(
996988
/// Record a parameter name → type binding in the type-map sink, using
997989
/// the default confidence of `0.9` shared by every Rust extractor.
998990
///
999-
/// Delegates to [`set_type_map_entry`] so duplicate keys are deduplicated with
1000-
/// highest-confidence-wins semantics (first-write-wins at the uniform 0.9
1001-
/// confidence), matching `setTypeMapEntry` in `src/extractors/helpers.ts`.
991+
/// Delegates to [`set_type_map_entry`]. Call [`dedup_type_map`] once after
992+
/// all entries have been pushed to collapse duplicates.
1002993
pub fn push_type_map_entry(
1003994
symbols: &mut FileSymbols,
1004995
name: impl Into<String>,
@@ -1007,6 +998,38 @@ pub fn push_type_map_entry(
1007998
set_type_map_entry(symbols, name, type_name, 0.9);
1008999
}
10091000

1001+
/// Deduplicate a type-map `Vec` in-place, keeping the highest-confidence
1002+
/// entry per key (first-write-wins on ties, matching `setTypeMapEntry` in
1003+
/// `src/extractors/helpers.ts`).
1004+
///
1005+
/// This is the read half of the two-phase write-then-dedup approach used by
1006+
/// all extractors. Call it once at the end of each `extract()` implementation
1007+
/// after all tree-walk passes have completed, for both `symbols.type_map` and
1008+
/// `symbols.return_type_map` when applicable.
1009+
///
1010+
/// Complexity: O(n) where n = number of entries (including duplicates).
1011+
pub fn dedup_type_map(entries: &mut Vec<TypeMapEntry>) {
1012+
if entries.len() <= 1 {
1013+
return;
1014+
}
1015+
use std::collections::HashMap;
1016+
// Drain all entries into a HashMap, keeping the highest-confidence value
1017+
// per key. On ties the first entry seen wins (insertion-order preserved
1018+
// by the `if conf > prev` guard), matching the previous first-write-wins
1019+
// behaviour of the old per-entry linear scan.
1020+
let mut map: HashMap<String, TypeMapEntry> = HashMap::with_capacity(entries.len());
1021+
for e in entries.drain(..) {
1022+
match map.get(&e.name) {
1023+
None => { map.insert(e.name.clone(), e); }
1024+
Some(prev) if e.confidence > prev.confidence => {
1025+
map.insert(e.name.clone(), e);
1026+
}
1027+
_ => {}
1028+
}
1029+
}
1030+
entries.extend(map.into_values());
1031+
}
1032+
10101033
/// C-family `declaration` / `parameter_declaration` type-map matcher.
10111034
///
10121035
/// The cpp / cuda / c extractors all emit verbatim copies of the same
@@ -1061,3 +1084,76 @@ where
10611084
_ => {}
10621085
}
10631086
}
1087+
1088+
#[cfg(test)]
1089+
mod tests {
1090+
use super::*;
1091+
use crate::types::TypeMapEntry;
1092+
1093+
fn entry(name: &str, type_name: &str, confidence: f64) -> TypeMapEntry {
1094+
TypeMapEntry { name: name.to_string(), type_name: type_name.to_string(), confidence }
1095+
}
1096+
1097+
#[test]
1098+
fn dedup_empty() {
1099+
let mut v: Vec<TypeMapEntry> = vec![];
1100+
dedup_type_map(&mut v);
1101+
assert!(v.is_empty());
1102+
}
1103+
1104+
#[test]
1105+
fn dedup_single() {
1106+
let mut v = vec![entry("x", "Foo", 0.9)];
1107+
dedup_type_map(&mut v);
1108+
assert_eq!(v.len(), 1);
1109+
assert_eq!(v[0].name, "x");
1110+
}
1111+
1112+
#[test]
1113+
fn dedup_no_duplicates() {
1114+
let mut v = vec![entry("x", "Foo", 0.9), entry("y", "Bar", 0.9)];
1115+
dedup_type_map(&mut v);
1116+
assert_eq!(v.len(), 2);
1117+
}
1118+
1119+
#[test]
1120+
fn dedup_keeps_highest_confidence() {
1121+
// Lower confidence written first, higher written second — higher wins.
1122+
let mut v = vec![
1123+
entry("x", "Low", 0.6),
1124+
entry("x", "High", 0.9),
1125+
];
1126+
dedup_type_map(&mut v);
1127+
assert_eq!(v.len(), 1);
1128+
assert_eq!(v[0].type_name, "High");
1129+
assert_eq!(v[0].confidence, 0.9);
1130+
}
1131+
1132+
#[test]
1133+
fn dedup_first_write_wins_on_equal_confidence() {
1134+
// Two entries with equal confidence — first one wins.
1135+
let mut v = vec![
1136+
entry("x", "First", 0.9),
1137+
entry("x", "Second", 0.9),
1138+
];
1139+
dedup_type_map(&mut v);
1140+
assert_eq!(v.len(), 1);
1141+
assert_eq!(v[0].type_name, "First");
1142+
}
1143+
1144+
#[test]
1145+
fn dedup_mixed_keys() {
1146+
let mut v = vec![
1147+
entry("a", "A1", 0.6),
1148+
entry("b", "B1", 0.9),
1149+
entry("a", "A2", 0.9), // higher — wins for "a"
1150+
entry("b", "B2", 0.7), // lower — loses for "b"
1151+
];
1152+
dedup_type_map(&mut v);
1153+
assert_eq!(v.len(), 2);
1154+
let a = v.iter().find(|e| e.name == "a").expect("a present");
1155+
let b = v.iter().find(|e| e.name == "b").expect("b present");
1156+
assert_eq!(a.type_name, "A2");
1157+
assert_eq!(b.type_name, "B1");
1158+
}
1159+
}

crates/codegraph-core/src/extractors/java.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ impl SymbolExtractor for JavaExtractor {
1313
walk_tree(&tree.root_node(), source, &mut symbols, match_java_node);
1414
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &JAVA_AST_CONFIG);
1515
walk_tree(&tree.root_node(), source, &mut symbols, match_java_type_map);
16+
dedup_type_map(&mut symbols.type_map);
1617
symbols
1718
}
1819
}

crates/codegraph-core/src/extractors/javascript.rs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ impl SymbolExtractor for JsExtractor {
4141
// Phase 8.3c–8.3f: points-to bindings (params, this-rebinding, arrays,
4242
// spread, for-of, object rest/props) for the pts constraint solver.
4343
walk_tree(&tree.root_node(), source, &mut symbols, match_js_pts_bindings);
44+
// Collapse duplicate keys accumulated during the tree walks (O(n)).
45+
dedup_type_map(&mut symbols.type_map);
46+
dedup_type_map(&mut symbols.return_type_map);
4447
symbols
4548
}
4649
}
@@ -635,12 +638,10 @@ fn find_return_new_expr_type<'a>(body: &Node<'a>, source: &'a [u8]) -> Option<&'
635638
None
636639
}
637640

638-
/// Insert `(fn_name → type_name)` into `return_type_map`, keeping the highest-confidence entry.
641+
/// Append a `(fn_name → type_name)` entry to `return_type_map`.
642+
/// Deduplication (highest-confidence-wins) is handled in bulk by
643+
/// [`dedup_type_map`] at the end of `extract()`.
639644
fn push_return_type_entry(symbols: &mut FileSymbols, fn_name: &str, type_name: &str, confidence: f64) {
640-
if let Some(pos) = symbols.return_type_map.iter().position(|e| e.name == fn_name) {
641-
if symbols.return_type_map[pos].confidence >= confidence { return; }
642-
symbols.return_type_map.swap_remove(pos);
643-
}
644645
symbols.return_type_map.push(TypeMapEntry {
645646
name: fn_name.to_string(),
646647
type_name: type_name.to_string(),

crates/codegraph-core/src/extractors/kotlin.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ impl SymbolExtractor for KotlinExtractor {
1313
walk_tree(&tree.root_node(), source, &mut symbols, match_kotlin_node);
1414
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &KOTLIN_AST_CONFIG);
1515
walk_tree(&tree.root_node(), source, &mut symbols, match_kotlin_type_map);
16+
dedup_type_map(&mut symbols.type_map);
1617
symbols
1718
}
1819
}

crates/codegraph-core/src/extractors/php.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ impl SymbolExtractor for PhpExtractor {
1313
walk_tree(&tree.root_node(), source, &mut symbols, match_php_node);
1414
walk_tree(&tree.root_node(), source, &mut symbols, match_php_type_map);
1515
walk_ast_nodes_with_config(&tree.root_node(), source, &mut symbols.ast_nodes, &PHP_AST_CONFIG);
16+
dedup_type_map(&mut symbols.type_map);
1617
symbols
1718
}
1819
}

0 commit comments

Comments
 (0)