mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +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';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
|
|
@ -41,21 +90,31 @@ export function resolveCImportTarget(
|
|||
// Exact match (path as-is in the workspace)
|
||||
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 targetBasename = normalizedTarget.slice(normalizedTarget.lastIndexOf('/') + 1);
|
||||
const bucket = suffixIndex(allFilePaths).get(targetBasename);
|
||||
if (bucket === undefined) return null;
|
||||
|
||||
let bestMatch: string | null = null;
|
||||
let bestDepth = Infinity;
|
||||
let bestNormalized = '';
|
||||
|
||||
for (const filePath of allFilePaths) {
|
||||
const normalized = filePath.replace(/\\/g, '/');
|
||||
if (normalized === normalizedTarget || normalized.endsWith(suffix)) {
|
||||
for (const cand of bucket) {
|
||||
if (cand.normalized === normalizedTarget || cand.normalized.endsWith(suffix)) {
|
||||
// Prefer shortest path (closest match)
|
||||
const depth = normalized.split('/').length;
|
||||
if (depth < bestDepth || (depth === bestDepth && normalized < bestNormalized)) {
|
||||
bestDepth = depth;
|
||||
bestMatch = filePath;
|
||||
bestNormalized = normalized;
|
||||
if (
|
||||
cand.depth < bestDepth ||
|
||||
(cand.depth === bestDepth && cand.normalized < bestNormalized)
|
||||
) {
|
||||
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 { 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
|
||||
* 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.
|
||||
const headerPaths = resolutionConfig as ReadonlySet<string> | undefined;
|
||||
if (headerPaths !== undefined && headerPaths.size > 0) {
|
||||
const augmented = new Set(allFilePaths);
|
||||
for (const h of headerPaths) augmented.add(h);
|
||||
return resolveCImportTarget(targetRaw, fromFile, augmented);
|
||||
return resolveCImportTarget(
|
||||
targetRaw,
|
||||
fromFile,
|
||||
augmentedFilePaths(allFilePaths, headerPaths),
|
||||
);
|
||||
}
|
||||
return resolveCImportTarget(targetRaw, fromFile, allFilePaths);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -46,6 +46,41 @@ export function clearStaticNames(): void {
|
|||
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`).
|
||||
* All module-scope defs from the target file are visible EXCEPT those
|
||||
|
|
@ -55,7 +90,7 @@ export function expandCWildcardNames(
|
|||
targetModuleScope: ScopeId,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
): readonly string[] {
|
||||
const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope);
|
||||
const target = moduleScopeIndex(parsedFiles).get(targetModuleScope);
|
||||
if (target === undefined) return [];
|
||||
|
||||
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
|
||||
* 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(
|
||||
targetModuleScope: ScopeId,
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
): readonly string[] {
|
||||
const target = parsedFiles.find((p) => p.moduleScope === targetModuleScope);
|
||||
const target = moduleScopeIndex(parsedFiles).get(targetModuleScope);
|
||||
if (target === undefined) return [];
|
||||
|
||||
// Build nodeId → owning Scope map from the structural scope tree.
|
||||
|
|
|
|||
|
|
@ -43,6 +43,40 @@ import {
|
|||
populateCppUserDefinedConversions,
|
||||
} 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
|
||||
* 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.
|
||||
const headerPaths = resolutionConfig as ReadonlySet<string> | undefined;
|
||||
if (headerPaths !== undefined && headerPaths.size > 0) {
|
||||
const augmented = new Set(allFilePaths);
|
||||
for (const h of headerPaths) augmented.add(h);
|
||||
return resolveCppImportTarget(targetRaw, fromFile, augmented);
|
||||
return resolveCppImportTarget(
|
||||
targetRaw,
|
||||
fromFile,
|
||||
augmentedFilePaths(allFilePaths, headerPaths),
|
||||
);
|
||||
}
|
||||
return resolveCppImportTarget(targetRaw, fromFile, allFilePaths);
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue