Skip to content

Commit f17eeba

Browse files
aksOpsPaperclip-Paperclipclaude
committed
perf: replace in-memory stats with Cypher aggregations, add cache invalidation
- P1: QueryService.getStats() and getDetailedStats() now use GraphStore.computeAggregateStats() which runs Cypher COUNT/GROUP BY queries instead of loading all nodes into heap via findAll(). This prevents OOM on large codebases (100K+ nodes). - P2: Added POST /api/cache/invalidate endpoint that clears all Spring caches. Allows cache refresh after re-enrichment without server restart. - P4: Wrapped Analyzer.runBatchedIndex() executor in try-with-resources to prevent thread leak on exception between creation (line 522) and close (line 639). - Code cleanup: removed dead matchesAnyExclude() method, removed dead branches in ProjectConfigLoader.applyOverrides(), fixed AbstractJavaParserDetector ThreadLocal misuse (PARSER.remove() defeated pooling), removed unused ServeCommand.COMMAND_NAME, removed unused QueryService.statsService field and useNeo4j() method. All 1431 tests pass. Co-Authored-By: Paperclip <noreply@paperclip.ing> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent abf7f92 commit f17eeba

11 files changed

Lines changed: 327 additions & 89 deletions

File tree

src/main/java/io/github/randomcodespace/iq/analyzer/Analyzer.java

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,7 @@ private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int b
522522
var batchExecutorService = parallelism != null && parallelism > 0
523523
? Executors.newFixedThreadPool(parallelism)
524524
: Executors.newVirtualThreadPerTaskExecutor();
525+
try (batchExecutorService) {
525526
List<DiscoveredFile> batch = new ArrayList<>(batchSize);
526527
for (int fileIdx = 0; fileIdx < files.size(); fileIdx++) {
527528
batch.add(files.get(fileIdx));
@@ -636,7 +637,7 @@ private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int b
636637
batch.clear();
637638
}
638639
}
639-
batchExecutorService.close();
640+
} // close batchExecutorService
640641

641642
if (cacheHits > 0) {
642643
report.accept("Cache hits: " + cacheHits + " / " + totalFiles + " files");
@@ -1290,15 +1291,6 @@ private static List<java.util.regex.Pattern> compileExcludePatterns(List<String>
12901291
.toList();
12911292
}
12921293

1293-
/**
1294-
* Check whether a file path matches any of the given pre-compiled exclude patterns.
1295-
*/
1296-
private static boolean matchesAnyExclude(String filePath, List<String> excludePatterns) {
1297-
if (excludePatterns == null) return false;
1298-
List<java.util.regex.Pattern> compiled = compileExcludePatterns(excludePatterns);
1299-
return matchesAnyCompiledExclude(filePath, compiled);
1300-
}
1301-
13021294
/**
13031295
* Check whether a file path matches any of the given pre-compiled patterns.
13041296
*/

src/main/java/io/github/randomcodespace/iq/api/GraphController.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22

33
import io.github.randomcodespace.iq.config.CodeIqConfig;
44
import io.github.randomcodespace.iq.query.QueryService;
5+
import org.springframework.cache.CacheManager;
56
import org.springframework.http.HttpStatus;
67
import org.springframework.http.MediaType;
78
import org.springframework.http.ResponseEntity;
89
import org.springframework.context.annotation.Profile;
910
import org.springframework.web.bind.annotation.GetMapping;
1011
import org.springframework.web.bind.annotation.PathVariable;
12+
import org.springframework.web.bind.annotation.PostMapping;
1113
import org.springframework.web.bind.annotation.RequestMapping;
1214
import org.springframework.web.bind.annotation.RequestParam;
1315
import org.springframework.web.bind.annotation.RestController;
@@ -30,11 +32,14 @@ public class GraphController {
3032

3133
private final QueryService queryService;
3234
private final CodeIqConfig config;
35+
private final CacheManager cacheManager;
3336

3437
public GraphController(@org.springframework.beans.factory.annotation.Autowired(required = false) QueryService queryService,
35-
CodeIqConfig config) {
38+
CodeIqConfig config,
39+
@org.springframework.beans.factory.annotation.Autowired(required = false) CacheManager cacheManager) {
3640
this.queryService = queryService;
3741
this.config = config;
42+
this.cacheManager = cacheManager;
3843
}
3944

4045
@GetMapping("/stats")
@@ -196,13 +201,6 @@ public List<Map<String, Object>> searchGraph(
196201
return queryService.searchGraph(q, Math.min(limit, 1000));
197202
}
198203

199-
/**
200-
* Check whether Neo4j (via QueryService) is available for queries.
201-
*/
202-
private boolean useNeo4j() {
203-
return queryService != null;
204-
}
205-
206204
private void requireQueryService() {
207205
if (queryService == null) {
208206
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,
@@ -250,6 +248,21 @@ public ResponseEntity<String> readFile(
250248
}
251249
}
252250

251+
@PostMapping("/cache/invalidate")
252+
public Map<String, Object> invalidateCache() {
253+
int cleared = 0;
254+
if (cacheManager != null) {
255+
for (String name : cacheManager.getCacheNames()) {
256+
var cache = cacheManager.getCache(name);
257+
if (cache != null) {
258+
cache.clear();
259+
cleared++;
260+
}
261+
}
262+
}
263+
return Map.of("status", "ok", "caches_cleared", cleared);
264+
}
265+
253266
// POST /api/analyze removed — API/MCP server is read-only.
254267
// Analysis is done locally via CLI: code-iq analyze / code-iq index
255268
// Data is loaded into Neo4j on serve startup (auto-enrich).

src/main/java/io/github/randomcodespace/iq/cli/ServeCommand.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,6 @@ public class ServeCommand implements Callable<Integer> {
3030

3131
private static final Logger log = LoggerFactory.getLogger(ServeCommand.class);
3232

33-
public static final String COMMAND_NAME = "serve";
34-
3533
@Parameters(index = "0", defaultValue = ".", description = "Path to analyzed codebase")
3634
private Path path;
3735

src/main/java/io/github/randomcodespace/iq/config/ProjectConfigLoader.java

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -162,21 +162,8 @@ private static void applyOverrides(Map<String, Object> data, CodeIqConfig config
162162
if (data.containsKey("max_radius")) {
163163
config.setMaxRadius(toInt(data.get("max_radius"), config.getMaxRadius()));
164164
}
165-
// Nested analysis section (matches Python config structure)
166-
if (data.get("analysis") instanceof Map<?, ?> analysis) {
167-
if (analysis.containsKey("parallelism")) {
168-
// Stored for CLI to pick up; not directly in CodeIqConfig
169-
}
170-
if (analysis.containsKey("incremental")) {
171-
// Available for future use
172-
}
173-
}
174-
// Nested output section
175-
if (data.get("output") instanceof Map<?, ?> output) {
176-
if (output.containsKey("max_nodes")) {
177-
// Available for future use
178-
}
179-
}
165+
// Nested analysis/output sections are recognized but not yet mapped to CodeIqConfig.
166+
// They are loaded and accessible via data map for CLI commands that need them.
180167
}
181168

182169
private static int toInt(Object value, int defaultValue) {

src/main/java/io/github/randomcodespace/iq/detector/java/AbstractJavaParserDetector.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ protected Optional<CompilationUnit> parse(DetectorContext ctx) {
2929
// JavaParser may throw AssertionError for unrecognized token kinds
3030
// (e.g. newer Java syntax). Fall back to regex in those cases.
3131
return Optional.empty();
32-
} finally {
33-
PARSER.remove();
3432
}
3533
}
3634

0 commit comments

Comments
 (0)