mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
perf(scope-resolution): index C/C++ #include resolution in finalize (O(n²)→O(n))
Kernel-scale C/C++ analysis ground in finalizeScopeModel because three per-#include operations each did a full O(F) scan with no index — the finalize O(n²) that surfaced once the #1983 parse-phase OOM was fixed: - expand{C,Cpp}WildcardNames: parsedFiles.find() per wildcard edge → O(R·F) - resolveImportTarget: new Set(allFilePaths) rebuilt per #include - resolveCImportTarget: suffix-match scanned all workspace paths Each is replaced with a WeakMap-per-pass index keyed on the stable parsedFiles/allFilePaths references that scope-resolution run.ts passes once per pass: - Map<ScopeId,ParsedFile> for wildcard expansion (c/static-linkage.ts + cpp/file-local-linkage.ts) - memoized augmented header set (c/scope-resolver.ts + cpp/scope-resolver.ts) - basename-bucketed suffix index in resolveCImportTarget (c/import-target.ts), shared by C and C++ since resolveCppImportTarget delegates to it Collapses the C/C++ finalize from O(R·F) to O(R+F). Pure-perf, byte-identical edge output: 962 targeted tests green (490 C + 472 C/C++ scope-resolution); the basename index preserves the exact endsWith('/'+target) match and the fewest-path-components-then-lexicographic tie-break. The kernel's ~25-30k .h headers are classified C++, so both providers must be fixed. Proven on the Linux kernel: the C finalize completed (sr-post-finalize lang=c → sr-end lang=c), which the pre-fix run never reached in 16+ min of grinding. Build-independent follow-ups (separate from this finalize fix), documented for later: emitFreeCallFallback same-name buckets (emit phase), buildGraphNodeLookup + precount global setup, the ParsedFile store-load, the dart/go/ruby expand-wildcards .find siblings, and the ~26GB scope-resolution memory floor (full kernel completion needs >~40GB RAM). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
716cd8aa9b
commit
b71c77b84a
5 changed files with 211 additions and 17 deletions
|
|
@ -1,5 +1,54 @@
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A workspace file path pre-decomposed for the suffix-match fallback:
|
||||||
|
* `original` is returned verbatim (preserving the prior `bestMatch = filePath`
|
||||||
|
* contract); `normalized` and `depth` are precomputed so the hot path does no
|
||||||
|
* per-element regex/`split`.
|
||||||
|
*/
|
||||||
|
interface CSuffixCandidate {
|
||||||
|
original: string;
|
||||||
|
normalized: string;
|
||||||
|
depth: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-pass memo: workspace paths bucketed by basename (last path segment),
|
||||||
|
* keyed on the `allFilePaths` set identity.
|
||||||
|
*
|
||||||
|
* `resolveCImportTarget` is called once per (quoted) C/C++ `#include` with the
|
||||||
|
* same `allFilePaths` set per pass (the augmented set is itself memoized in
|
||||||
|
* the C resolver). The old suffix-match fallback scanned ALL workspace paths
|
||||||
|
* per include — with a per-element `.replace`/`.split` and no early exit
|
||||||
|
* (the fewest-path-components tie-break forces a full scan) — i.e.
|
||||||
|
* O(R_suffix × (F+H)). A path can satisfy `endsWith('/'+target)` (or equal
|
||||||
|
* the target) ONLY IF its basename equals the target's last segment, so we
|
||||||
|
* pre-bucket by basename once (O(F+H), `normalized`/`depth` precomputed) and
|
||||||
|
* the fallback inspects a single small bucket → O(F+H) build + ~O(1)/include.
|
||||||
|
* `WeakMap`-keyed so it is reclaimed with the pass (no cross-pass staleness).
|
||||||
|
* Shared by C and C++ (`resolveCppImportTarget` delegates here).
|
||||||
|
*/
|
||||||
|
const suffixIndexByPaths = new WeakMap<ReadonlySet<string>, Map<string, CSuffixCandidate[]>>();
|
||||||
|
|
||||||
|
function suffixIndex(allFilePaths: ReadonlySet<string>): Map<string, CSuffixCandidate[]> {
|
||||||
|
let index = suffixIndexByPaths.get(allFilePaths);
|
||||||
|
if (index === undefined) {
|
||||||
|
index = new Map<string, CSuffixCandidate[]>();
|
||||||
|
for (const original of allFilePaths) {
|
||||||
|
const normalized = original.replace(/\\/g, '/');
|
||||||
|
const basename = normalized.slice(normalized.lastIndexOf('/') + 1);
|
||||||
|
let bucket = index.get(basename);
|
||||||
|
if (bucket === undefined) {
|
||||||
|
bucket = [];
|
||||||
|
index.set(basename, bucket);
|
||||||
|
}
|
||||||
|
bucket.push({ original, normalized, depth: normalized.split('/').length });
|
||||||
|
}
|
||||||
|
suffixIndexByPaths.set(allFilePaths, index);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolve a C #include path to a file in the workspace.
|
* Resolve a C #include path to a file in the workspace.
|
||||||
*
|
*
|
||||||
|
|
@ -41,21 +90,31 @@ export function resolveCImportTarget(
|
||||||
// Exact match (path as-is in the workspace)
|
// Exact match (path as-is in the workspace)
|
||||||
if (allFilePaths.has(normalizedTarget)) return normalizedTarget;
|
if (allFilePaths.has(normalizedTarget)) return normalizedTarget;
|
||||||
|
|
||||||
// Suffix match: find files ending with /targetRaw or equal to targetRaw
|
// Suffix match: find files ending with /targetRaw or equal to targetRaw.
|
||||||
|
// A path can only match `=== normalizedTarget` or `endsWith('/'+target)` if
|
||||||
|
// its basename equals the target's last segment, so we inspect only that
|
||||||
|
// basename bucket (built once per pass) instead of scanning every workspace
|
||||||
|
// path. Match condition + tie-break (fewest path components, then
|
||||||
|
// lexicographic on the normalized path) are byte-identical to the prior scan.
|
||||||
const suffix = '/' + normalizedTarget;
|
const suffix = '/' + normalizedTarget;
|
||||||
|
const targetBasename = normalizedTarget.slice(normalizedTarget.lastIndexOf('/') + 1);
|
||||||
|
const bucket = suffixIndex(allFilePaths).get(targetBasename);
|
||||||
|
if (bucket === undefined) return null;
|
||||||
|
|
||||||
let bestMatch: string | null = null;
|
let bestMatch: string | null = null;
|
||||||
let bestDepth = Infinity;
|
let bestDepth = Infinity;
|
||||||
let bestNormalized = '';
|
let bestNormalized = '';
|
||||||
|
|
||||||
for (const filePath of allFilePaths) {
|
for (const cand of bucket) {
|
||||||
const normalized = filePath.replace(/\\/g, '/');
|
if (cand.normalized === normalizedTarget || cand.normalized.endsWith(suffix)) {
|
||||||
if (normalized === normalizedTarget || normalized.endsWith(suffix)) {
|
|
||||||
// Prefer shortest path (closest match)
|
// Prefer shortest path (closest match)
|
||||||
const depth = normalized.split('/').length;
|
if (
|
||||||
if (depth < bestDepth || (depth === bestDepth && normalized < bestNormalized)) {
|
cand.depth < bestDepth ||
|
||||||
bestDepth = depth;
|
(cand.depth === bestDepth && cand.normalized < bestNormalized)
|
||||||
bestMatch = filePath;
|
) {
|
||||||
bestNormalized = normalized;
|
bestDepth = cand.depth;
|
||||||
|
bestMatch = cand.original;
|
||||||
|
bestNormalized = cand.normalized;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,42 @@ import { scanHeaderFiles } from './header-scan.js';
|
||||||
import { expandCWildcardNames, isStaticName, clearStaticNames } from './static-linkage.js';
|
import { expandCWildcardNames, isStaticName, clearStaticNames } from './static-linkage.js';
|
||||||
import { applyCStaticLinkageSideChannel } from './capture-side-channel.js';
|
import { applyCStaticLinkageSideChannel } from './capture-side-channel.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-pass memo of the augmented `#include`-resolution file set
|
||||||
|
* (`allFilePaths` ∪ header `.h` paths), keyed on the two stable source sets.
|
||||||
|
*
|
||||||
|
* `resolveImportTarget` is called once per C `#include`; the old code rebuilt
|
||||||
|
* a fresh ~F-entry `Set` on EVERY call (O(R × (F+H)) inserts + GC churn) and,
|
||||||
|
* worse, defeated `resolveCImportTarget`'s own per-set suffix-index memo by
|
||||||
|
* handing it a new set identity each time. Both `allFilePaths` (built once in
|
||||||
|
* scope-resolution `run.ts`) and the header set (`loadResolutionConfig`
|
||||||
|
* result) are stable per pass, so the union is built once and reused.
|
||||||
|
* `WeakMap`-keyed → reclaimed with the pass (no cross-pass staleness).
|
||||||
|
*/
|
||||||
|
const augmentedPathsByPass = new WeakMap<
|
||||||
|
ReadonlySet<string>,
|
||||||
|
WeakMap<ReadonlySet<string>, ReadonlySet<string>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
function augmentedFilePaths(
|
||||||
|
allFilePaths: ReadonlySet<string>,
|
||||||
|
headerPaths: ReadonlySet<string>,
|
||||||
|
): ReadonlySet<string> {
|
||||||
|
let byHeaders = augmentedPathsByPass.get(allFilePaths);
|
||||||
|
if (byHeaders === undefined) {
|
||||||
|
byHeaders = new WeakMap();
|
||||||
|
augmentedPathsByPass.set(allFilePaths, byHeaders);
|
||||||
|
}
|
||||||
|
let augmented = byHeaders.get(headerPaths);
|
||||||
|
if (augmented === undefined) {
|
||||||
|
const set = new Set(allFilePaths);
|
||||||
|
for (const h of headerPaths) set.add(h);
|
||||||
|
augmented = set;
|
||||||
|
byHeaders.set(headerPaths, augmented);
|
||||||
|
}
|
||||||
|
return augmented;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* C `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
* C `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
||||||
* the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
|
* the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
|
||||||
|
|
@ -55,9 +91,11 @@ export const cScopeResolver: ScopeResolver = {
|
||||||
// targets .h files classified as C++ in language detection.
|
// targets .h files classified as C++ in language detection.
|
||||||
const headerPaths = resolutionConfig as ReadonlySet<string> | undefined;
|
const headerPaths = resolutionConfig as ReadonlySet<string> | undefined;
|
||||||
if (headerPaths !== undefined && headerPaths.size > 0) {
|
if (headerPaths !== undefined && headerPaths.size > 0) {
|
||||||
const augmented = new Set(allFilePaths);
|
return resolveCImportTarget(
|
||||||
for (const h of headerPaths) augmented.add(h);
|
targetRaw,
|
||||||
return resolveCImportTarget(targetRaw, fromFile, augmented);
|
fromFile,
|
||||||
|
augmentedFilePaths(allFilePaths, headerPaths),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return resolveCImportTarget(targetRaw, fromFile, allFilePaths);
|
return resolveCImportTarget(targetRaw, fromFile, allFilePaths);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,41 @@ export function clearStaticNames(): void {
|
||||||
staticNames.clear();
|
staticNames.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-pass memo: `moduleScope` → owning `ParsedFile`, keyed on the
|
||||||
|
* `parsedFiles` array identity.
|
||||||
|
*
|
||||||
|
* The shared finalize Phase-4 loop calls `expandsWildcardTo`
|
||||||
|
* (→ `expandCWildcardNames`) ONCE PER RESOLVED `#include` edge, every time
|
||||||
|
* with the SAME `parsedFiles` reference (wired at scope-resolution
|
||||||
|
* `run.ts` — `allFilePaths`/`parsedFiles` are built once per pass). The old
|
||||||
|
* `parsedFiles.find(...)` therefore did a full O(F) scan per edge →
|
||||||
|
* O(R_include × F) overall; at Linux-kernel scale (F ≈ 63k C files, tens of
|
||||||
|
* thousands of resolved includes) that is ~10^10+ comparisons on a single
|
||||||
|
* thread — the dominant term in the scope-resolution finalize grind.
|
||||||
|
*
|
||||||
|
* Building the lookup once collapses it to O(R_include + F). `WeakMap`-keyed
|
||||||
|
* on the array so the index is reclaimed with the pass — no cross-pass
|
||||||
|
* staleness (mirrors the {@link clearStaticNames} discipline for server-mode
|
||||||
|
* / multi-repo reuse), and a fresh array transparently rebuilds.
|
||||||
|
*/
|
||||||
|
const moduleScopeIndexByPass = new WeakMap<readonly ParsedFile[], Map<ScopeId, ParsedFile>>();
|
||||||
|
|
||||||
|
function moduleScopeIndex(parsedFiles: readonly ParsedFile[]): Map<ScopeId, ParsedFile> {
|
||||||
|
let index = moduleScopeIndexByPass.get(parsedFiles);
|
||||||
|
if (index === undefined) {
|
||||||
|
index = new Map<ScopeId, ParsedFile>();
|
||||||
|
// First-wins to preserve `Array.find` semantics (returns the first match).
|
||||||
|
// `moduleScope` is unique per file in practice, so collisions are absent;
|
||||||
|
// the guard only formalises identical behaviour to the prior `.find`.
|
||||||
|
for (const p of parsedFiles) {
|
||||||
|
if (!index.has(p.moduleScope)) index.set(p.moduleScope, p);
|
||||||
|
}
|
||||||
|
moduleScopeIndexByPass.set(parsedFiles, index);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the names visible through a C wildcard import (`#include`).
|
* Return the names visible through a C wildcard import (`#include`).
|
||||||
* All module-scope defs from the target file are visible EXCEPT those
|
* All module-scope defs from the target file are visible EXCEPT those
|
||||||
|
|
@ -55,7 +90,7 @@ export function expandCWildcardNames(
|
||||||
targetModuleScope: ScopeId,
|
targetModuleScope: ScopeId,
|
||||||
parsedFiles: readonly ParsedFile[],
|
parsedFiles: readonly ParsedFile[],
|
||||||
): readonly string[] {
|
): readonly string[] {
|
||||||
const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope);
|
const target = moduleScopeIndex(parsedFiles).get(targetModuleScope);
|
||||||
if (target === undefined) return [];
|
if (target === undefined) return [];
|
||||||
|
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
|
|
|
||||||
|
|
@ -276,11 +276,37 @@ export function isCppDefGloballyVisible(filePath: string, nodeId: string): boole
|
||||||
* does, mirror this filter or harden registration so class/namespace
|
* does, mirror this filter or harden registration so class/namespace
|
||||||
* members never enter `localDefs` unqualified.
|
* members never enter `localDefs` unqualified.
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Per-pass memo: `moduleScope` → owning `ParsedFile`, keyed on the
|
||||||
|
* `parsedFiles` array identity. The shared finalize Phase-4 loop calls
|
||||||
|
* `expandsWildcardTo` (→ this) ONCE PER RESOLVED `#include` edge with the same
|
||||||
|
* `parsedFiles` reference; the old `parsedFiles.find(...)` was therefore O(F)
|
||||||
|
* per edge → O(R·F) overall (at kernel scale the ~25–30k `.h` headers are
|
||||||
|
* classified C++, so this fires hard — the C twin in `c/static-linkage.ts`).
|
||||||
|
* Building the lookup once collapses it to O(R+F). `WeakMap`-keyed so it is
|
||||||
|
* reclaimed with the pass (no cross-pass staleness; mirrors
|
||||||
|
* {@link clearFileLocalNames}).
|
||||||
|
*/
|
||||||
|
const moduleScopeIndexByPass = new WeakMap<readonly ParsedFile[], Map<ScopeId, ParsedFile>>();
|
||||||
|
|
||||||
|
function moduleScopeIndex(parsedFiles: readonly ParsedFile[]): Map<ScopeId, ParsedFile> {
|
||||||
|
let index = moduleScopeIndexByPass.get(parsedFiles);
|
||||||
|
if (index === undefined) {
|
||||||
|
index = new Map<ScopeId, ParsedFile>();
|
||||||
|
// First-wins to preserve `Array.find` semantics (returns the first match).
|
||||||
|
for (const p of parsedFiles) {
|
||||||
|
if (!index.has(p.moduleScope)) index.set(p.moduleScope, p);
|
||||||
|
}
|
||||||
|
moduleScopeIndexByPass.set(parsedFiles, index);
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
export function expandCppWildcardNames(
|
export function expandCppWildcardNames(
|
||||||
targetModuleScope: ScopeId,
|
targetModuleScope: ScopeId,
|
||||||
parsedFiles: readonly ParsedFile[],
|
parsedFiles: readonly ParsedFile[],
|
||||||
): readonly string[] {
|
): readonly string[] {
|
||||||
const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope);
|
const target = moduleScopeIndex(parsedFiles).get(targetModuleScope);
|
||||||
if (target === undefined) return [];
|
if (target === undefined) return [];
|
||||||
|
|
||||||
// Build nodeId → owning Scope map from the structural scope tree.
|
// Build nodeId → owning Scope map from the structural scope tree.
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,40 @@ import {
|
||||||
populateCppUserDefinedConversions,
|
populateCppUserDefinedConversions,
|
||||||
} from './user-defined-conversions.js';
|
} from './user-defined-conversions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Per-pass memo of the augmented `#include`-resolution file set
|
||||||
|
* (`allFilePaths` ∪ header paths), keyed on the two stable source sets.
|
||||||
|
* `resolveImportTarget` is called once per C++ `#include`; the old code rebuilt
|
||||||
|
* a fresh ~F-entry `Set` on every call AND defeated the shared
|
||||||
|
* `resolveCImportTarget` suffix-index memo (in `c/import-target.ts`) by handing
|
||||||
|
* it a new set identity each time. Both inputs are stable per pass, so the
|
||||||
|
* union is built once and reused. `WeakMap`-keyed → reclaimed with the pass.
|
||||||
|
* (Twin of the C resolver's `augmentedFilePaths`.)
|
||||||
|
*/
|
||||||
|
const augmentedPathsByPass = new WeakMap<
|
||||||
|
ReadonlySet<string>,
|
||||||
|
WeakMap<ReadonlySet<string>, ReadonlySet<string>>
|
||||||
|
>();
|
||||||
|
|
||||||
|
function augmentedFilePaths(
|
||||||
|
allFilePaths: ReadonlySet<string>,
|
||||||
|
headerPaths: ReadonlySet<string>,
|
||||||
|
): ReadonlySet<string> {
|
||||||
|
let byHeaders = augmentedPathsByPass.get(allFilePaths);
|
||||||
|
if (byHeaders === undefined) {
|
||||||
|
byHeaders = new WeakMap();
|
||||||
|
augmentedPathsByPass.set(allFilePaths, byHeaders);
|
||||||
|
}
|
||||||
|
let augmented = byHeaders.get(headerPaths);
|
||||||
|
if (augmented === undefined) {
|
||||||
|
const set = new Set(allFilePaths);
|
||||||
|
for (const h of headerPaths) set.add(h);
|
||||||
|
augmented = set;
|
||||||
|
byHeaders.set(headerPaths, augmented);
|
||||||
|
}
|
||||||
|
return augmented;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
* C++ `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by
|
||||||
* the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
|
* the generic `runScopeResolution` orchestrator (RFC #909 Ring 3).
|
||||||
|
|
@ -79,9 +113,11 @@ export const cppScopeResolver: ScopeResolver = {
|
||||||
// detection but are importable from .cpp files via #include.
|
// detection but are importable from .cpp files via #include.
|
||||||
const headerPaths = resolutionConfig as ReadonlySet<string> | undefined;
|
const headerPaths = resolutionConfig as ReadonlySet<string> | undefined;
|
||||||
if (headerPaths !== undefined && headerPaths.size > 0) {
|
if (headerPaths !== undefined && headerPaths.size > 0) {
|
||||||
const augmented = new Set(allFilePaths);
|
return resolveCppImportTarget(
|
||||||
for (const h of headerPaths) augmented.add(h);
|
targetRaw,
|
||||||
return resolveCppImportTarget(targetRaw, fromFile, augmented);
|
fromFile,
|
||||||
|
augmentedFilePaths(allFilePaths, headerPaths),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return resolveCppImportTarget(targetRaw, fromFile, allFilePaths);
|
return resolveCppImportTarget(targetRaw, fromFile, allFilePaths);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue