homegraph 1.4.1 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -11
- package/dist/bin/homegraph.js +295 -72
- package/dist/bin/homegraph.js.map +1 -1
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +32 -11
- package/dist/context/index.js.map +1 -1
- package/dist/db/index.d.ts +143 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +293 -16
- package/dist/db/index.js.map +1 -1
- package/dist/db/queries.d.ts +43 -4
- package/dist/db/queries.d.ts.map +1 -1
- package/dist/db/queries.js +138 -14
- package/dist/db/queries.js.map +1 -1
- package/dist/db/sqlite-adapter.d.ts +24 -17
- package/dist/db/sqlite-adapter.d.ts.map +1 -1
- package/dist/db/sqlite-adapter.js +292 -36
- package/dist/db/sqlite-adapter.js.map +1 -1
- package/dist/db/wal-valve.d.ts +15 -2
- package/dist/db/wal-valve.d.ts.map +1 -1
- package/dist/db/wal-valve.js +88 -6
- package/dist/db/wal-valve.js.map +1 -1
- package/dist/extraction/arkts-batch-worker.js +3 -2
- package/dist/extraction/arkts-batch-worker.js.map +1 -1
- package/dist/extraction/default-ignore.d.ts +39 -0
- package/dist/extraction/default-ignore.d.ts.map +1 -0
- package/dist/extraction/default-ignore.js +221 -0
- package/dist/extraction/default-ignore.js.map +1 -0
- package/dist/extraction/index.d.ts +9 -10
- package/dist/extraction/index.d.ts.map +1 -1
- package/dist/extraction/index.js +227 -323
- package/dist/extraction/index.js.map +1 -1
- package/dist/extraction/languages/arkts.d.ts +90 -6
- package/dist/extraction/languages/arkts.d.ts.map +1 -1
- package/dist/extraction/languages/arkts.js +1579 -128
- package/dist/extraction/languages/arkts.js.map +1 -1
- package/dist/extraction/store-worker.d.ts +20 -0
- package/dist/extraction/store-worker.d.ts.map +1 -0
- package/dist/extraction/store-worker.js +102 -0
- package/dist/extraction/store-worker.js.map +1 -0
- package/dist/extraction/store-writer.d.ts +48 -0
- package/dist/extraction/store-writer.d.ts.map +1 -0
- package/dist/extraction/store-writer.js +167 -0
- package/dist/extraction/store-writer.js.map +1 -0
- package/dist/extraction/tree-sitter.d.ts.map +1 -1
- package/dist/extraction/tree-sitter.js +47 -0
- package/dist/extraction/tree-sitter.js.map +1 -1
- package/dist/extraction/wasm-runtime-flags.d.ts +14 -4
- package/dist/extraction/wasm-runtime-flags.d.ts.map +1 -1
- package/dist/extraction/wasm-runtime-flags.js +55 -7
- package/dist/extraction/wasm-runtime-flags.js.map +1 -1
- package/dist/index.d.ts +16 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +135 -97
- package/dist/index.js.map +1 -1
- package/dist/mcp/query-worker.d.ts +2 -2
- package/dist/mcp/query-worker.js +2 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +42 -26
- package/dist/mcp/tools.js.map +1 -1
- package/dist/resolution/callback-synthesizer.d.ts +52 -16
- package/dist/resolution/callback-synthesizer.d.ts.map +1 -1
- package/dist/resolution/callback-synthesizer.js +234 -180
- package/dist/resolution/callback-synthesizer.js.map +1 -1
- package/dist/resolution/cooperative-yield.d.ts +10 -2
- package/dist/resolution/cooperative-yield.d.ts.map +1 -1
- package/dist/resolution/cooperative-yield.js +6 -4
- package/dist/resolution/cooperative-yield.js.map +1 -1
- package/dist/resolution/frameworks/go.d.ts.map +1 -1
- package/dist/resolution/frameworks/go.js +29 -6
- package/dist/resolution/frameworks/go.js.map +1 -1
- package/dist/resolution/import-resolver.d.ts +2 -3
- package/dist/resolution/import-resolver.d.ts.map +1 -1
- package/dist/resolution/import-resolver.js +166 -7
- package/dist/resolution/import-resolver.js.map +1 -1
- package/dist/resolution/index.d.ts +87 -2
- package/dist/resolution/index.d.ts.map +1 -1
- package/dist/resolution/index.js +739 -131
- package/dist/resolution/index.js.map +1 -1
- package/dist/resolution/memory-budget.d.ts +48 -0
- package/dist/resolution/memory-budget.d.ts.map +1 -0
- package/dist/resolution/memory-budget.js +162 -0
- package/dist/resolution/memory-budget.js.map +1 -0
- package/dist/resolution/name-matcher.d.ts +12 -1
- package/dist/resolution/name-matcher.d.ts.map +1 -1
- package/dist/resolution/name-matcher.js +439 -97
- package/dist/resolution/name-matcher.js.map +1 -1
- package/dist/resolution/resolver-pool.d.ts +106 -0
- package/dist/resolution/resolver-pool.d.ts.map +1 -0
- package/dist/resolution/resolver-pool.js +344 -0
- package/dist/resolution/resolver-pool.js.map +1 -0
- package/dist/resolution/resolver-worker.d.ts +17 -0
- package/dist/resolution/resolver-worker.d.ts.map +1 -0
- package/dist/resolution/resolver-worker.js +141 -0
- package/dist/resolution/resolver-worker.js.map +1 -0
- package/dist/resolution/types.d.ts +3 -0
- package/dist/resolution/types.d.ts.map +1 -1
- package/dist/spec/build/diff-parser.d.ts +55 -0
- package/dist/spec/build/diff-parser.d.ts.map +1 -0
- package/dist/spec/build/diff-parser.js +205 -0
- package/dist/spec/build/diff-parser.js.map +1 -0
- package/dist/spec/build/git-scanner.d.ts +93 -0
- package/dist/spec/build/git-scanner.d.ts.map +1 -0
- package/dist/spec/build/git-scanner.js +254 -0
- package/dist/spec/build/git-scanner.js.map +1 -0
- package/dist/spec/build/pipeline.d.ts +54 -0
- package/dist/spec/build/pipeline.d.ts.map +1 -0
- package/dist/spec/build/pipeline.js +170 -0
- package/dist/spec/build/pipeline.js.map +1 -0
- package/dist/spec/build/scan.d.ts +48 -0
- package/dist/spec/build/scan.d.ts.map +1 -0
- package/dist/spec/build/scan.js +89 -0
- package/dist/spec/build/scan.js.map +1 -0
- package/dist/spec/build/scope-resolver.d.ts +50 -0
- package/dist/spec/build/scope-resolver.d.ts.map +1 -0
- package/dist/spec/build/scope-resolver.js +105 -0
- package/dist/spec/build/scope-resolver.js.map +1 -0
- package/dist/spec/build/spec-extractor.d.ts +74 -0
- package/dist/spec/build/spec-extractor.d.ts.map +1 -0
- package/dist/spec/build/spec-extractor.js +344 -0
- package/dist/spec/build/spec-extractor.js.map +1 -0
- package/dist/spec/config.d.ts +35 -1
- package/dist/spec/config.d.ts.map +1 -1
- package/dist/spec/config.js +30 -5
- package/dist/spec/config.js.map +1 -1
- package/dist/spec/db/fts.d.ts.map +1 -1
- package/dist/spec/db/fts.js +19 -50
- package/dist/spec/db/fts.js.map +1 -1
- package/dist/spec/db/index.d.ts +5 -3
- package/dist/spec/db/index.d.ts.map +1 -1
- package/dist/spec/db/index.js +16 -1
- package/dist/spec/db/index.js.map +1 -1
- package/dist/spec/db/persist.d.ts +45 -0
- package/dist/spec/db/persist.d.ts.map +1 -0
- package/dist/spec/db/persist.js +66 -0
- package/dist/spec/db/persist.js.map +1 -0
- package/dist/spec/db/relations.d.ts +80 -1
- package/dist/spec/db/relations.d.ts.map +1 -1
- package/dist/spec/db/relations.js +175 -4
- package/dist/spec/db/relations.js.map +1 -1
- package/dist/spec/db/schema.d.ts +4 -0
- package/dist/spec/db/schema.d.ts.map +1 -1
- package/dist/spec/db/schema.js +4 -0
- package/dist/spec/db/schema.js.map +1 -1
- package/dist/spec/db/schema.sql +5 -1
- package/dist/spec/db/spec-node.d.ts +5 -0
- package/dist/spec/db/spec-node.d.ts.map +1 -1
- package/dist/spec/db/spec-node.js +11 -5
- package/dist/spec/db/spec-node.js.map +1 -1
- package/dist/spec/db/sql-utils.d.ts +11 -0
- package/dist/spec/db/sql-utils.d.ts.map +1 -0
- package/dist/spec/db/sql-utils.js +19 -0
- package/dist/spec/db/sql-utils.js.map +1 -0
- package/dist/spec/evolve/cluster-context.d.ts +30 -0
- package/dist/spec/evolve/cluster-context.d.ts.map +1 -0
- package/dist/spec/evolve/cluster-context.js +78 -0
- package/dist/spec/evolve/cluster-context.js.map +1 -0
- package/dist/spec/evolve/commit-spec-analyzer.d.ts +51 -0
- package/dist/spec/evolve/commit-spec-analyzer.d.ts.map +1 -0
- package/dist/spec/evolve/commit-spec-analyzer.js +88 -0
- package/dist/spec/evolve/commit-spec-analyzer.js.map +1 -0
- package/dist/spec/evolve/commit-spec-persister.d.ts +53 -0
- package/dist/spec/evolve/commit-spec-persister.d.ts.map +1 -0
- package/dist/spec/evolve/commit-spec-persister.js +93 -0
- package/dist/spec/evolve/commit-spec-persister.js.map +1 -0
- package/dist/spec/evolve/impact-locator.d.ts +23 -7
- package/dist/spec/evolve/impact-locator.d.ts.map +1 -1
- package/dist/spec/evolve/impact-locator.js +58 -14
- package/dist/spec/evolve/impact-locator.js.map +1 -1
- package/dist/spec/evolve/pipeline.d.ts +62 -25
- package/dist/spec/evolve/pipeline.d.ts.map +1 -1
- package/dist/spec/evolve/pipeline.js +436 -457
- package/dist/spec/evolve/pipeline.js.map +1 -1
- package/dist/spec/evolve/spec-rewriter.d.ts +13 -13
- package/dist/spec/evolve/spec-rewriter.d.ts.map +1 -1
- package/dist/spec/evolve/spec-rewriter.js +45 -45
- package/dist/spec/evolve/spec-rewriter.js.map +1 -1
- package/dist/spec/git/commits.d.ts +85 -0
- package/dist/spec/git/commits.d.ts.map +1 -0
- package/dist/spec/git/commits.js +218 -0
- package/dist/spec/git/commits.js.map +1 -0
- package/dist/spec/git/exec.d.ts +13 -0
- package/dist/spec/git/exec.d.ts.map +1 -0
- package/dist/spec/git/exec.js +19 -0
- package/dist/spec/git/exec.js.map +1 -0
- package/dist/spec/git/index.d.ts +9 -0
- package/dist/spec/git/index.d.ts.map +1 -0
- package/dist/spec/git/index.js +22 -0
- package/dist/spec/git/index.js.map +1 -0
- package/dist/spec/git-utils.d.ts +8 -0
- package/dist/spec/git-utils.d.ts.map +1 -0
- package/dist/spec/git-utils.js +14 -0
- package/dist/spec/git-utils.js.map +1 -0
- package/dist/spec/graph/index.d.ts +8 -0
- package/dist/spec/graph/index.d.ts.map +1 -0
- package/dist/spec/graph/index.js +17 -0
- package/dist/spec/graph/index.js.map +1 -0
- package/dist/spec/graph/queries.d.ts +6 -35
- package/dist/spec/graph/queries.d.ts.map +1 -1
- package/dist/spec/graph/queries.js +71 -248
- package/dist/spec/graph/queries.js.map +1 -1
- package/dist/spec/llm/agent-client.d.ts +59 -0
- package/dist/spec/llm/agent-client.d.ts.map +1 -0
- package/dist/spec/llm/agent-client.js +213 -0
- package/dist/spec/llm/agent-client.js.map +1 -0
- package/dist/spec/llm/agents/claude-code.d.ts +32 -0
- package/dist/spec/llm/agents/claude-code.d.ts.map +1 -0
- package/dist/spec/llm/agents/claude-code.js +125 -0
- package/dist/spec/llm/agents/claude-code.js.map +1 -0
- package/dist/spec/llm/agents/codex.d.ts +32 -0
- package/dist/spec/llm/agents/codex.d.ts.map +1 -0
- package/dist/spec/llm/agents/codex.js +145 -0
- package/dist/spec/llm/agents/codex.js.map +1 -0
- package/dist/spec/llm/agents/detect-utils.d.ts +28 -0
- package/dist/spec/llm/agents/detect-utils.d.ts.map +1 -0
- package/dist/spec/llm/agents/detect-utils.js +91 -0
- package/dist/spec/llm/agents/detect-utils.js.map +1 -0
- package/dist/spec/llm/agents/deveco-code.d.ts +33 -0
- package/dist/spec/llm/agents/deveco-code.d.ts.map +1 -0
- package/dist/spec/llm/agents/deveco-code.js +165 -0
- package/dist/spec/llm/agents/deveco-code.js.map +1 -0
- package/dist/spec/llm/agents/index.d.ts +31 -0
- package/dist/spec/llm/agents/index.d.ts.map +1 -0
- package/dist/spec/llm/agents/index.js +65 -0
- package/dist/spec/llm/agents/index.js.map +1 -0
- package/dist/spec/llm/agents/types.d.ts +76 -0
- package/dist/spec/llm/agents/types.d.ts.map +1 -0
- package/dist/spec/llm/agents/types.js +15 -0
- package/dist/spec/llm/agents/types.js.map +1 -0
- package/dist/spec/llm/client.d.ts +31 -11
- package/dist/spec/llm/client.d.ts.map +1 -1
- package/dist/spec/llm/client.js +143 -81
- package/dist/spec/llm/client.js.map +1 -1
- package/dist/spec/llm/factory.d.ts +41 -0
- package/dist/spec/llm/factory.d.ts.map +1 -0
- package/dist/spec/llm/factory.js +80 -0
- package/dist/spec/llm/factory.js.map +1 -0
- package/dist/spec/llm/prompts.d.ts +24 -8
- package/dist/spec/llm/prompts.d.ts.map +1 -1
- package/dist/spec/llm/prompts.js +100 -50
- package/dist/spec/llm/prompts.js.map +1 -1
- package/dist/spec/llm/retry.d.ts +53 -0
- package/dist/spec/llm/retry.d.ts.map +1 -0
- package/dist/spec/llm/retry.js +153 -0
- package/dist/spec/llm/retry.js.map +1 -0
- package/dist/spec/mine/clusterer.d.ts +63 -0
- package/dist/spec/mine/clusterer.d.ts.map +1 -0
- package/dist/spec/mine/clusterer.js +904 -0
- package/dist/spec/mine/clusterer.js.map +1 -0
- package/dist/spec/mine/clustering/features.d.ts +38 -0
- package/dist/spec/mine/clustering/features.d.ts.map +1 -0
- package/dist/spec/mine/clustering/features.js +108 -0
- package/dist/spec/mine/clustering/features.js.map +1 -0
- package/dist/spec/mine/clustering/index.d.ts +62 -0
- package/dist/spec/mine/clustering/index.d.ts.map +1 -0
- package/dist/spec/mine/clustering/index.js +259 -0
- package/dist/spec/mine/clustering/index.js.map +1 -0
- package/dist/spec/mine/clustering/leiden.d.ts +61 -0
- package/dist/spec/mine/clustering/leiden.d.ts.map +1 -0
- package/dist/spec/mine/clustering/leiden.js +489 -0
- package/dist/spec/mine/clustering/leiden.js.map +1 -0
- package/dist/spec/mine/clustering/text-similarity.d.ts +22 -0
- package/dist/spec/mine/clustering/text-similarity.d.ts.map +1 -0
- package/dist/spec/mine/clustering/text-similarity.js +98 -0
- package/dist/spec/mine/clustering/text-similarity.js.map +1 -0
- package/dist/spec/mine/generator.d.ts +54 -0
- package/dist/spec/mine/generator.d.ts.map +1 -0
- package/dist/spec/mine/generator.js +267 -0
- package/dist/spec/mine/generator.js.map +1 -0
- package/dist/spec/mine/persist.d.ts +37 -0
- package/dist/spec/mine/persist.d.ts.map +1 -0
- package/dist/spec/mine/persist.js +170 -0
- package/dist/spec/mine/persist.js.map +1 -0
- package/dist/spec/mine/pipeline.d.ts +41 -0
- package/dist/spec/mine/pipeline.d.ts.map +1 -0
- package/dist/spec/mine/pipeline.js +236 -0
- package/dist/spec/mine/pipeline.js.map +1 -0
- package/dist/spec/mine/progress-handler.d.ts +22 -0
- package/dist/spec/mine/progress-handler.d.ts.map +1 -0
- package/dist/spec/mine/progress-handler.js +108 -0
- package/dist/spec/mine/progress-handler.js.map +1 -0
- package/dist/spec/mine/progress.d.ts +23 -0
- package/dist/spec/mine/progress.d.ts.map +1 -0
- package/dist/spec/mine/progress.js +12 -0
- package/dist/spec/mine/progress.js.map +1 -0
- package/dist/spec/mine/scanner.d.ts +59 -0
- package/dist/spec/mine/scanner.d.ts.map +1 -0
- package/dist/spec/mine/scanner.js +383 -0
- package/dist/spec/mine/scanner.js.map +1 -0
- package/dist/spec/types.d.ts +32 -0
- package/dist/spec/types.d.ts.map +1 -1
- package/dist/spec/ui/index.d.ts +9 -0
- package/dist/spec/ui/index.d.ts.map +1 -0
- package/dist/spec/ui/index.js +16 -0
- package/dist/spec/ui/index.js.map +1 -0
- package/dist/spec/ui/progress-handler.d.ts +33 -0
- package/dist/spec/ui/progress-handler.d.ts.map +1 -0
- package/dist/spec/ui/progress-handler.js +132 -0
- package/dist/spec/ui/progress-handler.js.map +1 -0
- package/dist/spec/ui/progress.d.ts +24 -0
- package/dist/spec/ui/progress.d.ts.map +1 -0
- package/dist/spec/ui/progress.js +13 -0
- package/dist/spec/ui/progress.js.map +1 -0
- package/dist/spec/utils/fs.d.ts +45 -0
- package/dist/spec/utils/fs.d.ts.map +1 -0
- package/dist/spec/utils/fs.js +163 -0
- package/dist/spec/utils/fs.js.map +1 -0
- package/dist/spec/utils/index.d.ts +12 -0
- package/dist/spec/utils/index.d.ts.map +1 -0
- package/dist/spec/utils/index.js +26 -0
- package/dist/spec/utils/index.js.map +1 -0
- package/dist/spec/utils/meta.d.ts +32 -0
- package/dist/spec/utils/meta.d.ts.map +1 -0
- package/dist/spec/utils/meta.js +122 -0
- package/dist/spec/utils/meta.js.map +1 -0
- package/dist/spec/utils/truncate.d.ts +86 -0
- package/dist/spec/utils/truncate.d.ts.map +1 -0
- package/dist/spec/utils/truncate.js +172 -0
- package/dist/spec/utils/truncate.js.map +1 -0
- package/dist/spec/utils.d.ts +4 -16
- package/dist/spec/utils.d.ts.map +1 -1
- package/dist/spec/utils.js +6 -58
- package/dist/spec/utils.js.map +1 -1
- package/dist/sync/watcher.d.ts +25 -1
- package/dist/sync/watcher.d.ts.map +1 -1
- package/dist/sync/watcher.js +76 -3
- package/dist/sync/watcher.js.map +1 -1
- package/dist/telemetry/index.js +3 -3
- package/dist/telemetry/index.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +7 -3
package/dist/resolution/index.js
CHANGED
|
@@ -47,6 +47,7 @@ const fs = __importStar(require("fs"));
|
|
|
47
47
|
const path = __importStar(require("path"));
|
|
48
48
|
const name_matcher_1 = require("./name-matcher");
|
|
49
49
|
const import_resolver_1 = require("./import-resolver");
|
|
50
|
+
const resolver_pool_1 = require("./resolver-pool");
|
|
50
51
|
const frameworks_1 = require("./frameworks");
|
|
51
52
|
const callback_synthesizer_1 = require("./callback-synthesizer");
|
|
52
53
|
const cooperative_yield_1 = require("./cooperative-yield");
|
|
@@ -244,6 +245,32 @@ class ReferenceResolver {
|
|
|
244
245
|
qualifiedNameCache; // qualified_name → nodes cache
|
|
245
246
|
fileLinesCache; // file → split lines cache
|
|
246
247
|
methodMatchCache; // lang\0Type::method → matching method nodes
|
|
248
|
+
// Per-(language, methodName) owner index for getMethodMatches: buckets a
|
|
249
|
+
// method name's candidates by their qualifiedName's last two segments so a
|
|
250
|
+
// (type, method) query is a lookup instead of an O(candidates) filter per
|
|
251
|
+
// methodMatchCache miss. Derived purely from node rows (stable through the
|
|
252
|
+
// resolution loop, same window nameCache relies on); dropped in clearCaches.
|
|
253
|
+
methodOwnerIndexCache = new Map();
|
|
254
|
+
// Generation-tagged memo for getSupertypes. Supertype edges GROW during the
|
|
255
|
+
// resolution loop (batch k persists its implements/extends edges BEFORE
|
|
256
|
+
// batch k+1 fans out — the #1320 ordering), so a plain cache would freeze an
|
|
257
|
+
// early batch's emptier answer and change later batches' outcomes. Within
|
|
258
|
+
// one batch the edge state is fixed by that same ordering, so entries are
|
|
259
|
+
// tagged with a generation that advances at every batch entry point
|
|
260
|
+
// (resolveBatchYielding / resolveListForAdmission) — a stale-gen entry is
|
|
261
|
+
// recomputed, making the memo behavior-identical to no memo at every point
|
|
262
|
+
// in time. On the Swift compiler the unmemoized walk ran 971k times for
|
|
263
|
+
// 565s of combined worker time (~581µs each, recursion-multiplied).
|
|
264
|
+
supertypeGen = 0;
|
|
265
|
+
supertypeMemo = new Map();
|
|
266
|
+
/** Invalidate the getSupertypes memo — call when resolved edges may have advanced. */
|
|
267
|
+
advanceSupertypeGeneration() {
|
|
268
|
+
this.supertypeGen++;
|
|
269
|
+
// Lazy invalidation via the gen tag; bound the map so a long run over many
|
|
270
|
+
// batches doesn't accrete dead entries.
|
|
271
|
+
if (this.supertypeMemo.size > 50_000)
|
|
272
|
+
this.supertypeMemo.clear();
|
|
273
|
+
}
|
|
247
274
|
// Node kinds are a small fixed set (~24), so this is a plain Map, not an LRU.
|
|
248
275
|
// getNodesByKind returns the FULL node list for a kind; it was previously
|
|
249
276
|
// uncached — a per-ref `SELECT * FROM nodes WHERE kind=?` + row-mapping. Called
|
|
@@ -372,10 +399,19 @@ class ReferenceResolver {
|
|
|
372
399
|
this.qualifiedNameCache.clear();
|
|
373
400
|
this.fileLinesCache.clear();
|
|
374
401
|
this.methodMatchCache.clear();
|
|
402
|
+
this.methodOwnerIndexCache.clear();
|
|
403
|
+
this.supertypeMemo.clear();
|
|
404
|
+
this.supertypeGen++;
|
|
375
405
|
this.nodesByKindCache.clear();
|
|
376
406
|
this.knownNames = null;
|
|
377
407
|
this.knownFiles = null;
|
|
378
408
|
this.cachesWarmed = false;
|
|
409
|
+
// The import-resolver's and name-matcher's per-context memos assume the
|
|
410
|
+
// same stable window as the caches above — drop them together.
|
|
411
|
+
if (this.context) {
|
|
412
|
+
(0, import_resolver_1.clearImportResolverMemos)(this.context);
|
|
413
|
+
(0, name_matcher_1.clearNameMatcherMemos)(this.context);
|
|
414
|
+
}
|
|
379
415
|
}
|
|
380
416
|
/** `readFile` through the LRU content cache (null = read failed, also cached). */
|
|
381
417
|
readFileCached(filePath) {
|
|
@@ -424,15 +460,61 @@ class ReferenceResolver {
|
|
|
424
460
|
this.nameCache.set(methodName, candidates);
|
|
425
461
|
}
|
|
426
462
|
const want = `${typeName}::${methodName}`;
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
463
|
+
let matches;
|
|
464
|
+
if (typeName.includes('::') || methodName.includes(':')) {
|
|
465
|
+
// Legacy linear filter for the shapes the owner index below can't
|
|
466
|
+
// key exactly: a multi-segment typeName (the endsWith test then
|
|
467
|
+
// spans more than two `::` segments) and ObjC selectors (whose
|
|
468
|
+
// single/empty-keyword colons defeat the segment split). Tiny
|
|
469
|
+
// populations; the per-key memo above still amortizes them.
|
|
470
|
+
matches = [];
|
|
471
|
+
for (const m of candidates) {
|
|
472
|
+
if (m.kind !== 'method')
|
|
473
|
+
continue;
|
|
474
|
+
if (m.language !== language)
|
|
475
|
+
continue;
|
|
476
|
+
const qn = m.qualifiedName;
|
|
477
|
+
if (qn === want || qn.endsWith(`::${want}`))
|
|
478
|
+
matches.push(m);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
// Owner index: the linear filter above is O(all same-named methods)
|
|
483
|
+
// per CACHE MISS, and on overload-heavy landscapes the distinct
|
|
484
|
+
// (type, method) key space is so large the per-key memo never
|
|
485
|
+
// amortizes — Swift's `init` has tens of thousands of candidates
|
|
486
|
+
// and the compiler repo measured 732µs per failing call, most of it
|
|
487
|
+
// this scan (re-entered once per supertype recursion level, too).
|
|
488
|
+
// Bucket each (language, methodName)'s candidates ONCE by the
|
|
489
|
+
// qualifiedName's last two `::` segments — exactly the span the
|
|
490
|
+
// `qn === want || qn.endsWith('::' + want)` predicate tests for a
|
|
491
|
+
// segment-clean typeName — then every query is a map lookup.
|
|
492
|
+
// Bucket insertion follows candidate order, so each bucket is
|
|
493
|
+
// byte-identical to what the linear filter produced.
|
|
494
|
+
const idxKey = `${language} ${methodName}`;
|
|
495
|
+
let ownerIndex = this.methodOwnerIndexCache.get(idxKey);
|
|
496
|
+
if (!ownerIndex) {
|
|
497
|
+
ownerIndex = new Map();
|
|
498
|
+
for (const m of candidates) {
|
|
499
|
+
if (m.kind !== 'method')
|
|
500
|
+
continue;
|
|
501
|
+
if (m.language !== language)
|
|
502
|
+
continue;
|
|
503
|
+
const qn = m.qualifiedName;
|
|
504
|
+
const i2 = qn.lastIndexOf('::');
|
|
505
|
+
if (i2 < 0)
|
|
506
|
+
continue; // single-segment qn can never match `T::m`
|
|
507
|
+
const i1 = qn.lastIndexOf('::', i2 - 1);
|
|
508
|
+
const bucketKey = i1 < 0 ? qn : qn.slice(i1 + 2);
|
|
509
|
+
const bucket = ownerIndex.get(bucketKey);
|
|
510
|
+
if (bucket)
|
|
511
|
+
bucket.push(m);
|
|
512
|
+
else
|
|
513
|
+
ownerIndex.set(bucketKey, [m]);
|
|
514
|
+
}
|
|
515
|
+
this.methodOwnerIndexCache.set(idxKey, ownerIndex);
|
|
516
|
+
}
|
|
517
|
+
matches = ownerIndex.get(want) ?? [];
|
|
436
518
|
}
|
|
437
519
|
this.methodMatchCache.set(key, matches);
|
|
438
520
|
return matches;
|
|
@@ -524,20 +606,34 @@ class ReferenceResolver {
|
|
|
524
606
|
// Matching by simple name (not id) reconciles a type declared in one node
|
|
525
607
|
// (`KF::Builder`) with conformance declared in a separate extension node
|
|
526
608
|
// (`KF.Builder: KFOptionSetter`) — both have name `Builder`.
|
|
609
|
+
// Memoized per batch generation (see supertypeMemo): within a batch the
|
|
610
|
+
// edge state is fixed, and the conformance walk re-queries the same
|
|
611
|
+
// popular supertypes (Swift stdlib protocols especially) thousands of
|
|
612
|
+
// times per batch.
|
|
613
|
+
const memoKey = `${language} ${typeName}`;
|
|
614
|
+
const hit = this.supertypeMemo.get(memoKey);
|
|
615
|
+
if (hit && hit.gen === this.supertypeGen)
|
|
616
|
+
return hit.supers;
|
|
527
617
|
const typeNodes = this.context
|
|
528
618
|
.getNodesByName(typeName)
|
|
529
619
|
.filter((n) => SUPERTYPE_BEARING_KINDS.has(n.kind) && n.language === language);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
620
|
+
let supers;
|
|
621
|
+
if (typeNodes.length === 0) {
|
|
622
|
+
supers = [];
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
const supertypes = new Set();
|
|
626
|
+
for (const tn of typeNodes) {
|
|
627
|
+
for (const edge of this.queries.getOutgoingEdges(tn.id, ['implements', 'extends'])) {
|
|
628
|
+
const target = this.queries.getNodeById(edge.target);
|
|
629
|
+
if (target?.name && target.name !== typeName)
|
|
630
|
+
supertypes.add(target.name);
|
|
631
|
+
}
|
|
538
632
|
}
|
|
633
|
+
supers = [...supertypes];
|
|
539
634
|
}
|
|
540
|
-
|
|
635
|
+
this.supertypeMemo.set(memoKey, { gen: this.supertypeGen, supers });
|
|
636
|
+
return supers;
|
|
541
637
|
},
|
|
542
638
|
getImportMappings: (filePath, language) => {
|
|
543
639
|
const cacheKey = filePath;
|
|
@@ -615,12 +711,13 @@ class ReferenceResolver {
|
|
|
615
711
|
column: ref.column,
|
|
616
712
|
filePath: ref.filePath || this.getFilePathFromNodeId(ref.fromNodeId),
|
|
617
713
|
language: ref.language || this.getLanguageFromNodeId(ref.fromNodeId),
|
|
714
|
+
rowId: ref.rowId,
|
|
618
715
|
}));
|
|
619
716
|
const total = refs.length;
|
|
620
717
|
let lastReportedPercent = -1;
|
|
621
718
|
for (let i = 0; i < refs.length; i++) {
|
|
622
719
|
const ref = refs[i]; // Array index is guaranteed to be in bounds
|
|
623
|
-
const result = this.
|
|
720
|
+
const result = this.resolveOneTimed(ref);
|
|
624
721
|
if (result) {
|
|
625
722
|
resolved.push(result);
|
|
626
723
|
byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
|
|
@@ -782,10 +879,14 @@ class ReferenceResolver {
|
|
|
782
879
|
const existenceName = ref.language === 'arkts' && ref.referenceName.startsWith('.')
|
|
783
880
|
? ref.referenceName.slice(1)
|
|
784
881
|
: ref.referenceName;
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
882
|
+
const tPre = this.profileStages ? process.hrtime.bigint() : 0n;
|
|
883
|
+
const preFilterPass = (0, import_resolver_1.isNixPathImportRef)(ref) ||
|
|
884
|
+
this.hasAnyPossibleMatch(existenceName) ||
|
|
885
|
+
this.matchesAnyImport(ref) ||
|
|
886
|
+
this.frameworks.some((f) => f.claimsReference?.(ref.referenceName));
|
|
887
|
+
if (this.profileStages)
|
|
888
|
+
this.stageAdd('preFilter', ref, preFilterPass, tPre);
|
|
889
|
+
if (!preFilterPass) {
|
|
789
890
|
return null;
|
|
790
891
|
}
|
|
791
892
|
// Function-as-value refs (#756) get a dedicated, strictly-gated path:
|
|
@@ -811,7 +912,10 @@ class ReferenceResolver {
|
|
|
811
912
|
// JVM FQN imports skip framework/name-matcher: `import com.example.Bar`
|
|
812
913
|
// resolves directly through the qualifiedName index, which is unambiguous
|
|
813
914
|
// even when several `Bar` classes exist in different packages.
|
|
915
|
+
const tJvm = this.profileStages ? process.hrtime.bigint() : 0n;
|
|
814
916
|
const jvmImport = (0, import_resolver_1.resolveJvmImport)(ref, this.context);
|
|
917
|
+
if (this.profileStages)
|
|
918
|
+
this.stageAdd('jvmImport', ref, !!jvmImport, tJvm);
|
|
815
919
|
if (jvmImport)
|
|
816
920
|
return jvmImport;
|
|
817
921
|
// Razor/Blazor: a markup or `@code` type ref resolves through the file's
|
|
@@ -830,16 +934,49 @@ class ReferenceResolver {
|
|
|
830
934
|
// JS → native `calls`) — `gateFrameworkLanguage` only drops a type/import
|
|
831
935
|
// edge between two KNOWN families (see its doc), never a `calls` bridge or
|
|
832
936
|
// a config↔code edge.
|
|
937
|
+
const tFw = this.profileStages ? process.hrtime.bigint() : 0n;
|
|
938
|
+
let fwEarly = null;
|
|
833
939
|
for (const framework of this.frameworks) {
|
|
834
940
|
const result = this.gateFrameworkLanguage(framework.resolve(ref, this.context), ref);
|
|
835
941
|
if (result) {
|
|
836
|
-
if (result.confidence >= 0.9)
|
|
837
|
-
|
|
942
|
+
if (result.confidence >= 0.9) {
|
|
943
|
+
fwEarly = result; // High confidence, return immediately (below)
|
|
944
|
+
break;
|
|
945
|
+
}
|
|
838
946
|
candidates.push(result);
|
|
839
947
|
}
|
|
840
948
|
}
|
|
949
|
+
if (this.profileStages)
|
|
950
|
+
this.stageAdd('frameworks', ref, fwEarly !== null, tFw);
|
|
951
|
+
if (fwEarly)
|
|
952
|
+
return fwEarly;
|
|
841
953
|
// Strategy 2: Try import-based resolution
|
|
842
|
-
const
|
|
954
|
+
const tImp = this.profileStages ? process.hrtime.bigint() : 0n;
|
|
955
|
+
let importResult = this.gateLanguage((0, import_resolver_1.resolveViaImport)(ref, this.context), ref);
|
|
956
|
+
// ArkTS:
|
|
957
|
+
// - `instantiates`: never via import — `import { Foo }` + a constructor
|
|
958
|
+
// seed would mint a `new Foo` edge for every false CFG constructor in
|
|
959
|
+
// that file (99k instantiates on scene_board). Same-file / unique-class
|
|
960
|
+
// exact-match in matchReference only.
|
|
961
|
+
// - dotted `Class.method`: import → static member (cross-module path;
|
|
962
|
+
// Strategy 1 is same-file only).
|
|
963
|
+
// - bare calls: only when the import local maps to a function/method.
|
|
964
|
+
if (importResult &&
|
|
965
|
+
ref.language === 'arkts' &&
|
|
966
|
+
(ref.referenceKind === 'calls' || ref.referenceKind === 'instantiates')) {
|
|
967
|
+
if (ref.referenceKind === 'instantiates') {
|
|
968
|
+
importResult = null;
|
|
969
|
+
}
|
|
970
|
+
else {
|
|
971
|
+
const target = this.queries.getNodeById(importResult.targetNodeId);
|
|
972
|
+
const ok = target &&
|
|
973
|
+
(target.kind === 'function' || target.kind === 'method');
|
|
974
|
+
if (!ok)
|
|
975
|
+
importResult = null;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (this.profileStages)
|
|
979
|
+
this.stageAdd('viaImport', ref, !!importResult, tImp);
|
|
843
980
|
if (importResult) {
|
|
844
981
|
if (importResult.confidence >= 0.9)
|
|
845
982
|
return importResult;
|
|
@@ -861,7 +998,10 @@ class ReferenceResolver {
|
|
|
861
998
|
: null;
|
|
862
999
|
}
|
|
863
1000
|
// Strategy 3: Try name matching
|
|
1001
|
+
const tName = this.profileStages ? process.hrtime.bigint() : 0n;
|
|
864
1002
|
let nameResult = this.gateLanguage((0, name_matcher_1.matchReference)(ref, this.context), ref);
|
|
1003
|
+
if (this.profileStages)
|
|
1004
|
+
this.stageAdd('nameMatch', ref, !!nameResult, tName);
|
|
865
1005
|
// Nix has no ambient cross-file namespace — a callee binds lexically
|
|
866
1006
|
// (same file) or through explicit import/callPackage wiring (the import
|
|
867
1007
|
// path above). A cross-file name match is wrong by construction: every
|
|
@@ -971,6 +1111,52 @@ class ReferenceResolver {
|
|
|
971
1111
|
};
|
|
972
1112
|
});
|
|
973
1113
|
}
|
|
1114
|
+
/**
|
|
1115
|
+
* Split resolved refs into rows deletable by id and hand-built refs that
|
|
1116
|
+
* must fall back to the key-tuple delete. Rows loaded from the database
|
|
1117
|
+
* carry their row id and are deleted by exactly that id; the key tuple
|
|
1118
|
+
* omits line/col, so it also removes SIBLING rows — the same caller calling
|
|
1119
|
+
* the same callee at other lines — that a later batch hadn't attempted yet:
|
|
1120
|
+
* when a batch boundary split a caller's same-named call sites, the later
|
|
1121
|
+
* sites' edges were silently never created (#1269).
|
|
1122
|
+
*/
|
|
1123
|
+
static partitionResolvedCleanup(resolved) {
|
|
1124
|
+
const rowIds = [];
|
|
1125
|
+
const legacyKeys = [];
|
|
1126
|
+
for (const r of resolved) {
|
|
1127
|
+
if (r.original.rowId != null)
|
|
1128
|
+
rowIds.push(r.original.rowId);
|
|
1129
|
+
else
|
|
1130
|
+
legacyKeys.push({
|
|
1131
|
+
fromNodeId: r.original.fromNodeId,
|
|
1132
|
+
referenceName: r.original.referenceName,
|
|
1133
|
+
referenceKind: r.original.referenceKind,
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
return { rowIds, legacyKeys };
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Same row-id precision for parking unresolvable refs as status='failed'
|
|
1140
|
+
* (#1240): the key-tuple fallback would flip same-key sibling rows in later
|
|
1141
|
+
* batches to 'failed' before they were ever attempted, and resolution
|
|
1142
|
+
* outcome can differ per call site (receiver-type inference reads the
|
|
1143
|
+
* ref's line), so a sibling must not inherit this row's failure (#1269).
|
|
1144
|
+
*/
|
|
1145
|
+
static partitionFailedCleanup(unresolved) {
|
|
1146
|
+
const byRowId = [];
|
|
1147
|
+
const legacyKeys = [];
|
|
1148
|
+
for (const r of unresolved) {
|
|
1149
|
+
if (r.rowId != null)
|
|
1150
|
+
byRowId.push({ rowId: r.rowId, referenceName: r.referenceName });
|
|
1151
|
+
else
|
|
1152
|
+
legacyKeys.push({
|
|
1153
|
+
fromNodeId: r.fromNodeId,
|
|
1154
|
+
referenceName: r.referenceName,
|
|
1155
|
+
referenceKind: r.referenceKind,
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
return { byRowId, legacyKeys };
|
|
1159
|
+
}
|
|
974
1160
|
/**
|
|
975
1161
|
* Resolve and persist edges to database
|
|
976
1162
|
*/
|
|
@@ -984,11 +1170,9 @@ class ReferenceResolver {
|
|
|
984
1170
|
}
|
|
985
1171
|
// Clean up resolved refs from unresolved_refs table so metrics are accurate
|
|
986
1172
|
if (result.resolved.length > 0) {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
referenceKind: r.original.referenceKind,
|
|
991
|
-
})));
|
|
1173
|
+
const { rowIds, legacyKeys } = ReferenceResolver.partitionResolvedCleanup(result.resolved);
|
|
1174
|
+
this.queries.deleteReferencesByRowIds(rowIds);
|
|
1175
|
+
this.queries.deleteSpecificResolvedReferences(legacyKeys);
|
|
992
1176
|
}
|
|
993
1177
|
// Park unresolvable refs as status='failed' — parity with
|
|
994
1178
|
// resolveAndPersistBatched. Deleting them was wrong (#1240): a ref whose
|
|
@@ -1000,11 +1184,9 @@ class ReferenceResolver {
|
|
|
1000
1184
|
// is still 'pending', so any pending row at rest belongs to an
|
|
1001
1185
|
// interrupted run and the sweep can key off the pending count.
|
|
1002
1186
|
if (result.unresolved.length > 0) {
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
referenceKind: r.referenceKind,
|
|
1007
|
-
})));
|
|
1187
|
+
const { byRowId, legacyKeys } = ReferenceResolver.partitionFailedCleanup(result.unresolved);
|
|
1188
|
+
this.queries.markReferencesFailedByRowIds(byRowId);
|
|
1189
|
+
this.queries.markReferencesFailed(legacyKeys);
|
|
1008
1190
|
}
|
|
1009
1191
|
return result;
|
|
1010
1192
|
}
|
|
@@ -1026,22 +1208,22 @@ class ReferenceResolver {
|
|
|
1026
1208
|
this.queries.insertEdges(edges.slice(i, i + PERSIST_CHUNK));
|
|
1027
1209
|
await maybeYield();
|
|
1028
1210
|
}
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
referenceKind: r.original.referenceKind,
|
|
1033
|
-
}));
|
|
1034
|
-
for (let i = 0; i < resolvedKeys.length; i += PERSIST_CHUNK) {
|
|
1035
|
-
this.queries.deleteSpecificResolvedReferences(resolvedKeys.slice(i, i + PERSIST_CHUNK));
|
|
1211
|
+
const resolvedCleanup = ReferenceResolver.partitionResolvedCleanup(result.resolved);
|
|
1212
|
+
for (let i = 0; i < resolvedCleanup.rowIds.length; i += PERSIST_CHUNK) {
|
|
1213
|
+
this.queries.deleteReferencesByRowIds(resolvedCleanup.rowIds.slice(i, i + PERSIST_CHUNK));
|
|
1036
1214
|
await maybeYield();
|
|
1037
1215
|
}
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
for (let i = 0; i <
|
|
1044
|
-
this.queries.
|
|
1216
|
+
for (let i = 0; i < resolvedCleanup.legacyKeys.length; i += PERSIST_CHUNK) {
|
|
1217
|
+
this.queries.deleteSpecificResolvedReferences(resolvedCleanup.legacyKeys.slice(i, i + PERSIST_CHUNK));
|
|
1218
|
+
await maybeYield();
|
|
1219
|
+
}
|
|
1220
|
+
const failedCleanup = ReferenceResolver.partitionFailedCleanup(result.unresolved);
|
|
1221
|
+
for (let i = 0; i < failedCleanup.byRowId.length; i += PERSIST_CHUNK) {
|
|
1222
|
+
this.queries.markReferencesFailedByRowIds(failedCleanup.byRowId.slice(i, i + PERSIST_CHUNK));
|
|
1223
|
+
await maybeYield();
|
|
1224
|
+
}
|
|
1225
|
+
for (let i = 0; i < failedCleanup.legacyKeys.length; i += PERSIST_CHUNK) {
|
|
1226
|
+
this.queries.markReferencesFailed(failedCleanup.legacyKeys.slice(i, i + PERSIST_CHUNK));
|
|
1045
1227
|
await maybeYield();
|
|
1046
1228
|
}
|
|
1047
1229
|
return result;
|
|
@@ -1112,6 +1294,7 @@ class ReferenceResolver {
|
|
|
1112
1294
|
*/
|
|
1113
1295
|
async resolveBatchYielding(batch, maybeYield) {
|
|
1114
1296
|
this.warmCaches();
|
|
1297
|
+
this.advanceSupertypeGeneration();
|
|
1115
1298
|
const resolved = [];
|
|
1116
1299
|
const unresolved = [];
|
|
1117
1300
|
const byMethod = {};
|
|
@@ -1124,8 +1307,9 @@ class ReferenceResolver {
|
|
|
1124
1307
|
column: raw.column,
|
|
1125
1308
|
filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId),
|
|
1126
1309
|
language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId),
|
|
1310
|
+
rowId: raw.rowId,
|
|
1127
1311
|
};
|
|
1128
|
-
const result = this.
|
|
1312
|
+
const result = this.resolveOneTimed(ref);
|
|
1129
1313
|
if (result) {
|
|
1130
1314
|
resolved.push(result);
|
|
1131
1315
|
byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
|
|
@@ -1133,7 +1317,11 @@ class ReferenceResolver {
|
|
|
1133
1317
|
else {
|
|
1134
1318
|
unresolved.push(ref);
|
|
1135
1319
|
}
|
|
1136
|
-
|
|
1320
|
+
// Fast-path the per-ref yield check: awaiting the async no-op costs a
|
|
1321
|
+
// microtask hop per ref, which dominates at ~10⁵ refs (see MaybeYield).
|
|
1322
|
+
const y = maybeYield();
|
|
1323
|
+
if (y)
|
|
1324
|
+
await y;
|
|
1137
1325
|
}
|
|
1138
1326
|
return {
|
|
1139
1327
|
resolved,
|
|
@@ -1146,12 +1334,144 @@ class ReferenceResolver {
|
|
|
1146
1334
|
},
|
|
1147
1335
|
};
|
|
1148
1336
|
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Resolve a list of refs and return everything the ADMISSION side needs to
|
|
1339
|
+
* persist the outcome: resolutions, failures, the deferred post-pass refs
|
|
1340
|
+
* this run produced (drained, so the caller owns routing them), and stats.
|
|
1341
|
+
* This is the resolver-worker entry point — it runs the exact per-ref loop
|
|
1342
|
+
* of resolveBatchYielding, minus the main-thread yields (worker threads have
|
|
1343
|
+
* no watchdog heartbeat to starve). Results are in input order.
|
|
1344
|
+
*/
|
|
1345
|
+
/**
|
|
1346
|
+
* HOMEGRAPH_RESOLVE_PROFILE=1: per-outcome wall-clock histogram of
|
|
1347
|
+
* resolveOne, keyed by the winning strategy (`resolvedBy`) or
|
|
1348
|
+
* `fail:<referenceKind>` — the §7a.2 "profile the per-ref path" probe. The
|
|
1349
|
+
* kernel-scale batch loop is ~430s and CORE-INVARIANT (835.9s pooled-4-on-8
|
|
1350
|
+
* ≈ 812.5s sequential-on-2 for the whole superphase), so the next lever is
|
|
1351
|
+
* which CLASS of ref the time belongs to, not more parallelism. Off by
|
|
1352
|
+
* default: the hrtime pair costs ~100ns/ref only when the env is set.
|
|
1353
|
+
*/
|
|
1354
|
+
resolveProfile = process.env.HOMEGRAPH_RESOLVE_PROFILE ? new Map() : null;
|
|
1355
|
+
/**
|
|
1356
|
+
* HOMEGRAPH_RESOLVE_PROFILE=2 additionally attributes time to the
|
|
1357
|
+
* STRATEGIES inside resolveOne (`stage:<name>|<refKind>|hit/miss` rows in
|
|
1358
|
+
* the same histogram) — i.e. WHICH machinery a failing class of refs pays
|
|
1359
|
+
* for, not just that it fails. =1 keeps the per-outcome rows only.
|
|
1360
|
+
*/
|
|
1361
|
+
profileStages = process.env.HOMEGRAPH_RESOLVE_PROFILE === '2';
|
|
1362
|
+
stageAdd(stage, ref, hit, t0) {
|
|
1363
|
+
if (!this.resolveProfile)
|
|
1364
|
+
return;
|
|
1365
|
+
const dt = process.hrtime.bigint() - t0;
|
|
1366
|
+
const key = `stage:${stage}|${ref.referenceKind}|${hit ? 'hit' : 'miss'}`;
|
|
1367
|
+
const slot = this.resolveProfile.get(key);
|
|
1368
|
+
if (slot) {
|
|
1369
|
+
slot.n++;
|
|
1370
|
+
slot.ns += dt;
|
|
1371
|
+
}
|
|
1372
|
+
else {
|
|
1373
|
+
this.resolveProfile.set(key, { n: 1, ns: dt });
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
resolveOneTimed(ref) {
|
|
1377
|
+
if (!this.resolveProfile)
|
|
1378
|
+
return this.resolveOne(ref);
|
|
1379
|
+
const t0 = process.hrtime.bigint();
|
|
1380
|
+
const result = this.resolveOne(ref);
|
|
1381
|
+
const dt = process.hrtime.bigint() - t0;
|
|
1382
|
+
const key = result ? result.resolvedBy : `fail:${ref.referenceKind}`;
|
|
1383
|
+
const slot = this.resolveProfile.get(key);
|
|
1384
|
+
if (slot) {
|
|
1385
|
+
slot.n++;
|
|
1386
|
+
slot.ns += dt;
|
|
1387
|
+
}
|
|
1388
|
+
else {
|
|
1389
|
+
this.resolveProfile.set(key, { n: 1, ns: dt });
|
|
1390
|
+
}
|
|
1391
|
+
return result;
|
|
1392
|
+
}
|
|
1393
|
+
/** Dump the HOMEGRAPH_RESOLVE_PROFILE histogram to stderr (no-op when off). */
|
|
1394
|
+
dumpResolveProfile(label) {
|
|
1395
|
+
if (!this.resolveProfile || this.resolveProfile.size === 0)
|
|
1396
|
+
return;
|
|
1397
|
+
const rows = [...this.resolveProfile.entries()]
|
|
1398
|
+
.map(([k, v]) => ({ k, n: v.n, ms: Number(v.ns / 1000000n) }))
|
|
1399
|
+
.sort((a, b) => b.ms - a.ms);
|
|
1400
|
+
for (const r of rows) {
|
|
1401
|
+
console.error(`[resolve-profile] ${label} ${r.k}: n=${r.n} total=${(r.ms / 1000).toFixed(1)}s avg=${((r.ms * 1000) / Math.max(1, r.n)).toFixed(0)}µs`);
|
|
1402
|
+
}
|
|
1403
|
+
// =2 only: this thread's matchReference sub-stage table rides along.
|
|
1404
|
+
(0, name_matcher_1.dumpNameMatcherProfile)(label);
|
|
1405
|
+
}
|
|
1406
|
+
resolveListForAdmission(refs) {
|
|
1407
|
+
this.warmCaches();
|
|
1408
|
+
this.advanceSupertypeGeneration();
|
|
1409
|
+
const resolved = [];
|
|
1410
|
+
const unresolved = [];
|
|
1411
|
+
const byMethod = {};
|
|
1412
|
+
for (const raw of refs) {
|
|
1413
|
+
const ref = {
|
|
1414
|
+
fromNodeId: raw.fromNodeId,
|
|
1415
|
+
referenceName: raw.referenceName,
|
|
1416
|
+
referenceKind: raw.referenceKind,
|
|
1417
|
+
line: raw.line,
|
|
1418
|
+
column: raw.column,
|
|
1419
|
+
filePath: raw.filePath || this.getFilePathFromNodeId(raw.fromNodeId),
|
|
1420
|
+
language: raw.language || this.getLanguageFromNodeId(raw.fromNodeId),
|
|
1421
|
+
rowId: raw.rowId,
|
|
1422
|
+
};
|
|
1423
|
+
const result = this.resolveOneTimed(ref);
|
|
1424
|
+
if (result) {
|
|
1425
|
+
resolved.push(result);
|
|
1426
|
+
byMethod[result.resolvedBy] = (byMethod[result.resolvedBy] || 0) + 1;
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
unresolved.push(ref);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return {
|
|
1433
|
+
resolved,
|
|
1434
|
+
unresolved,
|
|
1435
|
+
deferredChain: this.deferredChainRefs.splice(0),
|
|
1436
|
+
deferredThisMember: this.deferredThisMemberRefs.splice(0),
|
|
1437
|
+
byMethod,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* The resolver's live ResolutionContext — resolver-pool workers use it to
|
|
1442
|
+
* run synthesis passes against their own read-only connection.
|
|
1443
|
+
*/
|
|
1444
|
+
getResolutionContext() {
|
|
1445
|
+
return this.context;
|
|
1446
|
+
}
|
|
1447
|
+
/**
|
|
1448
|
+
* Re-queue deferred post-pass refs produced by resolver workers, preserving
|
|
1449
|
+
* their admission order so resolveChainedCallsViaConformance /
|
|
1450
|
+
* resolveDeferredThisMemberRefs process them exactly as the sequential path
|
|
1451
|
+
* would have.
|
|
1452
|
+
*/
|
|
1453
|
+
appendDeferredFromWorkers(deferredChain, deferredThisMember) {
|
|
1454
|
+
this.deferredChainRefs.push(...deferredChain);
|
|
1455
|
+
this.deferredThisMemberRefs.push(...deferredThisMember);
|
|
1456
|
+
}
|
|
1149
1457
|
/**
|
|
1150
1458
|
* Resolve and persist in batches to keep memory bounded.
|
|
1151
1459
|
* Processes unresolved references in chunks, persisting edges and cleaning
|
|
1152
1460
|
* up resolved refs after each batch to avoid accumulating large arrays.
|
|
1153
1461
|
*/
|
|
1154
|
-
async resolveAndPersistBatched(onProgress, batchSize = 5000, onSynthesisProgress
|
|
1462
|
+
async resolveAndPersistBatched(onProgress, batchSize = 5000, onSynthesisProgress,
|
|
1463
|
+
// When provided, big batches fan out across a read-only resolver-worker
|
|
1464
|
+
// pool with results admitted in canonical order (see resolver-pool.ts).
|
|
1465
|
+
// Sequential fallback on any pool failure. HOMEGRAPH_NO_PARALLEL_RESOLVE=1
|
|
1466
|
+
// disables entirely. bulkEdgeLoad hooks (when provided) bracket the batch
|
|
1467
|
+
// loop with drop/recreate of the non-unique edge indexes on big runs —
|
|
1468
|
+
// see DatabaseConnection.beginBulkEdgeLoad. backpressure (when provided)
|
|
1469
|
+
// is the WAL valve's writer-side backstop (WalCheckpointValve.backpressure):
|
|
1470
|
+
// called at pool-idle boundaries so a full backfill can actually complete —
|
|
1471
|
+
// the valve's timer-driven passive passes stay perpetually partial against
|
|
1472
|
+
// the pool's continuous reads, which is how a kernel-scale resolution grew
|
|
1473
|
+
// a 22GB WAL on a 4.6GB DB (migration plan §7a.1).
|
|
1474
|
+
parallel) {
|
|
1155
1475
|
// Resolution runs on the indexer's MAIN thread, and the #850 liveness
|
|
1156
1476
|
// watchdog SIGKILLs a process whose event loop stalls past its window (60s
|
|
1157
1477
|
// by default). A single dense batch's resolveAll — or the synthesis pass
|
|
@@ -1159,6 +1479,19 @@ class ReferenceResolver {
|
|
|
1159
1479
|
// (#1091). A shared yielder lets both give the watchdog heartbeat a regular
|
|
1160
1480
|
// window to fire; see ./cooperative-yield.
|
|
1161
1481
|
const maybeYield = (0, cooperative_yield_1.createYielder)();
|
|
1482
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS) {
|
|
1483
|
+
console.error(`[pool-timing] backpressure hook: ${parallel?.backpressure ? 'present' : 'absent'}`);
|
|
1484
|
+
}
|
|
1485
|
+
// HOMEGRAPH_RESOLVE_PROFILE loop-stage attribution: the §7a.2 kernel-scale
|
|
1486
|
+
// histogram showed resolveOne owns only ~93s of the ~436s batch loop —
|
|
1487
|
+
// these counters name where the other ~340s goes (reads, edge build+insert,
|
|
1488
|
+
// deletes/marks, the per-batch count guard).
|
|
1489
|
+
const loopProf = process.env.HOMEGRAPH_RESOLVE_PROFILE
|
|
1490
|
+
? { read: 0, settle: 0, backpressure: 0, recycle: 0, createEdges: 0, insertEdges: 0, deletes: 0, marks: 0, countGuard: 0 }
|
|
1491
|
+
: null;
|
|
1492
|
+
const lp = (k, t0) => { if (loopProf)
|
|
1493
|
+
loopProf[k] = (loopProf[k] ?? 0) + (Date.now() - t0); };
|
|
1494
|
+
let tLp = 0;
|
|
1162
1495
|
await this.warmCachesYielding(maybeYield);
|
|
1163
1496
|
const total = this.queries.getUnresolvedReferencesCount();
|
|
1164
1497
|
let processed = 0;
|
|
@@ -1168,95 +1501,370 @@ class ReferenceResolver {
|
|
|
1168
1501
|
unresolved: 0,
|
|
1169
1502
|
byMethod: {},
|
|
1170
1503
|
};
|
|
1171
|
-
//
|
|
1172
|
-
//
|
|
1504
|
+
// Parallel pool, started immediately but never awaited up front: early
|
|
1505
|
+
// batches run sequentially while the workers boot (module load + readonly
|
|
1506
|
+
// DB open + framework detect + cache warm ≈ hundreds of ms), and the loop
|
|
1507
|
+
// switches to fan-out the moment the pool reports ready — so pool boot
|
|
1508
|
+
// costs zero wall-clock. Any failure downgrades to sequential permanently.
|
|
1509
|
+
let pool = null;
|
|
1510
|
+
let poolReady = false;
|
|
1511
|
+
// True once pool creation has been attempted by EITHER engage site (the
|
|
1512
|
+
// up-front ref-count gate or the adaptive projection below) — a pool that
|
|
1513
|
+
// failed or was destroyed must stay down (downgrade is permanent), and
|
|
1514
|
+
// tryCreate's sizing probes shouldn't re-run every batch on hosts that
|
|
1515
|
+
// declined.
|
|
1516
|
+
let poolEngageTried = false;
|
|
1517
|
+
const createPool = (t0, why) => {
|
|
1518
|
+
poolEngageTried = true;
|
|
1519
|
+
if (!parallel)
|
|
1520
|
+
return null;
|
|
1521
|
+
const p = resolver_pool_1.ResolverPool.tryCreate(parallel.dbPath, this.projectRoot);
|
|
1522
|
+
p?.ready().then(() => {
|
|
1523
|
+
poolReady = true;
|
|
1524
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
|
|
1525
|
+
console.error(`[pool-timing] pool ready after ${Date.now() - t0}ms (${why})`);
|
|
1526
|
+
}, () => {
|
|
1527
|
+
void p.destroy().catch(() => undefined);
|
|
1528
|
+
if (pool === p)
|
|
1529
|
+
pool = null;
|
|
1530
|
+
});
|
|
1531
|
+
return p;
|
|
1532
|
+
};
|
|
1533
|
+
if (parallel && total >= (0, resolver_pool_1.minRefsForPool)()) {
|
|
1534
|
+
pool = createPool(Date.now(), 'ref-count');
|
|
1535
|
+
}
|
|
1536
|
+
// Adaptive engagement bar (see the batch-loop hook): projected remaining
|
|
1537
|
+
// sequential settle above this boots the pool mid-loop. Boot is async and
|
|
1538
|
+
// fan-out waits for ready, so a marginal engage costs background boot
|
|
1539
|
+
// only; the bar just needs to clear the fan-out's own overhead class.
|
|
1540
|
+
const ADAPTIVE_ENGAGE_SETTLE_MS = 400;
|
|
1541
|
+
let adaptiveSeqMs = 0;
|
|
1542
|
+
let adaptiveSeqRefs = 0;
|
|
1543
|
+
// Process in PIPELINED batches (double-buffer). The enumeration is the
|
|
1544
|
+
// head of the pending set in rowid order; every ref a persisted batch
|
|
1545
|
+
// processed leaves the pending set (resolved rows are deleted,
|
|
1173
1546
|
// unresolvable ones flip to status='failed'), shifting the remaining
|
|
1174
1547
|
// pending rows forward.
|
|
1175
1548
|
let prevRemaining = Number.POSITIVE_INFINITY;
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1549
|
+
// Cadence for the worker connection recycling below — ~8 batches
|
|
1550
|
+
// ≈ 40k refs between recycles keeps the WAL shallow at kernel scale
|
|
1551
|
+
// while a small sync never recycles at all. (25 recovered only half
|
|
1552
|
+
// the write tax — the WAL re-deepened between recycles; reopens are
|
|
1553
|
+
// sub-millisecond so the shorter cadence is ~free.)
|
|
1554
|
+
const RECYCLE_EVERY_BATCHES = 8;
|
|
1555
|
+
let batchesSinceRecycle = 0;
|
|
1556
|
+
// Begin one batch: fan out to the pool when it's ready and the batch is
|
|
1557
|
+
// big enough — workers then resolve batch k+1 WHILE the main thread
|
|
1558
|
+
// persists batch k (persist measured at ~58% of resolution wall on a
|
|
1559
|
+
// 255k-ref repo, all of it previously spent with the pool idle).
|
|
1560
|
+
// Sequential batches stay lazy: they run on the main thread at settle
|
|
1561
|
+
// time, where an early start would only contend with the persist.
|
|
1562
|
+
const beginBatch = (batch) => {
|
|
1563
|
+
if (pool && poolReady && resolver_pool_1.ResolverPool.worthParallel(batch.length)) {
|
|
1564
|
+
return {
|
|
1565
|
+
mode: 'pool',
|
|
1566
|
+
settled: pool.resolveBatch(batch).then((out) => ({ ok: true, out }), (err) => ({ ok: false, err })),
|
|
1567
|
+
};
|
|
1194
1568
|
}
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1569
|
+
return { mode: 'seq' };
|
|
1570
|
+
};
|
|
1571
|
+
// Settle an in-flight batch to a ResolutionResult. Deferred post-pass refs
|
|
1572
|
+
// are appended HERE, in loop order — never inside the fan-out promise — so
|
|
1573
|
+
// admission order stays exactly the sequential order even while a later
|
|
1574
|
+
// batch resolves concurrently. A pool failure downgrades to sequential
|
|
1575
|
+
// permanently and re-resolves this batch on the main thread.
|
|
1576
|
+
const settleBatch = async (inFlight, batch) => {
|
|
1577
|
+
if (inFlight.mode === 'pool') {
|
|
1578
|
+
const settled = await inFlight.settled;
|
|
1579
|
+
if (settled.ok) {
|
|
1580
|
+
this.appendDeferredFromWorkers(settled.out.deferredChain, settled.out.deferredThisMember);
|
|
1581
|
+
return {
|
|
1582
|
+
resolved: settled.out.resolved,
|
|
1583
|
+
unresolved: settled.out.unresolved,
|
|
1584
|
+
stats: {
|
|
1585
|
+
total: batch.length,
|
|
1586
|
+
resolved: settled.out.resolved.length,
|
|
1587
|
+
unresolved: settled.out.unresolved.length,
|
|
1588
|
+
byMethod: settled.out.byMethod,
|
|
1589
|
+
},
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
(0, errors_1.logDebug)('Parallel resolution failed; falling back to sequential', {
|
|
1593
|
+
error: settled.err instanceof Error ? settled.err.message : String(settled.err),
|
|
1594
|
+
});
|
|
1595
|
+
if (pool)
|
|
1596
|
+
await pool.destroy().catch(() => undefined);
|
|
1597
|
+
pool = null;
|
|
1204
1598
|
}
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1599
|
+
return this.resolveBatchYielding(batch, maybeYield);
|
|
1600
|
+
};
|
|
1601
|
+
// Bulk edge load: on big runs, drop the non-unique edge indexes for the
|
|
1602
|
+
// duration of the batch loop (the identity index stays — OR IGNORE dedup
|
|
1603
|
+
// and the source-keyed supertype-walk reads both live on it). Recreated in
|
|
1604
|
+
// the inner finally BEFORE synthesis, whose passes read kind-keyed.
|
|
1605
|
+
// Measured on a 224k-edge resolution set: insert 2.8s → 1.1s + 0.3s
|
|
1606
|
+
// recreate. Same ref-count gate as the pool so small syncs never pay the
|
|
1607
|
+
// recreate cost.
|
|
1608
|
+
let bulkEdgesActive = false;
|
|
1609
|
+
if (parallel?.bulkEdgeLoad && total >= (0, resolver_pool_1.minRefsForPool)()) {
|
|
1610
|
+
try {
|
|
1611
|
+
parallel.bulkEdgeLoad.begin();
|
|
1612
|
+
bulkEdgesActive = true;
|
|
1217
1613
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1614
|
+
catch { /* keep the indexes; inserts just pay the per-row maintenance */ }
|
|
1615
|
+
}
|
|
1616
|
+
// Same gate for the ref-index window: the loop's deletes stop maintaining
|
|
1617
|
+
// the five sync-path unresolved_refs indexes, and the end-of-loop rebuild
|
|
1618
|
+
// is near-free (only failed refs survive the loop).
|
|
1619
|
+
let bulkRefsActive = false;
|
|
1620
|
+
if (parallel?.refIndexLoad && total >= (0, resolver_pool_1.minRefsForPool)()) {
|
|
1621
|
+
try {
|
|
1622
|
+
parallel.refIndexLoad.begin();
|
|
1623
|
+
bulkRefsActive = true;
|
|
1224
1624
|
}
|
|
1225
|
-
|
|
1226
|
-
onProgress?.(processed, total);
|
|
1227
|
-
// Yield so progress UI can render between batches
|
|
1228
|
-
await new Promise(resolve => setImmediate(resolve));
|
|
1229
|
-
// NOTE: there used to be an extra early break here when a batch resolved
|
|
1230
|
-
// nothing (`result.unresolved.length === batch.length`). That was wrong:
|
|
1231
|
-
// an all-unresolvable batch still DELETES its rows (progress), yet the
|
|
1232
|
-
// break abandoned every batch after it in the same run — on a repo whose
|
|
1233
|
-
// first 5000 refs are all external/stdlib calls, resolution stopped at
|
|
1234
|
-
// batch one and left the rest of the table as permanent orphans (#1187).
|
|
1235
|
-
// The count-based guard below catches the true no-progress case.
|
|
1236
|
-
// Non-progress guard (defense-in-depth). Because we re-read from offset 0
|
|
1237
|
-
// each pass, the PENDING population MUST shrink every iteration — resolved
|
|
1238
|
-
// refs are deleted and unresolvable ones are marked failed above, and both
|
|
1239
|
-
// leave the pending set the batch reader sees. If it didn't shrink, a
|
|
1240
|
-
// resolver returned a match whose `original.referenceName` differs from the
|
|
1241
|
-
// stored row, so the keyed delete/update no-ops, and we'd re-read +
|
|
1242
|
-
// re-resolve + re-insert the same rows forever (the runaway that grew a
|
|
1243
|
-
// 99-file repo to 5M edges / 1.4 GB before the Go-fallback fix). Stop
|
|
1244
|
-
// rather than grow the graph without bound.
|
|
1245
|
-
const remaining = this.queries.getUnresolvedReferencesCount();
|
|
1246
|
-
if (remaining >= prevRemaining)
|
|
1247
|
-
break;
|
|
1248
|
-
prevRemaining = remaining;
|
|
1625
|
+
catch { /* keep the indexes; deletes just pay the per-row maintenance */ }
|
|
1249
1626
|
}
|
|
1250
|
-
// Dynamic-edge synthesis: now that all base `calls` edges are persisted,
|
|
1251
|
-
// synthesize observer/callback dispatch edges (dispatcher → registered
|
|
1252
|
-
// callbacks) that static parsing leaves out. Best-effort — never fail the
|
|
1253
|
-
// index on it. See docs/design/callback-edge-synthesis.md.
|
|
1254
1627
|
try {
|
|
1255
|
-
|
|
1628
|
+
try {
|
|
1629
|
+
tLp = Date.now();
|
|
1630
|
+
let batch = this.queries.getUnresolvedReferencesBatchAfter(0, batchSize);
|
|
1631
|
+
lp('read', tLp);
|
|
1632
|
+
let inFlight = batch.length > 0 ? beginBatch(batch) : null;
|
|
1633
|
+
while (batch.length > 0 && inFlight) {
|
|
1634
|
+
// Prefetch the NEXT batch before this one persists: this batch's rows
|
|
1635
|
+
// are still pending (nothing has mutated the table since they were
|
|
1636
|
+
// read), so seeking past this batch's last row id in the same rowid
|
|
1637
|
+
// enumeration yields the following batch (keyset — OFFSET re-walked the
|
|
1638
|
+
// accumulated failed prefix every read, 54.6s at kernel scale, §7a.2).
|
|
1639
|
+
tLp = Date.now();
|
|
1640
|
+
const nextBatch = this.queries.getUnresolvedReferencesBatchAfter(batch[batch.length - 1].rowId, batchSize);
|
|
1641
|
+
lp('read', tLp);
|
|
1642
|
+
const tBatch = Date.now();
|
|
1643
|
+
const result = await settleBatch(inFlight, batch);
|
|
1644
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
|
|
1645
|
+
console.error(`[pool-timing] batch ${inFlight.mode}: ${batch.length} refs in ${Date.now() - tBatch}ms`);
|
|
1646
|
+
lp('settle', tBatch);
|
|
1647
|
+
// Adaptive pool engagement: the fixed ref-count gate can't see PER-REF
|
|
1648
|
+
// cost, and settle rates differ ~9× by language (56k Rust refs cost
|
|
1649
|
+
// more sequential settle than 154k Go refs — 36µs vs 4µs measured on
|
|
1650
|
+
// tokio/prometheus). After each sequential batch, project the remaining
|
|
1651
|
+
// settle from the observed rate and boot the pool mid-loop when it
|
|
1652
|
+
// clears the bar. The loop already switches to fan-out only when the
|
|
1653
|
+
// async boot reports ready, admission order is mode-independent, and
|
|
1654
|
+
// 2-core/low-memory hosts still decline inside tryCreate's sizing —
|
|
1655
|
+
// so the switch changes wall-clock, never the graph.
|
|
1656
|
+
if (inFlight.mode === 'seq' && parallel && pool === null && !poolEngageTried) {
|
|
1657
|
+
adaptiveSeqMs += Date.now() - tBatch;
|
|
1658
|
+
adaptiveSeqRefs += batch.length;
|
|
1659
|
+
const remaining = total - processed - batch.length;
|
|
1660
|
+
const projectedMs = (adaptiveSeqMs / Math.max(1, adaptiveSeqRefs)) * Math.max(0, remaining);
|
|
1661
|
+
if (projectedMs >= ADAPTIVE_ENGAGE_SETTLE_MS) {
|
|
1662
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS) {
|
|
1663
|
+
console.error(`[pool-timing] adaptive engage: projected ${Math.round(projectedMs)}ms sequential settle over ${remaining} remaining refs`);
|
|
1664
|
+
}
|
|
1665
|
+
pool = createPool(Date.now(), 'adaptive');
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
// WAL-valve backstop at the ONE pool-idle boundary of the double-buffer
|
|
1669
|
+
// (this batch settled, the next not yet fanned out): past the hard cap
|
|
1670
|
+
// the writer parks for a full backfill here, where the pool's readers
|
|
1671
|
+
// are all between statements — so the backfill completes, readers
|
|
1672
|
+
// re-enter at SQLite's backfilled mark, and the next persist commit
|
|
1673
|
+
// WRAPS the WAL instead of growing it. No-op (one fstat) under the cap.
|
|
1674
|
+
tLp = Date.now();
|
|
1675
|
+
const bp = parallel?.backpressure?.();
|
|
1676
|
+
if (bp)
|
|
1677
|
+
await bp;
|
|
1678
|
+
lp('backpressure', tLp);
|
|
1679
|
+
// Recycle the workers' read connections periodically at this same
|
|
1680
|
+
// worker-idle boundary (batch k settled, batch k+1 not yet fanned
|
|
1681
|
+
// out): a long-lived reader pins WAL checkpoint progress, and the
|
|
1682
|
+
// deep WAL that accumulates behind it taxes the writer's OWN page
|
|
1683
|
+
// operations — the §7a.6 writes-under-readers finding (deletes
|
|
1684
|
+
// 42.6s → 118.8s from 0 to 4 attached readers; an aggressive valve
|
|
1685
|
+
// recovered the writes but paid +129s in full-park folds). Releasing
|
|
1686
|
+
// the read marks every ~25 batches lets the existing checkpoints
|
|
1687
|
+
// advance instead, at ~milliseconds of reopen cost. A failed recycle
|
|
1688
|
+
// downgrades to sequential permanently, same as a failed fan-out.
|
|
1689
|
+
if (pool && poolReady && ++batchesSinceRecycle >= RECYCLE_EVERY_BATCHES) {
|
|
1690
|
+
batchesSinceRecycle = 0;
|
|
1691
|
+
tLp = Date.now();
|
|
1692
|
+
try {
|
|
1693
|
+
await pool.recycleWorkers();
|
|
1694
|
+
}
|
|
1695
|
+
catch (err) {
|
|
1696
|
+
(0, errors_1.logDebug)('Worker connection recycle failed; falling back to sequential', {
|
|
1697
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1698
|
+
});
|
|
1699
|
+
await pool.destroy().catch(() => undefined);
|
|
1700
|
+
pool = null;
|
|
1701
|
+
}
|
|
1702
|
+
lp('recycle', tLp);
|
|
1703
|
+
}
|
|
1704
|
+
// Persist in bounded sub-transactions with yields between: a whole
|
|
1705
|
+
// batch's edge insert / keyed deletes are otherwise one solid
|
|
1706
|
+
// synchronous span each on a multi-GB index, sitting BETWEEN the
|
|
1707
|
+
// per-ref yields — the last unyielded stretch of the resolution loop.
|
|
1708
|
+
// Crash semantics are unchanged (already several transactions): edges
|
|
1709
|
+
// land before their refs are deleted, so a kill mid-way re-resolves
|
|
1710
|
+
// the remainder idempotently on the next run/sweep (#1187).
|
|
1711
|
+
const PERSIST_CHUNK = 1000;
|
|
1712
|
+
const tPersist = Date.now();
|
|
1713
|
+
// Persist edges BEFORE fanning out the next batch: later batches read
|
|
1714
|
+
// this batch's edges — resolveMethodOnType walks supertype chains over
|
|
1715
|
+
// `extends`/`implements` edges that earlier batches resolved, so a
|
|
1716
|
+
// receiver typed as a subclass only reaches a method declared on its
|
|
1717
|
+
// base class if those edges are visible. (Validated on dubbo: fanning
|
|
1718
|
+
// out first downgraded exactly those supertype-method resolutions from
|
|
1719
|
+
// the 0.9 typed-receiver path to the 0.65 word-overlap fallback.)
|
|
1720
|
+
tLp = Date.now();
|
|
1721
|
+
const edges = this.createEdges(result.resolved);
|
|
1722
|
+
lp('createEdges', tLp);
|
|
1723
|
+
tLp = Date.now();
|
|
1724
|
+
for (let i = 0; i < edges.length; i += PERSIST_CHUNK) {
|
|
1725
|
+
this.queries.insertEdges(edges.slice(i, i + PERSIST_CHUNK));
|
|
1726
|
+
await maybeYield();
|
|
1727
|
+
}
|
|
1728
|
+
lp('insertEdges', tLp);
|
|
1729
|
+
// NOW fan the next batch out — workers see exactly the edge state the
|
|
1730
|
+
// sequential baseline would (every batch ≤ this one committed), while
|
|
1731
|
+
// the main thread spends the REST of the persist (ref deletes + failed
|
|
1732
|
+
// parking below) overlapped with their resolution — the double-buffer.
|
|
1733
|
+
const nextInFlight = nextBatch.length > 0 ? beginBatch(nextBatch) : null;
|
|
1734
|
+
// Clean up resolved refs so they don't appear in the next batch —
|
|
1735
|
+
// by row id, so a same-key sibling ref in a LATER batch (same caller
|
|
1736
|
+
// calling the same callee at another line) is left pending for its own
|
|
1737
|
+
// attempt instead of being swept out with this batch's rows (#1269).
|
|
1738
|
+
tLp = Date.now();
|
|
1739
|
+
let removedThisBatch = 0;
|
|
1740
|
+
const resolvedCleanup = ReferenceResolver.partitionResolvedCleanup(result.resolved);
|
|
1741
|
+
for (let i = 0; i < resolvedCleanup.rowIds.length; i += PERSIST_CHUNK) {
|
|
1742
|
+
removedThisBatch += this.queries.deleteReferencesByRowIds(resolvedCleanup.rowIds.slice(i, i + PERSIST_CHUNK));
|
|
1743
|
+
await maybeYield();
|
|
1744
|
+
}
|
|
1745
|
+
for (let i = 0; i < resolvedCleanup.legacyKeys.length; i += PERSIST_CHUNK) {
|
|
1746
|
+
removedThisBatch += this.queries.deleteSpecificResolvedReferences(resolvedCleanup.legacyKeys.slice(i, i + PERSIST_CHUNK));
|
|
1747
|
+
await maybeYield();
|
|
1748
|
+
}
|
|
1749
|
+
lp('deletes', tLp);
|
|
1750
|
+
// Park unresolvable refs from this batch as status='failed' so they
|
|
1751
|
+
// leave the pending set (the batch reader and non-progress guard below
|
|
1752
|
+
// only see pending rows) but stay retryable when a later sync adds a
|
|
1753
|
+
// symbol that could satisfy them (#1240).
|
|
1754
|
+
tLp = Date.now();
|
|
1755
|
+
const failedCleanup = ReferenceResolver.partitionFailedCleanup(result.unresolved);
|
|
1756
|
+
for (let i = 0; i < failedCleanup.byRowId.length; i += PERSIST_CHUNK) {
|
|
1757
|
+
removedThisBatch += this.queries.markReferencesFailedByRowIds(failedCleanup.byRowId.slice(i, i + PERSIST_CHUNK));
|
|
1758
|
+
await maybeYield();
|
|
1759
|
+
}
|
|
1760
|
+
for (let i = 0; i < failedCleanup.legacyKeys.length; i += PERSIST_CHUNK) {
|
|
1761
|
+
removedThisBatch += this.queries.markReferencesFailed(failedCleanup.legacyKeys.slice(i, i + PERSIST_CHUNK));
|
|
1762
|
+
await maybeYield();
|
|
1763
|
+
}
|
|
1764
|
+
lp('marks', tLp);
|
|
1765
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
|
|
1766
|
+
console.error(`[pool-timing] batch persist: ${Date.now() - tPersist}ms`);
|
|
1767
|
+
// Aggregate stats
|
|
1768
|
+
aggregateStats.total += result.stats.total;
|
|
1769
|
+
aggregateStats.resolved += result.stats.resolved;
|
|
1770
|
+
aggregateStats.unresolved += result.stats.unresolved;
|
|
1771
|
+
for (const [method, count] of Object.entries(result.stats.byMethod)) {
|
|
1772
|
+
aggregateStats.byMethod[method] = (aggregateStats.byMethod[method] || 0) + count;
|
|
1773
|
+
}
|
|
1774
|
+
processed += batch.length;
|
|
1775
|
+
onProgress?.(processed, total);
|
|
1776
|
+
// Yield so progress UI can render between batches
|
|
1777
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
1778
|
+
// NOTE: there used to be an extra early break here when a batch resolved
|
|
1779
|
+
// nothing (`result.unresolved.length === batch.length`). That was wrong:
|
|
1780
|
+
// an all-unresolvable batch still DELETES its rows (progress), yet the
|
|
1781
|
+
// break abandoned every batch after it in the same run — on a repo whose
|
|
1782
|
+
// first 5000 refs are all external/stdlib calls, resolution stopped at
|
|
1783
|
+
// batch one and left the rest of the table as permanent orphans (#1187).
|
|
1784
|
+
// The count-based guard below catches the true no-progress case.
|
|
1785
|
+
// Non-progress guard (defense-in-depth). Each iteration enumerates from
|
|
1786
|
+
// the head of the pending set, so the PENDING population MUST shrink
|
|
1787
|
+
// every iteration — resolved refs are deleted and unresolvable ones are
|
|
1788
|
+
// marked failed above, and both leave the pending set the batch reader
|
|
1789
|
+
// sees. If it didn't shrink, a resolver returned a match whose
|
|
1790
|
+
// `original.referenceName` differs from the stored row, so the keyed
|
|
1791
|
+
// delete/update no-ops, and we'd re-read + re-resolve + re-insert the
|
|
1792
|
+
// same rows forever (the runaway that grew a 99-file repo to 5M edges /
|
|
1793
|
+
// 1.4 GB before the Go-fallback fix). Stop rather than grow the graph
|
|
1794
|
+
// without bound. (An in-flight prefetched batch is abandoned unsettled —
|
|
1795
|
+
// fan-out has no side effects until settleBatch appends its results.)
|
|
1796
|
+
// Non-progress signal, now O(1): `changes` summed across this batch's
|
|
1797
|
+
// deletes + failed-parks is the DIRECT evidence the guard's old count
|
|
1798
|
+
// diff inferred — a resolver returning a mismatched name makes the keyed
|
|
1799
|
+
// cleanup no-op, which shows up here as zero removals. The per-batch
|
|
1800
|
+
// COUNT(*) it replaces walked every remaining pending row — O(N²/batch)
|
|
1801
|
+
// over a run, 93.9s of the kernel-scale batch loop (§7a.2). A REAL count
|
|
1802
|
+
// runs only on the suspicious path (claimed-work batch removed nothing —
|
|
1803
|
+
// e.g. every row was a sibling a legacy-key sweep already consumed),
|
|
1804
|
+
// where it arbitrates stop-vs-continue exactly as before.
|
|
1805
|
+
if (removedThisBatch <= 0 && batch.length > 0) {
|
|
1806
|
+
tLp = Date.now();
|
|
1807
|
+
const remaining = this.queries.getUnresolvedReferencesCount();
|
|
1808
|
+
lp('countGuard', tLp);
|
|
1809
|
+
if (remaining >= prevRemaining)
|
|
1810
|
+
break;
|
|
1811
|
+
prevRemaining = remaining;
|
|
1812
|
+
}
|
|
1813
|
+
// Advance the pipeline: the prefetched batch (already fanned out when
|
|
1814
|
+
// the pool is on) becomes the current one.
|
|
1815
|
+
batch = nextBatch;
|
|
1816
|
+
inFlight = nextInFlight;
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
finally {
|
|
1820
|
+
// Recreate the edge indexes BEFORE synthesis (kind-keyed reads) and on
|
|
1821
|
+
// any error path. A crash before this line is healed by the next
|
|
1822
|
+
// DatabaseConnection open (schema.sql re-applies IF NOT EXISTS).
|
|
1823
|
+
if (bulkRefsActive) {
|
|
1824
|
+
const tRef = Date.now();
|
|
1825
|
+
await parallel.refIndexLoad.end();
|
|
1826
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
|
|
1827
|
+
console.error(`[phase-timing] ref-index-recreate: ${Date.now() - tRef}ms`);
|
|
1828
|
+
}
|
|
1829
|
+
if (bulkEdgesActive) {
|
|
1830
|
+
const tIdx = Date.now();
|
|
1831
|
+
await parallel.bulkEdgeLoad.end();
|
|
1832
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
|
|
1833
|
+
console.error(`[phase-timing] edge-index-recreate: ${Date.now() - tIdx}ms`);
|
|
1834
|
+
// The recreate just wrote every non-unique edge index into the WAL
|
|
1835
|
+
// (multi-GB at kernel scale) with the pool idle — fold before the
|
|
1836
|
+
// synthesis passes pin readers against it for minutes.
|
|
1837
|
+
const bp = parallel?.backpressure?.();
|
|
1838
|
+
if (bp)
|
|
1839
|
+
await bp;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
// Dynamic-edge synthesis: now that all base `calls` edges are persisted,
|
|
1843
|
+
// synthesize observer/callback dispatch edges (dispatcher → registered
|
|
1844
|
+
// callbacks) that static parsing leaves out. Best-effort — never fail the
|
|
1845
|
+
// index on it. The pool (when it survived resolution) is REUSED to fan the
|
|
1846
|
+
// independent passes across its read-only workers — that's why its destroy
|
|
1847
|
+
// lives in the finally below, after synthesis, not at the end of the batch
|
|
1848
|
+
// loop. See docs/design/callback-edge-synthesis.md.
|
|
1849
|
+
const tSynth = Date.now();
|
|
1850
|
+
try {
|
|
1851
|
+
aggregateStats.byMethod['callback-synthesis'] = await (0, callback_synthesizer_1.synthesizeCallbackEdges)(this.queries, this.context, onSynthesisProgress, pool, parallel?.backpressure);
|
|
1852
|
+
}
|
|
1853
|
+
catch {
|
|
1854
|
+
// synthesis is additive and optional; ignore failures
|
|
1855
|
+
}
|
|
1856
|
+
if (process.env.HOMEGRAPH_SYNTH_TIMINGS)
|
|
1857
|
+
console.error(`[phase-timing] callback-synthesis: ${Date.now() - tSynth}ms`);
|
|
1858
|
+
}
|
|
1859
|
+
finally {
|
|
1860
|
+
if (pool)
|
|
1861
|
+
await pool.destroy().catch(() => undefined);
|
|
1256
1862
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1863
|
+
if (loopProf) {
|
|
1864
|
+
const parts = Object.entries(loopProf).map(([k, v]) => `${k}=${(v / 1000).toFixed(1)}s`).join(' ');
|
|
1865
|
+
console.error(`[resolve-profile] loop-stages ${parts}`);
|
|
1259
1866
|
}
|
|
1867
|
+
this.dumpResolveProfile('main');
|
|
1260
1868
|
return {
|
|
1261
1869
|
resolved: [],
|
|
1262
1870
|
unresolved: [],
|