mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-09 03:17:54 +00:00
perf(mro): replace O(n³) C3 merge loop with O(n²) head-pointer algorithm (#1316)
* perf(mro): replace O(n³) C3 merge loop with O(n²) head-pointer algorithm The C3 linearization merge loop used Array.shift() (O(n) per call) and Array.indexOf() for tail membership checks (O(n) per scan), producing O(n³) total complexity across deep single-inheritance chains. A 2000-class chain took ~43s, exceeding the 15s test timeout. Replace with: - Uint32Array head pointers (O(1) advance, no array mutation) - Pre-computed tail-count Map (O(1) membership check, decremented on head advance) The deep-chain test now completes in ~2s. Closes #1309 * fix(mro): address review findings for C3 merge optimization - Add test for C3 merge-conflict inconsistency (non-cyclic): classic A(X,Y) + B(Y,X) → C(A,B) incompatible ordering, assert fallback to BFS ancestors - Clarify tailCount decrement comment to state the invariant explicitly - Move deep-chain performance test to dedicated describe('performance') block (was incorrectly nested under 'cyclic inheritance')
This commit is contained in:
parent
e92328d474
commit
7be1a5a72d
2 changed files with 58 additions and 18 deletions
|
|
@ -157,19 +157,27 @@ export function c3Linearize(
|
|||
|
||||
// Add the direct parents list as the final sequence
|
||||
const sequences = [...parentLinearizations, [...directParents]];
|
||||
const heads = new Uint32Array(sequences.length); // head pointer per sequence
|
||||
const result: string[] = [];
|
||||
|
||||
// Tail-count map: how many sequences contain this id at index > head.
|
||||
// O(1) membership check replaces O(n) indexOf scans.
|
||||
const tailCount = new Map<string, number>();
|
||||
for (const seq of sequences) {
|
||||
for (let i = 1; i < seq.length; i++) {
|
||||
tailCount.set(seq[i], (tailCount.get(seq[i]) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
let remaining = sequences.reduce((n, s) => n + s.length, 0);
|
||||
let inconsistent = false;
|
||||
while (sequences.some((s) => s.length > 0)) {
|
||||
// Find a good head: one that doesn't appear in the tail of any other sequence
|
||||
|
||||
while (remaining > 0) {
|
||||
let head: string | null = null;
|
||||
for (const seq of sequences) {
|
||||
if (seq.length === 0) continue;
|
||||
const candidate = seq[0];
|
||||
const inTail = sequences.some(
|
||||
(other) => other.length > 1 && other.indexOf(candidate, 1) !== -1,
|
||||
);
|
||||
if (!inTail) {
|
||||
for (let si = 0; si < sequences.length; si++) {
|
||||
if (heads[si] >= sequences[si].length) continue;
|
||||
const candidate = sequences[si][heads[si]];
|
||||
if ((tailCount.get(candidate) ?? 0) === 0) {
|
||||
head = candidate;
|
||||
break;
|
||||
}
|
||||
|
|
@ -182,10 +190,19 @@ export function c3Linearize(
|
|||
|
||||
result.push(head);
|
||||
|
||||
// Remove the chosen head from all sequences
|
||||
for (const seq of sequences) {
|
||||
if (seq.length > 0 && seq[0] === head) {
|
||||
seq.shift();
|
||||
// Advance head pointers past the chosen head; update tail counts
|
||||
for (let si = 0; si < sequences.length; si++) {
|
||||
if (heads[si] >= sequences[si].length) continue;
|
||||
if (sequences[si][heads[si]] === head) {
|
||||
heads[si]++;
|
||||
remaining--;
|
||||
// promoted was in this sequence's active tail; now it's the new head — remove from tailCount
|
||||
if (heads[si] < sequences[si].length) {
|
||||
const promoted = sequences[si][heads[si]];
|
||||
const prev = tailCount.get(promoted)!;
|
||||
if (prev <= 1) tailCount.delete(promoted);
|
||||
else tailCount.set(promoted, prev - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -540,10 +540,35 @@ describe('computeMRO', () => {
|
|||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns null for C3 merge-conflict inconsistency (non-cyclic)', () => {
|
||||
// Classic incompatible ordering: A(X,Y) and B(Y,X) → C(A,B) is unresolvable
|
||||
const graph = createKnowledgeGraph();
|
||||
addClass(graph, 'X', 'python');
|
||||
addClass(graph, 'Y', 'python');
|
||||
addClass(graph, 'A', 'python');
|
||||
addClass(graph, 'B', 'python');
|
||||
addClass(graph, 'C', 'python');
|
||||
addExtends(graph, 'A', 'X');
|
||||
addExtends(graph, 'A', 'Y');
|
||||
addExtends(graph, 'B', 'Y');
|
||||
addExtends(graph, 'B', 'X');
|
||||
addExtends(graph, 'C', 'A');
|
||||
addExtends(graph, 'C', 'B');
|
||||
addMethod(graph, 'X', 'foo');
|
||||
|
||||
const result = computeMRO(graph);
|
||||
expect(result).toBeDefined();
|
||||
// C3 fails for C — falls back to BFS ancestors
|
||||
const entryC = result.entries.find((e) => e.className === 'C');
|
||||
expect(entryC).toBeDefined();
|
||||
// BFS fallback still produces an MRO (just not C3-ordered)
|
||||
expect(entryC!.mro.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- Performance (deep chains) -------------------------------------------
|
||||
describe('performance', () => {
|
||||
it('handles very deep single-inheritance chain without stack overflow', () => {
|
||||
// Chain of 2000 classes: C0 ← C1 ← C2 ← ... ← C1999
|
||||
// The iterative c3Linearize handles this without blowing the stack.
|
||||
// (The recursive version overflows at ~1K–5K levels depending on platform.)
|
||||
const graph = createKnowledgeGraph();
|
||||
const DEPTH = 2000;
|
||||
for (let i = 0; i < DEPTH; i++) {
|
||||
|
|
@ -552,12 +577,10 @@ describe('computeMRO', () => {
|
|||
for (let i = 1; i < DEPTH; i++) {
|
||||
addExtends(graph, `C${i}`, `C${i - 1}`);
|
||||
}
|
||||
// Add a method on the root so MRO produces an entry
|
||||
addMethod(graph, 'C0', 'baseMethod');
|
||||
|
||||
const result = computeMRO(graph);
|
||||
expect(result).toBeDefined();
|
||||
// The deepest class should have all ancestors in its MRO
|
||||
const deepest = result.entries.find((e) => e.className === `C${DEPTH - 1}`);
|
||||
if (deepest) {
|
||||
expect(deepest.mro.length).toBe(DEPTH - 1);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue