Skip to content

Commit 90401ee

Browse files
committed
Merge branch 'perf/cypher-stats-cache-invalidation'
2 parents 820dfab + 336a40a commit 90401ee

9 files changed

Lines changed: 308 additions & 81 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
@@ -520,6 +520,7 @@ private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int b
520520
var batchExecutorService = parallelism != null && parallelism > 0
521521
? Executors.newFixedThreadPool(parallelism)
522522
: Executors.newVirtualThreadPerTaskExecutor();
523+
try (batchExecutorService) {
523524
List<DiscoveredFile> batch = new ArrayList<>(batchSize);
524525
for (int fileIdx = 0; fileIdx < files.size(); fileIdx++) {
525526
batch.add(files.get(fileIdx));
@@ -634,7 +635,7 @@ private AnalysisResult runBatchedWithCache(Path root, Integer parallelism, int b
634635
batch.clear();
635636
}
636637
}
637-
batchExecutorService.close();
638+
} // close batchExecutorService
638639

639640
if (cacheHits > 0) {
640641
report.accept("Cache hits: " + cacheHits + " / " + totalFiles + " files");
@@ -1288,15 +1289,6 @@ private static List<java.util.regex.Pattern> compileExcludePatterns(List<String>
12881289
.toList();
12891290
}
12901291

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

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.context.annotation.Profile;
99
import org.springframework.web.bind.annotation.GetMapping;
1010
import org.springframework.web.bind.annotation.PathVariable;
11+
import org.springframework.web.bind.annotation.PostMapping;
1112
import org.springframework.web.bind.annotation.RequestMapping;
1213
import org.springframework.web.bind.annotation.RequestParam;
1314
import org.springframework.web.bind.annotation.RestController;
@@ -196,13 +197,6 @@ public List<Map<String, Object>> searchGraph(
196197
return queryService.searchGraph(q, Math.min(limit, 1000));
197198
}
198199

199-
/**
200-
* Check whether Neo4j (via QueryService) is available for queries.
201-
*/
202-
private boolean useNeo4j() {
203-
return queryService != null;
204-
}
205-
206200
private void requireQueryService() {
207201
if (queryService == null) {
208202
throw new ResponseStatusException(HttpStatus.SERVICE_UNAVAILABLE,

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/graph/GraphStore.java

Lines changed: 257 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
import java.util.Map;
2222
import java.util.Optional;
2323
import java.util.Set;
24+
import java.util.TreeMap;
25+
import java.util.stream.Collectors;
2426

2527
/**
2628
* Facade service over the Neo4j graph backend.
@@ -400,7 +402,7 @@ public long countEdges() {
400402
public long countDistinctFiles() {
401403
try (Transaction tx = graphDb.beginTx()) {
402404
var result = tx.execute(
403-
"MATCH (n:CodeNode) WHERE n.filePath IS NOT NULL "
405+
"MATCH (n:CodeNode) WHERE n.filePath IS NOT NULL AND n.filePath <> '' "
404406
+ "RETURN count(DISTINCT n.filePath) AS cnt");
405407
if (result.hasNext()) {
406408
return ((Number) result.next().get("cnt")).longValue();
@@ -646,6 +648,260 @@ public Map<String, Object> getTopology() {
646648
return topology;
647649
}
648650

651+
// --- Stats aggregation queries (Cypher-only, no node hydration) ---
652+
653+
/**
654+
* Compute all categorized stats via Cypher aggregation.
655+
* Never loads full nodes into heap — safe for 100K+ node graphs.
656+
*/
657+
public Map<String, Object> computeAggregateStats() {
658+
Map<String, Object> result = new LinkedHashMap<>();
659+
result.put("graph", computeGraphStats());
660+
result.put("languages", computeLanguageStats());
661+
result.put("frameworks", computeFrameworkStats());
662+
result.put("infra", computeInfraStats());
663+
result.put("connections", computeConnectionStats());
664+
result.put("auth", computeAuthStats());
665+
result.put("architecture", computeArchitectureStats());
666+
return result;
667+
}
668+
669+
/**
670+
* Compute stats for a single category via Cypher aggregation.
671+
*/
672+
public Map<String, Object> computeAggregateCategoryStats(String category) {
673+
return switch (category.toLowerCase()) {
674+
case "graph" -> computeGraphStats();
675+
case "languages" -> computeLanguageStats();
676+
case "frameworks" -> computeFrameworkStats();
677+
case "infra" -> computeInfraStats();
678+
case "connections" -> computeConnectionStats();
679+
case "auth" -> computeAuthStats();
680+
case "architecture" -> computeArchitectureStats();
681+
default -> null;
682+
};
683+
}
684+
685+
private Map<String, Object> computeGraphStats() {
686+
Map<String, Object> graph = new LinkedHashMap<>();
687+
graph.put("nodes", count());
688+
graph.put("edges", countEdges());
689+
graph.put("files", countDistinctFiles());
690+
return graph;
691+
}
692+
693+
private Map<String, Object> computeLanguageStats() {
694+
Map<String, Long> langCounts = new TreeMap<>();
695+
try (Transaction tx = graphDb.beginTx()) {
696+
var result = tx.execute(
697+
"MATCH (n:CodeNode) WHERE n.prop_language IS NOT NULL "
698+
+ "RETURN toLower(n.prop_language) AS lang, count(n) AS cnt");
699+
while (result.hasNext()) {
700+
var row = result.next();
701+
String lang = String.valueOf(row.get("lang")).trim();
702+
if (!lang.isBlank()) {
703+
langCounts.merge(lang, ((Number) row.get("cnt")).longValue(), Long::sum);
704+
}
705+
}
706+
}
707+
if (langCounts.isEmpty()) {
708+
for (Map<String, Object> row : countByFileExtension()) {
709+
String ext = String.valueOf(row.get("ext")).trim().toLowerCase();
710+
String lang = extensionToLanguage(ext);
711+
if (lang != null) {
712+
langCounts.merge(lang, ((Number) row.get("cnt")).longValue(), Long::sum);
713+
}
714+
}
715+
}
716+
return new LinkedHashMap<>(sortByValueDesc(langCounts));
717+
}
718+
719+
private Map<String, Object> computeFrameworkStats() {
720+
Map<String, Long> fwCounts = new TreeMap<>();
721+
try (Transaction tx = graphDb.beginTx()) {
722+
var result = tx.execute(
723+
"MATCH (n:CodeNode) WHERE n.prop_framework IS NOT NULL "
724+
+ "RETURN n.prop_framework AS fw, count(n) AS cnt");
725+
while (result.hasNext()) {
726+
var row = result.next();
727+
String fw = String.valueOf(row.get("fw")).trim();
728+
if (!fw.isBlank()) {
729+
fwCounts.merge(fw, ((Number) row.get("cnt")).longValue(), Long::sum);
730+
}
731+
}
732+
}
733+
return new LinkedHashMap<>(sortByValueDesc(fwCounts));
734+
}
735+
736+
private Map<String, Object> computeInfraStats() {
737+
Map<String, Object> infra = new LinkedHashMap<>();
738+
739+
Map<String, Long> databases = new TreeMap<>();
740+
try (Transaction tx = graphDb.beginTx()) {
741+
var result = tx.execute(
742+
"MATCH (n:CodeNode) WHERE n.kind = 'database_connection' AND n.prop_db_type IS NOT NULL "
743+
+ "RETURN n.prop_db_type AS dbType, count(n) AS cnt");
744+
while (result.hasNext()) {
745+
var row = result.next();
746+
String dbType = normalizeDbType(String.valueOf(row.get("dbType")));
747+
if (dbType != null) {
748+
databases.merge(dbType, ((Number) row.get("cnt")).longValue(), Long::sum);
749+
}
750+
}
751+
}
752+
infra.put("databases", sortByValueDesc(databases));
753+
754+
Map<String, Long> messaging = new TreeMap<>();
755+
try (Transaction tx = graphDb.beginTx()) {
756+
var result = tx.execute(
757+
"MATCH (n:CodeNode) WHERE n.kind IN ['topic', 'queue', 'message_queue'] "
758+
+ "RETURN coalesce(n.prop_protocol, n.label, 'unknown') AS protocol, count(n) AS cnt");
759+
while (result.hasNext()) {
760+
var row = result.next();
761+
messaging.merge(String.valueOf(row.get("protocol")), ((Number) row.get("cnt")).longValue(), Long::sum);
762+
}
763+
}
764+
infra.put("messaging", sortByValueDesc(messaging));
765+
766+
Map<String, Long> cloud = new TreeMap<>();
767+
try (Transaction tx = graphDb.beginTx()) {
768+
var result = tx.execute(
769+
"MATCH (n:CodeNode) WHERE n.kind IN ['azure_resource', 'infra_resource'] "
770+
+ "RETURN coalesce(n.prop_resource_type, n.label, 'unknown') AS resType, count(n) AS cnt");
771+
while (result.hasNext()) {
772+
var row = result.next();
773+
cloud.merge(String.valueOf(row.get("resType")), ((Number) row.get("cnt")).longValue(), Long::sum);
774+
}
775+
}
776+
infra.put("cloud", sortByValueDesc(cloud));
777+
778+
return infra;
779+
}
780+
781+
private Map<String, Object> computeConnectionStats() {
782+
Map<String, Object> connections = new LinkedHashMap<>();
783+
try (Transaction tx = graphDb.beginTx()) {
784+
var restResult = tx.execute(
785+
"MATCH (n:CodeNode) WHERE n.kind = 'endpoint' "
786+
+ "AND (n.prop_protocol IS NULL OR n.prop_protocol <> 'grpc') "
787+
+ "RETURN coalesce(toUpper(n.prop_http_method), 'UNKNOWN') AS method, count(n) AS cnt");
788+
Map<String, Long> restByMethod = new TreeMap<>();
789+
while (restResult.hasNext()) {
790+
var row = restResult.next();
791+
restByMethod.put(String.valueOf(row.get("method")), ((Number) row.get("cnt")).longValue());
792+
}
793+
long restTotal = restByMethod.values().stream().mapToLong(Long::longValue).sum();
794+
Map<String, Object> rest = new LinkedHashMap<>();
795+
rest.put("total", restTotal);
796+
rest.put("by_method", sortByValueDesc(restByMethod));
797+
connections.put("rest", rest);
798+
799+
var grpcResult = tx.execute(
800+
"MATCH (n:CodeNode) WHERE n.kind = 'endpoint' AND n.prop_protocol = 'grpc' RETURN count(n) AS cnt");
801+
connections.put("grpc", grpcResult.hasNext() ? ((Number) grpcResult.next().get("cnt")).longValue() : 0L);
802+
803+
var wsResult = tx.execute(
804+
"MATCH (n:CodeNode) WHERE n.kind = 'websocket_endpoint' RETURN count(n) AS cnt");
805+
connections.put("websocket", wsResult.hasNext() ? ((Number) wsResult.next().get("cnt")).longValue() : 0L);
806+
807+
var prodResult = tx.execute(
808+
"MATCH ()-[r:RELATES_TO]->() WHERE r.kind IN ['produces', 'publishes'] RETURN count(r) AS cnt");
809+
connections.put("producers", prodResult.hasNext() ? ((Number) prodResult.next().get("cnt")).longValue() : 0L);
810+
811+
var consResult = tx.execute(
812+
"MATCH ()-[r:RELATES_TO]->() WHERE r.kind IN ['consumes', 'listens'] RETURN count(r) AS cnt");
813+
connections.put("consumers", consResult.hasNext() ? ((Number) consResult.next().get("cnt")).longValue() : 0L);
814+
}
815+
return connections;
816+
}
817+
818+
private Map<String, Object> computeAuthStats() {
819+
Map<String, Long> authCounts = new TreeMap<>();
820+
try (Transaction tx = graphDb.beginTx()) {
821+
var guardResult = tx.execute(
822+
"MATCH (n:CodeNode) WHERE n.kind = 'guard' AND n.prop_auth_type IS NOT NULL "
823+
+ "RETURN n.prop_auth_type AS authType, count(n) AS cnt");
824+
while (guardResult.hasNext()) {
825+
var row = guardResult.next();
826+
String authType = String.valueOf(row.get("authType")).trim();
827+
if (!authType.isBlank()) {
828+
authCounts.merge(authType, ((Number) row.get("cnt")).longValue(), Long::sum);
829+
}
830+
}
831+
var fwResult = tx.execute(
832+
"MATCH (n:CodeNode) WHERE n.prop_framework STARTS WITH 'auth:' "
833+
+ "RETURN n.prop_framework AS fw, count(n) AS cnt");
834+
while (fwResult.hasNext()) {
835+
var row = fwResult.next();
836+
String fw = String.valueOf(row.get("fw")).trim();
837+
String authType = fw.substring("auth:".length()).trim();
838+
if (!authType.isEmpty()) {
839+
authCounts.merge(authType, ((Number) row.get("cnt")).longValue(), Long::sum);
840+
}
841+
}
842+
}
843+
return new LinkedHashMap<>(sortByValueDesc(authCounts));
844+
}
845+
846+
private Map<String, Object> computeArchitectureStats() {
847+
Map<String, Object> arch = new LinkedHashMap<>();
848+
Map<String, String> kindToLabel = Map.of(
849+
"class", "classes", "interface", "interfaces",
850+
"abstract_class", "abstract_classes", "enum", "enums",
851+
"annotation_type", "annotation_types", "module", "modules",
852+
"method", "methods");
853+
List<String> archKinds = List.of("class", "interface", "abstract_class", "enum",
854+
"annotation_type", "module", "method");
855+
try (Transaction tx = graphDb.beginTx()) {
856+
var result = tx.execute(
857+
"MATCH (n:CodeNode) WHERE n.kind IN $kinds RETURN n.kind AS kind, count(n) AS cnt",
858+
Map.of("kinds", archKinds));
859+
while (result.hasNext()) {
860+
var row = result.next();
861+
String kind = (String) row.get("kind");
862+
long cnt = ((Number) row.get("cnt")).longValue();
863+
if (cnt > 0) {
864+
arch.put(kindToLabel.getOrDefault(kind, kind), cnt);
865+
}
866+
}
867+
}
868+
return arch;
869+
}
870+
871+
private static final Map<String, String> STATS_DB_TYPE_NORMALIZE = Map.ofEntries(
872+
Map.entry("mysql", "MySQL"), Map.entry("postgresql", "PostgreSQL"),
873+
Map.entry("postgres", "PostgreSQL"), Map.entry("sqlserver", "SQL Server"),
874+
Map.entry("mssql", "SQL Server"), Map.entry("oracle", "Oracle"),
875+
Map.entry("h2", "H2"), Map.entry("sqlite", "SQLite"),
876+
Map.entry("mariadb", "MariaDB"), Map.entry("mongo", "MongoDB"),
877+
Map.entry("mongodb", "MongoDB"), Map.entry("redis", "Redis"),
878+
Map.entry("neo4j", "Neo4j"));
879+
880+
private static String normalizeDbType(String raw) {
881+
String lower = raw.trim().toLowerCase();
882+
if (lower.contains("@")) lower = lower.substring(0, lower.indexOf('@'));
883+
return STATS_DB_TYPE_NORMALIZE.getOrDefault(lower, raw.trim());
884+
}
885+
886+
private static String extensionToLanguage(String ext) {
887+
return switch (ext) {
888+
case "java" -> "java"; case "kt", "kts" -> "kotlin";
889+
case "py" -> "python"; case "js", "mjs", "cjs" -> "javascript";
890+
case "ts", "tsx" -> "typescript"; case "go" -> "go";
891+
case "rs" -> "rust"; case "cs" -> "csharp";
892+
case "scala" -> "scala"; case "cpp", "cc", "cxx" -> "cpp";
893+
case "proto" -> "protobuf"; default -> null;
894+
};
895+
}
896+
897+
private static <K> Map<K, Long> sortByValueDesc(Map<K, Long> map) {
898+
return map.entrySet().stream()
899+
.sorted(Map.Entry.<K, Long>comparingByValue().reversed())
900+
.collect(Collectors.toMap(
901+
Map.Entry::getKey, Map.Entry::getValue,
902+
(a, b) -> a, LinkedHashMap::new));
903+
}
904+
649905
// --- Internal helpers ---
650906

651907
/**

0 commit comments

Comments
 (0)