Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion code2llm/analysis/call_graph_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,36 @@ def calculate_call_metrics(
metrics: Dict[str, Any] | None = None,
) -> Dict[str, Dict[str, Any]]:
"""Calculate fan-in, fan-out and populate called_by for all functions."""
from code2llm.analysis.native_bridge import native_calculate_call_metrics

res_metrics = metrics if metrics is not None else {}

# Fast path: native Rust
fn_tuples = []
for name, fi in functions.items():
calls = list(getattr(fi, "calls", []))
called_by = list(getattr(fi, "called_by", []))
raw_cc = getattr(fi, "complexity", 1.0)
cc_val = float(raw_cc.get("cyclomatic", 1.0) if isinstance(raw_cc, dict) else (raw_cc or 1.0))
fn_tuples.append((name, calls, called_by, cc_val))

native_res = native_calculate_call_metrics(fn_tuples)
if native_res is not None:
computed_metrics, called_by_map = native_res
for name, callers in called_by_map.items():
if name in functions:
fi = functions[name]
if hasattr(fi, "called_by"):
fi.called_by = callers
for name, (fan_in, fan_out, cc) in computed_metrics.items():
res_metrics[name] = {
"fan_in": fan_in,
"fan_out": fan_out,
"complexity": cc,
}
return res_metrics

# Fallback: Python implementation
for caller_name, caller_info in functions.items():
calls = getattr(caller_info, "calls", [])
for callee_name in calls:
Expand All @@ -213,7 +243,6 @@ def calculate_call_metrics(
if called_by is not None and caller_name not in called_by:
called_by.append(caller_name)

res_metrics = metrics if metrics is not None else {}
for func_name, func_info in functions.items():
calls = getattr(func_info, "calls", [])
called_by = getattr(func_info, "called_by", [])
Expand Down
28 changes: 28 additions & 0 deletions code2llm/analysis/native_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,4 +254,32 @@ def native_resolve_call_graph(
return None


def native_find_pipeline_paths(
nodes: List[str],
edges: List[Tuple[str, str]],
min_length: int = 3,
max_pipelines: int = 12,
) -> Optional[List[List[str]]]:
"""Find pipeline paths using native Rust."""
if _HAS_NATIVE_RUST and hasattr(code2llm_rust, "find_pipeline_paths"):
try:
return code2llm_rust.find_pipeline_paths(nodes, edges, min_length, max_pipelines)
except Exception as e:
logger.debug("code2llm_rust.find_pipeline_paths failed: %s", e)
return None


def native_calculate_call_metrics(
functions: List[Tuple[str, List[str], List[str], float]],
) -> Optional[Tuple[Dict[str, Tuple[int, int, float]], Dict[str, List[str]]]]:
"""Calculate call metrics (fan_in, fan_out, complexity) and callers using native Rust."""
if _HAS_NATIVE_RUST and hasattr(code2llm_rust, "calculate_call_metrics"):
try:
return code2llm_rust.calculate_call_metrics(functions)
except Exception as e:
logger.debug("code2llm_rust.calculate_call_metrics failed: %s", e)
return None




27 changes: 21 additions & 6 deletions code2llm/analysis/pipeline_detector.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,13 +150,28 @@ def detect(
if se_info is None:
se_info = self._se_detector.analyze_all(funcs)

# Build networkx DiGraph
graph = self._build_graph(funcs)
if graph.number_of_nodes() == 0:
return []
# Fast path: native Rust pipeline path finding
from code2llm.analysis.native_bridge import native_find_pipeline_paths

# Find pipeline candidates (longest paths in DAG)
paths = self._find_pipeline_paths(graph)
nodes = list(funcs.keys())
edges: List[Tuple[str, str]] = []
for qname, fi in funcs.items():
for callee in fi.calls:
resolved = self._resolver.resolve(callee, funcs, caller=fi)
if resolved and resolved != qname:
edges.append((qname, resolved))

native_paths = native_find_pipeline_paths(
nodes, edges, MIN_PIPELINE_LENGTH, MAX_PIPELINES
)
if native_paths is not None:
paths = native_paths
else:
# Build networkx DiGraph
graph = self._build_graph(funcs)
if graph.number_of_nodes() == 0:
return []
paths = self._find_pipeline_paths(graph)

# Build Pipeline objects with stages, purity, domain
pipelines = self._build_pipelines(paths, funcs, se_info)
Expand Down
8 changes: 8 additions & 0 deletions docs/analysis/rust-optimization-breakdown.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ Through systematic profiling and code analysis of `code2llm` during multi-langua
- **Problem**: In `ProjectAnalyzer._build_call_graph`, iterating through every function's call list in Python, executing string `rsplit` operations to find module matches, and building `called_by` with quadratic member checks adds significant runtime on large repositories.
- **Native Rust Solution**: `calls::resolve_call_graph` resolving simple-to-full name candidate lookups with same-module affinity in a single linear pass in native Rust, returning resolved forward calls, backward caller maps, and root entry points.

### Bottleneck L: Pipeline Path Finding & Component Exploration
- **Problem**: In `PipelineDetector._find_pipeline_paths`, executing depth-bounded DFS with Python set union copies (`visited | {successor}`) and NetworkX DAG longest path algorithms on large call graphs creates massive interpreter overhead.
- **Native Rust Solution**: `pipelines::find_pipeline_paths` searching longest paths and weakly connected components directly in native Rust graph structures.

### Bottleneck M: Call Graph Fan-In / Fan-Out Metrics
- **Problem**: In `calculate_call_metrics`, calculating pairwise caller/callee metrics across thousands of functions in Python dictionaries causes high GC churn.
- **Native Rust Solution**: `calls::calculate_call_metrics` vectorizing fan-in, fan-out, and caller maps in a single linear-time native pass.

## 3. Architecture & Refactoring Strategy

To prepare `code2llm` for clean separation and extraction into the `code2llm-rust` package, the following refactoring was implemented:
Expand Down
64 changes: 64 additions & 0 deletions packages/code2llm-rust/src/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,57 @@ pub fn resolve_call_graph(
}
}

/// Result of call metrics calculation.
pub struct CallMetricsResult {
pub metrics: HashMap<String, (usize, usize, f64)>, // fan_in, fan_out, complexity
pub called_by: HashMap<String, Vec<String>>,
}

/// Fast native calculation of fan-in, fan-out and callers.
pub fn calculate_call_metrics(
functions: &[(String, Vec<String>, Vec<String>, f64)],
) -> CallMetricsResult {
let known_funcs: HashSet<&str> = functions.iter().map(|(n, _, _, _)| n.as_str()).collect();

let mut callers_map: HashMap<String, Vec<String>> = HashMap::with_capacity(functions.len());
let mut callers_set: HashMap<&str, HashSet<&str>> = HashMap::with_capacity(functions.len());

for (name, _, called_by, _) in functions {
let entry_set = callers_set.entry(name.as_str()).or_default();
let entry_vec = callers_map.entry(name.clone()).or_default();
for caller in called_by {
if entry_set.insert(caller.as_str()) {
entry_vec.push(caller.clone());
}
}
}

for (caller_name, calls, _, _) in functions {
for callee in calls {
let callee_str = callee.as_str();
if known_funcs.contains(callee_str) {
let entry_set = callers_set.entry(callee_str).or_default();
if entry_set.insert(caller_name.as_str()) {
callers_map.entry(callee.clone()).or_default().push(caller_name.clone());
}
}
}
}

let mut metrics = HashMap::with_capacity(functions.len());
for (name, calls, _, complexity) in functions {
let unique_calls: HashSet<&str> = calls.iter().map(|s| s.as_str()).collect();
let fan_out = unique_calls.len();
let fan_in = callers_set.get(name.as_str()).map(|s| s.len()).unwrap_or(0);
metrics.insert(name.clone(), (fan_in, fan_out, *complexity));
}

CallMetricsResult {
metrics,
called_by: callers_map,
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -183,5 +234,18 @@ mod tests {
assert_eq!(res.called_by["db.query"], vec!["app.helper"]);
assert_eq!(res.entry_points, vec!["app.main"]);
}

#[test]
fn test_calculate_call_metrics() {
let input = vec![
("app.main".to_string(), vec!["app.helper".to_string()], vec![], 2.0),
("app.helper".to_string(), vec![], vec![], 5.0),
];

let res = calculate_call_metrics(&input);
assert_eq!(res.metrics["app.main"], (0, 1, 2.0));
assert_eq!(res.metrics["app.helper"], (1, 0, 5.0));
assert_eq!(res.called_by["app.helper"], vec!["app.main"]);
}
}

32 changes: 32 additions & 0 deletions packages/code2llm-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod reachability;
pub mod smells;
pub mod cache;
pub mod toon;
pub mod pipelines;

/// Extract the body of a function between braces starting at `start_line` (1-indexed).
#[pyfunction]
Expand Down Expand Up @@ -239,6 +240,35 @@ fn resolve_call_graph(
})
}

/// Detect pipeline paths from call graph.
#[pyfunction]
#[pyo3(signature = (nodes, edges, min_length = 3, max_pipelines = 12))]
fn find_pipeline_paths(
py: Python<'_>,
nodes: Vec<String>,
edges: Vec<(String, String)>,
min_length: usize,
max_pipelines: usize,
) -> Vec<Vec<String>> {
py.allow_threads(|| pipelines::find_pipeline_paths(&nodes, &edges, min_length, max_pipelines))
}

/// Calculate call metrics (fan_in, fan_out, complexity) and callers.
#[pyfunction]
#[pyo3(signature = (functions))]
fn calculate_call_metrics(
py: Python<'_>,
functions: Vec<(String, Vec<String>, Vec<String>, f64)>,
) -> (
HashMap<String, (usize, usize, f64)>,
HashMap<String, Vec<String>>,
) {
py.allow_threads(|| {
let res = calls::calculate_call_metrics(&functions);
(res.metrics, res.called_by)
})
}

/// Module initialization
#[pymodule]
fn code2llm_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
Expand All @@ -258,6 +288,8 @@ fn code2llm_rust(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(check_changed_files, m)?)?;
m.add_function(wrap_pyfunction!(format_toon_header, m)?)?;
m.add_function(wrap_pyfunction!(resolve_call_graph, m)?)?;
m.add_function(wrap_pyfunction!(find_pipeline_paths, m)?)?;
m.add_function(wrap_pyfunction!(calculate_call_metrics, m)?)?;
m.add("__version__", env!("CARGO_PKG_VERSION"))?;
Ok(())
}
Expand Down
Loading
Loading