// perf.test.ts - Performance regression tests for Imhotep core hot paths // Uses Node.js built-in test runner and performance hooks. // Targets (V1 developer guide): // Unit tests < 10ms each // Integration tests < 100ms each // Compilation < 5ms per assertion // Extraction < 50ms per page // Solving < 1ms per clause import { describe, it } from 'node:test' import assert from 'node:assert' import { performance } from 'node:perf_hooks' import { compile } from 'imhotep-dsl' import { normalizeSubjects, normalizeBoxes, normalizeTopology, normalizeFrames, buildTransformChain, composeTransform2D, clearTransformCaches, } from 'imhotep-geometry' import { evaluate, registerDefaultClauses } from 'imhotep-solver' describe('Performance regression — compilation', () => { it('compiles 100 assertions in under 500ms', () => { const assertions = generateAssertions(100) const start = performance.now() const ir = compile(assertions) const duration = performance.now() - start assert.strictEqual( duration < 500, true, `Compilation took ${duration.toFixed(2)}ms` ) }) it('compiles 10 assertions in under 50ms (5ms per assertion)', () => { const assertions = generateAssertions(10) const start = performance.now() compile(assertions) const duration = performance.now() - start assert.strictEqual( duration < 50, true, `Compilation took ${duration.toFixed(2)}ms` ) }) }) describe('Performance regression — world creation', () => { it('normalizes 1000 subjects in under 50ms', () => { const raw = generateRawSubjects(1000) const start = performance.now() normalizeSubjects(raw) const duration = performance.now() - start assert.strictEqual( duration < 50, true, `Subject normalization took ${duration.toFixed(2)}ms` ) }) it('normalizes 1000 boxes in under 50ms', () => { const raw = generateRawBoxes(1000) const start = performance.now() normalizeBoxes(raw) const duration = performance.now() - start assert.strictEqual( duration < 50, true, `Box normalization took ${duration.toFixed(2)}ms` ) }) it('normalizes topology in under 25ms', () => { const raw = generateRawTopology(1000) const start = performance.now() normalizeTopology(raw, 1000) const duration = performance.now() - start assert.strictEqual( duration < 25, true, `Topology normalization took ${duration.toFixed(2)}ms` ) }) }) describe('Performance regression — frame resolution', () => { it('resolves 100 transform chains in under 50ms', () => { const matrices = generateTransformMatrices() const start = performance.now() for (let i = 0; i < 100; i++) { buildTransformChain(matrices) } const duration = performance.now() - start assert.strictEqual( duration < 50, true, `Frame resolution took ${duration.toFixed(2)}ms` ) }) it('composes 100 2D transforms in under 100ms', () => { const start = performance.now() for (let i = 0; i < 100; i++) { composeTransform2D(0, 0, i, i, 0, 1, 1) } const duration = performance.now() - start assert.strictEqual( duration < 100, true, `Transform composition took ${duration.toFixed(2)}ms` ) }) it('caches repeated transform chains (second pass under 1ms)', () => { const matrices = generateTransformMatrices() clearTransformCaches() buildTransformChain(matrices) // warm cache const start = performance.now() for (let i = 0; i < 100; i++) { buildTransformChain(matrices) } const duration = performance.now() - start assert.strictEqual( duration < 1, true, `Cached frame resolution took ${duration.toFixed(2)}ms` ) }) }) describe('Performance regression — solving', () => { it('evaluates 100 clauses in under 100ms (1ms per clause)', () => { registerDefaultClauses() const world = generateMinimalWorld() const clauses = generateClauses(100) const start = performance.now() evaluate(world, clauses, { skipMissingEvaluators: true }) const duration = performance.now() - start assert.strictEqual( duration < 100, true, `Solving took ${duration.toFixed(2)}ms` ) }) it('evaluates 10 clauses in under 25ms', () => { registerDefaultClauses() const world = generateMinimalWorld() const clauses = generateClauses(10) const start = performance.now() evaluate(world, clauses, { skipMissingEvaluators: true }) const duration = performance.now() - start assert.strictEqual( duration < 25, true, `Solving took ${duration.toFixed(2)}ms` ) }) }) // --------------------------------------------------------------------------- // Mock data generators // --------------------------------------------------------------------------- function generateAssertions(n: number): Array<{ type: 'RelationAssertion' relation: string subject: { type: 'SelectorRef'; value: string } reference: { type: 'SelectorRef'; value: string } options: { type: 'RelationOptions' } }> { return Array.from({ length: n }, (_, i) => ({ type: 'RelationAssertion' as const, relation: i % 2 === 0 ? 'leftOf' : 'above', subject: { type: 'SelectorRef' as const, value: `.s-${i}` }, reference: { type: 'SelectorRef' as const, value: `.r-${i}` }, options: { type: 'RelationOptions' as const }, })) } function generateRawSubjects(n: number): Array<{ id: number domNodeId: number kind: string primaryBoxId: number firstFragmentId: number fragmentCount: number firstTextRunId: number textRunCount: number }> { return Array.from({ length: n }, (_, i) => ({ id: i, domNodeId: i, kind: 'element', primaryBoxId: i, firstFragmentId: 0, fragmentCount: 0, firstTextRunId: 0, textRunCount: 0, })) } function generateRawBoxes(n: number): Array<{ id: number subjectId: number frameId: number borderLeft: number borderTop: number borderRight: number borderBottom: number paddingLeft: number paddingTop: number paddingRight: number paddingBottom: number contentLeft: number contentTop: number contentRight: number contentBottom: number }> { return Array.from({ length: n }, (_, i) => ({ id: i, subjectId: i, frameId: 0, borderLeft: 0, borderTop: 0, borderRight: 10, borderBottom: 10, paddingLeft: 0, paddingTop: 0, paddingRight: 0, paddingBottom: 0, contentLeft: 0, contentTop: 0, contentRight: 10, contentBottom: 10, })) } function generateRawTopology(n: number): { containingBlockOf: number[] nearestPositionedAncestorOf: number[] scrollContainerOf: number[] stackingContextOf: number[] formattingContextOf: number[] clippingRootOf: number[] paintOrderBucket: number[] paintOrderIndex: number[] } { return { containingBlockOf: Array.from({ length: n }, () => 0), nearestPositionedAncestorOf: Array.from({ length: n }, () => 0), scrollContainerOf: Array.from({ length: n }, () => 0), stackingContextOf: Array.from({ length: n }, () => 0), formattingContextOf: Array.from({ length: n }, () => 0), clippingRootOf: Array.from({ length: n }, () => 0), paintOrderBucket: Array.from({ length: n }, () => 0), paintOrderIndex: Array.from({ length: n }, () => 0), } } function generateTransformMatrices(): Float64Array[] { return [ new Float64Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]), new Float64Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 10, 10, 0, 1]), ] } function generateMinimalWorld(): any { return { sceneId: 'scene_1', snapshotId: 'snapshot_1', env: { viewportWidth: 1280, viewportHeight: 720, deviceScaleFactor: 1, colorScheme: 'light', pointer: 'fine', hover: false, reducedMotion: false, locale: 'en', writingMode: 'horizontal-tb', }, strings: { values: [] }, subjects: { ids: new Uint32Array([0, 1]), domNodeId: new Uint32Array([0, 1]), subjectKind: new Uint16Array([1, 1]), primaryBoxId: new Uint32Array([0, 1]), firstFragmentId: new Uint32Array([0, 0]), fragmentCount: new Uint16Array([0, 0]), }, boxes: { boxId: new Uint32Array([0, 1]), subjectId: new Uint32Array([0, 1]), frameId: new Uint32Array([0, 0]), borderLeft: new Float64Array([0, 0]), borderTop: new Float64Array([0, 0]), borderRight: new Float64Array([10, 10]), borderBottom: new Float64Array([10, 10]), paddingLeft: new Float64Array([0, 0]), paddingTop: new Float64Array([0, 0]), paddingRight: new Float64Array([0, 0]), paddingBottom: new Float64Array([0, 0]), contentLeft: new Float64Array([0, 0]), contentTop: new Float64Array([0, 0]), contentRight: new Float64Array([10, 10]), contentBottom: new Float64Array([10, 10]), }, rects: { rectId: new Uint32Array([0, 1]), left: new Float64Array([0, 5]), top: new Float64Array([0, 5]), right: new Float64Array([10, 15]), bottom: new Float64Array([10, 15]), }, topology: { containingBlockOf: new Uint32Array([0, 0]), nearestPositionedAncestorOf: new Uint32Array([0, 0]), scrollContainerOf: new Uint32Array([0, 0]), stackingContextOf: new Uint32Array([0, 0]), formattingContextOf: new Uint32Array([0, 0]), clippingRootOf: new Uint32Array([0, 0]), paintOrderBucket: new Uint16Array([0, 0]), paintOrderIndex: new Uint32Array([0, 1]), }, scroll: { containerId: new Uint32Array([0]), scrollLeft: new Float64Array([0]), scrollTop: new Float64Array([0]), scrollWidth: new Float64Array([1280]), scrollHeight: new Float64Array([720]), clientWidth: new Float64Array([1280]), clientHeight: new Float64Array([720]), }, clipping: { clipNodeId: new Uint32Array([0]), subjectId: new Uint32Array([0]), clipKind: new Uint16Array([1]), clipLeft: new Float64Array([0]), clipTop: new Float64Array([0]), clipRight: new Float64Array([1280]), clipBottom: new Float64Array([720]), parentClipNodeId: new Uint32Array([0]), }, visibility: { subjectId: new Uint32Array([0, 1]), isRendered: new Uint8Array([1, 1]), isVisible: new Uint8Array([1, 1]), visibleArea: new Float64Array([100, 100]), clippedArea: new Float64Array([0, 0]), }, } } function generateClauses(n: number): Array<{ clauseId: string clauseKind: string version: number subjectRef: number referenceRef?: number }> { const kinds = [ 'relation.leftOf', 'relation.above', 'relation.overlaps', 'relation.inside', ] return Array.from({ length: n }, (_, i) => ({ clauseId: `clause-${i}`, clauseKind: kinds[i % kinds.length], version: 1, subjectRef: 0, referenceRef: 1, })) }