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

View File

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

View File

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

View File

@@ -9,6 +9,7 @@ import org.eclipse.jdt.core.dom.*;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.StringJoiner;
/** /**
* Walks a call path forward, propagating known parameter / local bindings so switch-case * 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 VariableTracer variableTracer;
private final ConstantResolver constantResolver; private final ConstantResolver constantResolver;
private final TypeResolver typeResolver; private final TypeResolver typeResolver;
private final Map<String, Map<String, String>> traceBindingsCache = new HashMap<>();
public PathBindingEvaluator( public PathBindingEvaluator(
CodebaseContext context, CodebaseContext context,
@@ -34,10 +36,26 @@ public class PathBindingEvaluator {
/** Visible for tests: bindings accumulated while walking a path forward (ignores constraint rejection). */ /** 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> traceBindings(List<String> path, Map<String, List<CallEdge>> callGraph, CallGraphPathFinder pathFinder) {
Map<String, String> bindings = new HashMap<>();
if (path == null || path.size() < 2) { 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++) { for (int i = 0; i < path.size() - 1; i++) {
String caller = path.get(i); String caller = path.get(i);
String target = path.get(i + 1); String target = path.get(i + 1);
@@ -50,6 +68,14 @@ public class PathBindingEvaluator {
return bindings; 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( public boolean isPathCompatible(
List<String> path, List<String> path,
Map<String, List<CallEdge>> callGraph, Map<String, List<CallEdge>> callGraph,