perf(synthesis): provably-empty pass gates + prefilters — render/expo/rn/mybatis stop scanning repos they can't match; iface memo (#1389)

Store-arc round 2 (#1388 follow-up). The synthesis pool barrier on dubbo
carried ~1.4s of passes that provably could not emit an edge for the
project: reactRenderEdges fanned out over every class before checking for
a render method (now: one indexed name lookup bounds candidates — not a
language gate, Java Litho-style render+setState still matches);
expo/rn cross-platform pairing streamed every method row without the
languages their edges require (now registry-gated: expo needs swift AND
kotlin file-languages, rn needs a JS-family caller for isBridge);
mybatis built its full java-method index before discovering there were no
mapper-XML methods (now collects the XML side first). ifaceEdges — real
work — stops re-fetching a hub interface's methods once per implementer
and skips supertype-less classes before any per-class lookup.

dubbo warm wall 8.49-8.79 → 8.14-8.24s (n=3/arm, caffeinated); barrier
784→435ms; the full removed pass work lands on low-core envelopes where
synthesis runs sequentially. Dumps byte-identical: dubbo old-vs-new,
pooled-vs-sequential, kernel-vs-wasm (441,270 rows) + excalidraw JSX-live
control (89,903 rows, 46 react-render edges reproduced). Suite 2,689 ×2
with CODEGRAPH_KERNEL_EXPECT=1.

Also ships the diagnostics that located the round (zero cost when off):
CODEGRAPH_RESOLVE_PROFILE=2 attributes per-ref time to resolveOne's
strategies (stage:*) and the name-matcher's sub-matchers (nm:*);
CODEGRAPH_SYNTH_TIMINGS now prints the store worker's decode-vs-SQL
split. Killed by measurement, recorded in the PR: import-failure negative
cache (both-outcome names exist — static imports resolve via
instance-method on jvm-miss), jvm-miss early return (1,939 later-strategy
edges), jsxEdges language gate (Java generics text produces jsx edges),
and §4d buffer→bind on Spring repos (extract() hook forces the decoded
path — kernel=0 bundles measured).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-07-20 21:15:58 -05:00
committed by GitHub
co-authored by Claude Fable 5
parent 27c3c55436
commit 082ea65f3a
5 changed files with 179 additions and 23 deletions
+46 -8
View File
@@ -16,7 +16,7 @@ import {
FrameworkResolver,
ImportMapping,
} from './types';
import { matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesKnownFamily } from './name-matcher';
import { matchReference, matchFunctionRef, matchDottedCallChain, matchScopedCallChain, matchMethodCall, sameLanguageFamily, crossesKnownFamily, dumpNameMatcherProfile } from './name-matcher';
import { resolveViaImport, resolveJvmImport, extractImportMappings, extractReExports, loadCppIncludeDirs, isPhpIncludePathRef, isCobolCopybookRef, isNixPathImportRef, clearImportResolverMemos } from './import-resolver';
import { ResolverPool, minRefsForPool } from './resolver-pool';
import { detectFrameworks } from './frameworks';
@@ -805,12 +805,14 @@ export class ReferenceResolver {
ref.language === 'arkts' && ref.referenceName.startsWith('.')
? ref.referenceName.slice(1)
: ref.referenceName;
if (
!isNixPathImportRef(ref) &&
!this.hasAnyPossibleMatch(existenceName) &&
!this.matchesAnyImport(ref) &&
!this.frameworks.some((f) => f.claimsReference?.(ref.referenceName))
) {
const tPre = this.profileStages ? process.hrtime.bigint() : 0n;
const preFilterPass =
isNixPathImportRef(ref) ||
this.hasAnyPossibleMatch(existenceName) ||
this.matchesAnyImport(ref) ||
this.frameworks.some((f) => f.claimsReference?.(ref.referenceName));
if (this.profileStages) this.stageAdd('preFilter', ref, preFilterPass, tPre);
if (!preFilterPass) {
return null;
}
@@ -838,7 +840,9 @@ export class ReferenceResolver {
// JVM FQN imports skip framework/name-matcher: `import com.example.Bar`
// resolves directly through the qualifiedName index, which is unambiguous
// even when several `Bar` classes exist in different packages.
const tJvm = this.profileStages ? process.hrtime.bigint() : 0n;
const jvmImport = resolveJvmImport(ref, this.context);
if (this.profileStages) this.stageAdd('jvmImport', ref, !!jvmImport, tJvm);
if (jvmImport) return jvmImport;
// Razor/Blazor: a markup or `@code` type ref resolves through the file's
@@ -858,16 +862,25 @@ export class ReferenceResolver {
// JS → native `calls`) — `gateFrameworkLanguage` only drops a type/import
// edge between two KNOWN families (see its doc), never a `calls` bridge or
// a config↔code edge.
const tFw = this.profileStages ? process.hrtime.bigint() : 0n;
let fwEarly: ResolvedRef | null = null;
for (const framework of this.frameworks) {
const result = this.gateFrameworkLanguage(framework.resolve(ref, this.context), ref);
if (result) {
if (result.confidence >= 0.9) return result; // High confidence, return immediately
if (result.confidence >= 0.9) {
fwEarly = result; // High confidence, return immediately (below)
break;
}
candidates.push(result);
}
}
if (this.profileStages) this.stageAdd('frameworks', ref, fwEarly !== null, tFw);
if (fwEarly) return fwEarly;
// Strategy 2: Try import-based resolution
const tImp = this.profileStages ? process.hrtime.bigint() : 0n;
const importResult = this.gateLanguage(resolveViaImport(ref, this.context), ref);
if (this.profileStages) this.stageAdd('viaImport', ref, !!importResult, tImp);
if (importResult) {
if (importResult.confidence >= 0.9) return importResult;
candidates.push(importResult);
@@ -892,7 +905,9 @@ export class ReferenceResolver {
}
// Strategy 3: Try name matching
const tName = this.profileStages ? process.hrtime.bigint() : 0n;
let nameResult = this.gateLanguage(matchReference(ref, this.context), ref);
if (this.profileStages) this.stageAdd('nameMatch', ref, !!nameResult, tName);
// Nix has no ambient cross-file namespace — a callee binds lexically
// (same file) or through explicit import/callPackage wiring (the import
// path above). A cross-file name match is wrong by construction: every
@@ -1278,6 +1293,27 @@ export class ReferenceResolver {
private resolveProfile: Map<string, { n: number; ns: bigint }> | null =
process.env.CODEGRAPH_RESOLVE_PROFILE ? new Map() : null;
/**
* CODEGRAPH_RESOLVE_PROFILE=2 additionally attributes time to the
* STRATEGIES inside resolveOne (`stage:<name>|<refKind>|hit/miss` rows in
* the same histogram) — i.e. WHICH machinery a failing class of refs pays
* for, not just that it fails. =1 keeps the per-outcome rows only.
*/
private profileStages: boolean = process.env.CODEGRAPH_RESOLVE_PROFILE === '2';
private stageAdd(stage: string, ref: UnresolvedRef, hit: boolean, t0: bigint): void {
if (!this.resolveProfile) return;
const dt = process.hrtime.bigint() - t0;
const key = `stage:${stage}|${ref.referenceKind}|${hit ? 'hit' : 'miss'}`;
const slot = this.resolveProfile.get(key);
if (slot) {
slot.n++;
slot.ns += dt;
} else {
this.resolveProfile.set(key, { n: 1, ns: dt });
}
}
private resolveOneTimed(ref: UnresolvedRef): ResolvedRef | null {
if (!this.resolveProfile) return this.resolveOne(ref);
const t0 = process.hrtime.bigint();
@@ -1305,6 +1341,8 @@ export class ReferenceResolver {
`[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`
);
}
// =2 only: this thread's matchReference sub-stage table rides along.
dumpNameMatcherProfile(label);
}
resolveListForAdmission(refs: UnresolvedReference[]): {