mirror of
https://github.com/giancarloerra/socraticode.git
synced 2026-07-03 14:05:21 +02:00
2d686a2688
Addresses six gaps in the prior Impact Analysis work: 1. **Scale benchmarks** (was missing): tests/unit/symbol-graph-scale.test.ts exercises sharding/hashing at 10k–100k symbol volumes with loose regression thresholds (>10× slowdown to fail). 2. **Per-language symbol-extraction tests** (was ~15% of plan): tests/unit/graph-symbols.test.ts now covers Rust, Java/Kotlin/Scala (JVM), C#, C/C++, Ruby, PHP, Swift, Bash, and the regex fallback path. 3. **Phase F (per-file incremental updates)** — implemented as src/services/symbol-graph-incremental.ts with updateChangedFilesSymbolGraph(). Re-extracts changed files, diffs against persisted payloads via contentHash, patches only affected name (≤27) and reverse-call (≤256) shards, and updates meta counts incrementally. Integration tests in tests/integration/symbol-graph-incremental.test.ts. Watcher wiring is documented as a follow-up in CHANGELOG (still does full rebuild on save). 4. **Integration tests for the four new MCP tools**: codebase_impact, codebase_flow, codebase_symbol, codebase_symbols added to tests/integration/tools.test.ts. 5. **Symbol-graph store unit tests**: tests/unit/symbol-graph-store.test.ts covers nameShardKey, allNameShardKeys, reverseShardKey, reverseShardHex, contentHashOf — 14 tests. 6. **Bug fix**: Java/Kotlin/Swift/Scala silently failed because ast-grep throws 'Invalid Kind' when a queried node-kind doesn't exist for that grammar (e.g. object_declaration is Kotlin-only). The outer try/catch in extractSymbolsAndCalls swallowed the error and returned only <module>. Fixed via safeFindAll wrapper applied to all 36 call sites in src/services/graph-symbols.ts. Also fixes a CodeRabbit-flagged comment/code mismatch in src/services/graph-entrypoints.ts (now actually checks for it/describe test names as the comment claimed). Tests: 676 unit pass. Lint clean. CodeRabbit clean. Snyk clean.
111 lines
4.3 KiB
TypeScript
111 lines
4.3 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-only
|
||
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
|
||
|
||
/**
|
||
* Symbol-graph scale & smoke benchmarks.
|
||
*
|
||
* These are CPU-only synthetic benchmarks — they exercise the sharding
|
||
* helpers, hash function, and in-memory shard packing using realistic
|
||
* volumes (10k–100k symbols). They do NOT hit Qdrant; that path is covered
|
||
* by the Qdrant-backed integration tests in
|
||
* `tests/integration/symbol-graph-incremental.test.ts`.
|
||
*
|
||
* Goal: catch order-of-magnitude regressions in the sharding/hashing layer
|
||
* before they reach a real codebase. Thresholds are deliberately loose to
|
||
* avoid CI flakiness — we only fail on >10× slowdowns.
|
||
*/
|
||
|
||
import { describe, expect, it } from "vitest";
|
||
import {
|
||
contentHashOf,
|
||
nameShardKey,
|
||
reverseShardKey,
|
||
} from "../../src/services/symbol-graph-store.js";
|
||
|
||
describe("symbol-graph scale benchmarks", () => {
|
||
it("nameShardKey handles 100k names quickly with even distribution", () => {
|
||
const buckets = new Map<string, number>();
|
||
const N = 100_000;
|
||
const start = process.hrtime.bigint();
|
||
const ALPHA = "abcdefghijklmnopqrstuvwxyz_";
|
||
for (let i = 0; i < N; i++) {
|
||
// Spread first character across all 27 shard buckets.
|
||
const first = ALPHA[i % ALPHA.length];
|
||
const name = `${first}${i.toString(36)}`;
|
||
const key = nameShardKey(name);
|
||
buckets.set(key, (buckets.get(key) ?? 0) + 1);
|
||
}
|
||
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||
// 100k name-key calls should complete in well under 1s on any modern CPU.
|
||
expect(elapsedMs).toBeLessThan(1000);
|
||
// Distribution: at least 20 distinct shard buckets must be hit.
|
||
expect(buckets.size).toBeGreaterThan(20);
|
||
});
|
||
|
||
it("reverseShardKey distributes 50k file paths across hundreds of buckets", () => {
|
||
const buckets = new Map<number, number>();
|
||
const N = 50_000;
|
||
const start = process.hrtime.bigint();
|
||
for (let i = 0; i < N; i++) {
|
||
const filePath = `src/module_${i % 500}/file_${i}.ts`;
|
||
const bucket = reverseShardKey(filePath);
|
||
buckets.set(bucket, (buckets.get(bucket) ?? 0) + 1);
|
||
}
|
||
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||
expect(elapsedMs).toBeLessThan(2000);
|
||
// SHA-256 mod 256 should hit a wide spread of buckets.
|
||
expect(buckets.size).toBeGreaterThan(150);
|
||
});
|
||
|
||
it("contentHashOf processes 10MB of synthetic source under a 5s budget", () => {
|
||
const N = 1000;
|
||
// ~10KB per file × 1000 files = ~10MB total.
|
||
const source = "function foo() { return 1; }\n".repeat(350);
|
||
const start = process.hrtime.bigint();
|
||
let lastHash = "";
|
||
for (let i = 0; i < N; i++) {
|
||
lastHash = contentHashOf(source + i);
|
||
}
|
||
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||
expect(elapsedMs).toBeLessThan(5000);
|
||
expect(lastHash).toMatch(/^[0-9a-f]{64}$/);
|
||
});
|
||
|
||
it("packing 10k symbols into name shards stays under 200ms", () => {
|
||
// Simulates the in-memory shard map build that persistSymbolGraph does
|
||
// before the network round-trip.
|
||
const ALPHA = "abcdefghijklmnopqrstuvwxyz_";
|
||
const N = 10_000;
|
||
const shards: Record<string, Array<{ file: string; id: string }>> = {};
|
||
const start = process.hrtime.bigint();
|
||
for (let i = 0; i < N; i++) {
|
||
const first = ALPHA[i % ALPHA.length];
|
||
const name = `${first}sym_${i.toString(36)}`;
|
||
const key = nameShardKey(name);
|
||
let bucket = shards[key];
|
||
if (!bucket) {
|
||
bucket = [];
|
||
shards[key] = bucket;
|
||
}
|
||
bucket.push({ file: `src/file_${i % 1000}.ts`, id: `src/file_${i}.ts::${name}` });
|
||
}
|
||
const elapsedMs = Number(process.hrtime.bigint() - start) / 1e6;
|
||
expect(elapsedMs).toBeLessThan(200);
|
||
expect(Object.keys(shards).length).toBeGreaterThan(10);
|
||
});
|
||
|
||
it("memory headroom: 100k payload-shaped objects fit under 200MB heap", () => {
|
||
// Sanity check that the in-memory shape we build at scale doesn't blow up.
|
||
const N = 100_000;
|
||
const before = process.memoryUsage().heapUsed;
|
||
const arr: Array<{ file: string; line: number; name: string }> = [];
|
||
for (let i = 0; i < N; i++) {
|
||
arr.push({ file: `src/m${i % 1000}/f${i}.ts`, line: i % 500, name: `s${i}` });
|
||
}
|
||
const after = process.memoryUsage().heapUsed;
|
||
const deltaMB = (after - before) / (1024 * 1024);
|
||
expect(deltaMB).toBeLessThan(200);
|
||
expect(arr.length).toBe(N);
|
||
});
|
||
});
|