perf: bound OneNote TOC hierarchy building

This commit is contained in:
2026-07-22 19:54:23 +02:00
parent b6bb75f976
commit ebcaf436cf
4 changed files with 505 additions and 83 deletions

View File

@@ -1,6 +1,29 @@
import { describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { resolveTocChildPath } from './toc-hierarchy.js';
import type { OneNotePackagedSectionDto } from '../model/dto.js';
import type {
OneNoteTableOfContents,
OneNoteTableOfContentsEntry,
} from '../parser/parse-toc.js';
import {
DESKTOP_REVISION_STORE_FORMAT,
ONE_SECTION_FILE_TYPE,
} from '../parser/detect-format.js';
import type { OnePkgError } from './errors.js';
import {
buildOneNotePackageTree,
resolveTocChildPath,
} from './toc-hierarchy.js';
import type { ExtractedCabFile } from './types.js';
const parseTocMock = vi.hoisted(() => vi.fn());
vi.mock('../parser/parse-toc.js', async (importOriginal) => ({
...(await importOriginal<typeof import('../parser/parse-toc.js')>()),
parseOneNoteTableOfContents: parseTocMock,
}));
beforeEach(() => parseTocMock.mockReset());
describe('ONEPKG TOC child paths', () => {
it('joins a single NFC-normalized child name within its section group', () => {
@@ -18,3 +41,244 @@ describe('ONEPKG TOC child paths', () => {
}
);
});
describe('ONEPKG TOC hierarchy budgets and indexes', () => {
it('resolves identity fallbacks in authored order with near-linear section lookups', async () => {
const count = 400;
const pathReads = { count: 0 };
const entries: OneNoteTableOfContentsEntry[] = [];
const extracted: ExtractedCabFile[] = [tocFile()];
const sections: OneNotePackagedSectionDto[] = [];
for (let index = 0; index < count; index += 1) {
const identity = indexedGuid(index + 1);
const path = `Section-${index.toString().padStart(4, '0')}.one`;
extracted.push(sectionFile(path, identity, index + 1));
sections.push(sectionWithObservedPath(path, pathReads));
entries.push({
filename: `Alias-${index.toString().padStart(4, '0')}.one`,
fileIdentity: identity,
orderingId: index,
});
}
parseTocMock.mockReturnValue(toc(entries));
let yields = 0;
const result = await buildOneNotePackageTree(extracted, sections, {
limits: { maxTocOperations: count * 8 + 100 },
cancellation: {
yield: () => {
yields += 1;
},
},
});
expect(result.tree.nodes).toHaveLength(count);
expect(
result.tree.nodes.map((node) =>
node.kind === 'section' ? node.sectionId : `group:${node.id}`
)
).toEqual(sections.map(({ id }) => id));
expect(pathReads.count).toBeLessThanOrEqual(count * 5);
expect(yields).toBeGreaterThan(0);
});
it('keeps ambiguous identities unresolved and fallback sections sorted', async () => {
const identity = indexedGuid(42);
parseTocMock.mockReturnValue(
toc([{ filename: 'Alias.one', fileIdentity: identity, orderingId: 1 }])
);
const sections = [section('Z.one'), section('A.one')];
const result = await buildOneNotePackageTree(
[
tocFile(),
sectionFile('Z.one', identity, 1),
sectionFile('A.one', identity, 2),
],
sections
);
expect(
result.tree.nodes.map((node) =>
node.kind === 'section' ? node.sectionId : `group:${node.id}`
)
).toEqual(['A.one', 'Z.one']);
expect(result.diagnostics.map(({ code }) => code)).toEqual(
expect.arrayContaining([
'onetoc2-entry-unresolved',
'onetoc2-unlisted-sections',
])
);
expect(result.diagnostics).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'onetoc2-section-filename-mismatch',
}),
])
);
});
it('caps adversarial unresolved-entry diagnostics with an explicit marker', async () => {
parseTocMock.mockReturnValue(
toc(
Array.from({ length: 50 }, (_, index) => ({
filename: `Missing-${index}.one`,
fileIdentity: indexedGuid(index + 1),
orderingId: index,
}))
)
);
const result = await buildOneNotePackageTree([tocFile()], [], {
limits: { maxTocDiagnostics: 5, maxTocOperations: 1_000 },
});
expect(result.diagnostics).toHaveLength(5);
expect(result.diagnostics.slice(0, 4).map(({ code }) => code)).toEqual([
'onetoc2-entry-unresolved',
'onetoc2-entry-unresolved',
'onetoc2-entry-unresolved',
'onetoc2-entry-unresolved',
]);
expect(result.diagnostics[4]?.code).toBe('onetoc2-diagnostic-limit');
});
it('fails closed at the hierarchy operation limit', async () => {
parseTocMock.mockReturnValue(
toc(
Array.from({ length: 20 }, (_, index) => ({
filename: `Missing-${index}.one`,
fileIdentity: indexedGuid(index + 1),
orderingId: index,
}))
)
);
await expect(
buildOneNotePackageTree([tocFile()], [], {
limits: { maxTocOperations: 10 },
})
).rejects.toEqual(
expect.objectContaining<Partial<OnePkgError>>({
diagnostic: expect.objectContaining({
code: 'onetoc2-operation-limit',
}),
})
);
});
it('yields and observes cancellation during a large hierarchy build', async () => {
parseTocMock.mockReturnValue(
toc(
Array.from({ length: 1_000 }, (_, index) => ({
filename: `Missing-${index}.one`,
fileIdentity: indexedGuid(index + 1),
orderingId: index,
}))
)
);
const controller = new AbortController();
let yields = 0;
await expect(
buildOneNotePackageTree([tocFile()], [], {
cancellation: {
signal: controller.signal,
yield: () => {
yields += 1;
controller.abort();
},
},
})
).rejects.toEqual(
expect.objectContaining<Partial<OnePkgError>>({
diagnostic: expect.objectContaining({ code: 'onepkg-cancelled' }),
})
);
expect(yields).toBe(1);
});
});
function toc(entries: OneNoteTableOfContentsEntry[]): OneNoteTableOfContents {
return {
sourceName: 'Open Notebook.onetoc2',
fileIdentity: indexedGuid(0),
entries,
diagnostics: [],
};
}
function tocFile(): ExtractedCabFile {
return extractedFile('Open Notebook.onetoc2', Uint8Array.of(1), 0);
}
function sectionFile(
path: string,
identity: string,
index: number
): ExtractedCabFile {
const bytes = new Uint8Array(64);
writeGuid(bytes, 0, ONE_SECTION_FILE_TYPE);
writeGuid(bytes, 16, identity);
writeGuid(bytes, 48, DESKTOP_REVISION_STORE_FORMAT);
return extractedFile(path, bytes, index);
}
function extractedFile(
path: string,
bytes: Uint8Array,
index: number
): ExtractedCabFile {
return {
index,
path,
normalizedPath: path,
folderIndex: 0,
uncompressedOffset: 0,
uncompressedSize: bytes.byteLength,
attributes: 0,
compression: { kind: 'none', raw: 0, label: 'None' },
bytes,
};
}
function section(path: string): OneNotePackagedSectionDto {
return {
id: path,
path,
displayName: path.slice(0, -4),
parseStatus: 'not-requested',
diagnostics: [],
};
}
function sectionWithObservedPath(
path: string,
reads: { count: number }
): OneNotePackagedSectionDto {
const result = section(path);
Object.defineProperty(result, 'path', {
enumerable: true,
get: () => {
reads.count += 1;
return path;
},
});
return result;
}
function indexedGuid(index: number): string {
const plain = index.toString(16).toUpperCase().padStart(32, '0');
return `{${plain.slice(0, 8)}-${plain.slice(8, 12)}-${plain.slice(12, 16)}-${plain.slice(16, 20)}-${plain.slice(20)}}`;
}
function writeGuid(target: Uint8Array, offset: number, guid: string): void {
const plain = guid.replace(/[{}-]/gu, '');
const canonical = Array.from({ length: 16 }, (_, index) =>
Number.parseInt(plain.slice(index * 2, index * 2 + 2), 16)
);
const order = [3, 2, 1, 0, 5, 4, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15];
for (let index = 0; index < order.length; index += 1) {
target[offset + order[index]!] = canonical[index]!;
}
}