Improve export perf: guard debug logs, scope triggers, memoize path bindings.

Reduce multi-config call-chain cost by filtering triggers before pathfinding and cache path-binding walks per session, while avoiding debug string work on hot constant-resolution paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 09:59:57 +02:00
parent 5d3333d494
commit 82a70b5374
4 changed files with 47 additions and 9 deletions

View File

@@ -1,5 +1,6 @@
package click.kamil.springstatemachineexporter.analysis.enricher;
import click.kamil.springstatemachineexporter.analysis.enricher.MachineScopeFilter;
import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
import click.kamil.springstatemachineexporter.analysis.model.CallChain;
import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
@@ -26,10 +27,12 @@ public class CallChainEnricher implements AnalysisEnricher {
List<TriggerPoint> canonicalTriggers = intelligence.findTriggerPoints().stream()
.map(trigger -> MachineEnumCanonicalizer.canonicalizeTriggerPoint(trigger, machineTypes))
.collect(Collectors.toList());
List<TriggerPoint> scopedTriggers = MachineScopeFilter.filterTriggersForMachine(
canonicalTriggers, result.getName(), context);
List<CallChain> chains = intelligence.findCallChains(
result.getMetadata().getEntryPoints(),
canonicalTriggers
scopedTriggers
);
chains = chains.stream()
.map(chain -> chain.getTriggerPoint() == null

View File

@@ -104,6 +104,7 @@ public abstract class AbstractCallGraphEngine implements CallGraphEngine {
polymorphicCallCache.clear();
context.clearAnalysisCaches();
pathFinder.clearAnalysisCaches();
pathBindingEvaluator.clearAnalysisCaches();
parsedNodeCache.clear();
Map<String, List<CallEdge>> callGraph = buildCallGraph();
List<CallChain> chains = new ArrayList<>();

View File

@@ -305,19 +305,26 @@ public class ConstantExtractor {
CompilationUnit contextCu,
ResolutionBudget budget,
boolean allowWidenToImplementations) {
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
final boolean debug = log.isDebugEnabled();
if (debug) {
log.debug("ConstantExtractor.resolveMethodReturnConstant CALLED: className={}, methodName={}", className, methodName);
log.debug("Entering resolveMethodReturnConstant with className={}, methodName={}, depth={}", className, methodName, depth);
}
if (budget == null) {
budget = ResolutionBudget.defaults();
}
final ResolutionBudget activeBudget = budget;
if (activeBudget.isMethodReturnExhausted(depth)) {
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
if (debug) {
log.debug("Exiting resolveMethodReturnConstant early (depth limit) for {}.{}", className, methodName);
}
return Collections.emptyList();
}
String fqn = className + "." + methodName;
if (!visited.add(fqn)) {
log.debug("Exiting resolveMethodReturnConstant early (cycle detected) for fqn={}", fqn);
if (debug) {
log.debug("Exiting resolveMethodReturnConstant early (cycle detected) for fqn={}", fqn);
}
return Collections.emptyList();
}
@@ -483,8 +490,9 @@ public class ConstantExtractor {
}
}
visited.remove(fqn);
log.debug("resolveMethodReturnConstant className={} methodName={} returns: {}", className, methodName, constants);
log.debug("resolveMethodReturnConstant for class={}, method={} resolved constants: {}", className, methodName, constants);
if (debug) {
log.debug("resolveMethodReturnConstant className={} methodName={} returns: {}", className, methodName, constants);
}
return constants;
}

View File

@@ -9,6 +9,7 @@ import org.eclipse.jdt.core.dom.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
/**
* Walks a call path forward, propagating known parameter / local bindings so switch-case
@@ -20,6 +21,7 @@ public class PathBindingEvaluator {
private final VariableTracer variableTracer;
private final ConstantResolver constantResolver;
private final TypeResolver typeResolver;
private final Map<String, Map<String, String>> traceBindingsCache = new HashMap<>();
public PathBindingEvaluator(
CodebaseContext context,
@@ -34,10 +36,26 @@ public class PathBindingEvaluator {
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */
Map<String, String> traceBindings(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
Map<String, String> bindings = new HashMap<>();
if (path == null || path.size() < 2) {
return bindings;
return Map.of();
}
String cacheKey = pathCacheKey(path);
Map<String, String> cached = traceBindingsCache.get(cacheKey);
if (cached != null) {
return new HashMap<>(cached);
}
Map<String, String> bindings = computeTraceBindings(path, callGraph, pathFinder);
traceBindingsCache.put(cacheKey, Map.copyOf(bindings));
return new HashMap<>(bindings);
}
public void clearAnalysisCaches() {
traceBindingsCache.clear();
}
private Map<String, String> computeTraceBindings(
List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
Map<String, String> bindings = new HashMap<>();
for (int i = 0; i < path.size() - 1; i++) {
String caller = path.get(i);
String target = path.get(i + 1);
@@ -50,6 +68,14 @@ public class PathBindingEvaluator {
return bindings;
}
private static String pathCacheKey(List<String> path) {
StringJoiner joiner = new StringJoiner("\0");
for (String hop : path) {
joiner.add(hop);
}
return joiner.toString();
}
public boolean isPathCompatible(
List<String> path,
Map<String, List<CallEdge>> callGraph,