v1.1.0: pooled runtime, 959 tests, production hardening (0 squash)
This commit is contained in:
@@ -0,0 +1,379 @@
|
||||
// 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 10ms', () => {
|
||||
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 < 10,
|
||||
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,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user