Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
293 lines
7.4 KiB
TypeScript
293 lines
7.4 KiB
TypeScript
/**
|
|
* Benchmark script for @tldraw/validate DictValidator
|
|
*
|
|
* Usage:
|
|
* yarn tsx internal/scripts/benchmark-dict-validator.ts
|
|
*
|
|
* This script measures the performance of the DictValidator in @tldraw/validate.
|
|
* It tests both fresh validation and validateUsingKnownGoodVersion scenarios.
|
|
*/
|
|
|
|
import * as T from '../../packages/validate/src/lib/validation'
|
|
|
|
const ITERATIONS_PER_RUN = 50_000
|
|
const TOTAL_RUNS = 200
|
|
|
|
interface BenchmarkResult {
|
|
first100Avg: number
|
|
last100Avg: number
|
|
opsPerSecFirst: number
|
|
opsPerSecLast: number
|
|
}
|
|
|
|
function runBenchmark(name: string, fn: () => void): BenchmarkResult {
|
|
const times: number[] = []
|
|
|
|
for (let run = 0; run < TOTAL_RUNS; run++) {
|
|
const start = performance.now()
|
|
for (let i = 0; i < ITERATIONS_PER_RUN; i++) {
|
|
fn()
|
|
}
|
|
times.push(performance.now() - start)
|
|
}
|
|
|
|
const first100 = times.slice(0, 100)
|
|
const last100 = times.slice(100)
|
|
const first100Avg = first100.reduce((a, b) => a + b, 0) / first100.length
|
|
const last100Avg = last100.reduce((a, b) => a + b, 0) / last100.length
|
|
|
|
return {
|
|
first100Avg,
|
|
last100Avg,
|
|
opsPerSecFirst: (ITERATIONS_PER_RUN / first100Avg) * 1000,
|
|
opsPerSecLast: (ITERATIONS_PER_RUN / last100Avg) * 1000,
|
|
}
|
|
}
|
|
|
|
function formatOps(ops: number): string {
|
|
if (ops >= 1_000_000) {
|
|
return `${(ops / 1_000_000).toFixed(2)}M`
|
|
}
|
|
if (ops >= 1_000) {
|
|
return `${(ops / 1_000).toFixed(2)}K`
|
|
}
|
|
return ops.toFixed(0)
|
|
}
|
|
|
|
function printResult(name: string, result: BenchmarkResult): void {
|
|
const warmupChange = ((result.first100Avg / result.last100Avg - 1) * 100).toFixed(1)
|
|
const warmupDir = Number(warmupChange) > 0 ? 'slower' : 'faster'
|
|
|
|
console.log(`--- ${name} ---`)
|
|
console.log(
|
|
` First 100 runs: ${result.first100Avg.toFixed(2)}ms (${formatOps(result.opsPerSecFirst)} ops/sec)`
|
|
)
|
|
console.log(
|
|
` Last 100 runs: ${result.last100Avg.toFixed(2)}ms (${formatOps(result.opsPerSecLast)} ops/sec)`
|
|
)
|
|
console.log(` JIT stability: First 100 ${Math.abs(Number(warmupChange))}% ${warmupDir}`)
|
|
console.log('')
|
|
}
|
|
|
|
// Test data for dict validators
|
|
// Small dict (3 keys) - common case for simple configs
|
|
const smallDict: Record<string, number> = {
|
|
width: 100,
|
|
height: 200,
|
|
depth: 50,
|
|
}
|
|
|
|
// Medium dict (10 keys) - typical shape props
|
|
const mediumDict: Record<string, number> = {}
|
|
for (let i = 0; i < 10; i++) {
|
|
mediumDict[`key${i}`] = i * 10
|
|
}
|
|
|
|
// Large dict (50 keys) - stress test
|
|
const largeDict: Record<string, number> = {}
|
|
for (let i = 0; i < 50; i++) {
|
|
largeDict[`key${i}`] = i * 10
|
|
}
|
|
|
|
// Line shape points scenario - realistic use case
|
|
interface LinePoint {
|
|
id: string
|
|
index: string
|
|
x: number
|
|
y: number
|
|
}
|
|
const linePoints: Record<string, LinePoint> = {}
|
|
for (let i = 0; i < 5; i++) {
|
|
const id = `point${i}`
|
|
linePoints[id] = { id, index: `a${i}`, x: i * 10, y: i * 20 }
|
|
}
|
|
|
|
// Validators
|
|
const stringToNumberDict = T.dict(T.string, T.number)
|
|
const linePointValidator = T.object({
|
|
id: T.string,
|
|
index: T.string,
|
|
x: T.number,
|
|
y: T.number,
|
|
})
|
|
const linePointsDict = T.dict(T.string, linePointValidator)
|
|
|
|
console.log('='.repeat(70))
|
|
console.log('DICT VALIDATOR BENCHMARK')
|
|
console.log('='.repeat(70))
|
|
console.log(`Iterations per run: ${ITERATIONS_PER_RUN.toLocaleString()}`)
|
|
console.log(`Total runs: ${TOTAL_RUNS}`)
|
|
console.log(`Comparing first 100 runs (warmup) vs last 100 runs (stable)`)
|
|
console.log('')
|
|
|
|
// --- Fresh validation benchmarks ---
|
|
console.log('='.repeat(70))
|
|
console.log('FRESH VALIDATION (validate)')
|
|
console.log('='.repeat(70))
|
|
console.log('')
|
|
|
|
// Small dict fresh validation
|
|
{
|
|
const testDicts = [smallDict, { ...smallDict, extra: 999 }, { a: 1, b: 2, c: 3 }]
|
|
let idx = 0
|
|
printResult(
|
|
'Small dict (3 keys)',
|
|
runBenchmark('SmallDict', () => {
|
|
stringToNumberDict.validate(testDicts[idx++ % testDicts.length])
|
|
})
|
|
)
|
|
}
|
|
|
|
// Medium dict fresh validation
|
|
{
|
|
const testDicts = [mediumDict, { ...mediumDict }]
|
|
let idx = 0
|
|
printResult(
|
|
'Medium dict (10 keys)',
|
|
runBenchmark('MediumDict', () => {
|
|
stringToNumberDict.validate(testDicts[idx++ % testDicts.length])
|
|
})
|
|
)
|
|
}
|
|
|
|
// Large dict fresh validation
|
|
{
|
|
const testDicts = [largeDict, { ...largeDict }]
|
|
let idx = 0
|
|
printResult(
|
|
'Large dict (50 keys)',
|
|
runBenchmark('LargeDict', () => {
|
|
stringToNumberDict.validate(testDicts[idx++ % testDicts.length])
|
|
})
|
|
)
|
|
}
|
|
|
|
// Line points dict (realistic scenario)
|
|
{
|
|
const testDicts = [linePoints, { ...linePoints }]
|
|
let idx = 0
|
|
printResult(
|
|
'Line points dict (5 nested objects)',
|
|
runBenchmark('LinePoints', () => {
|
|
linePointsDict.validate(testDicts[idx++ % testDicts.length])
|
|
})
|
|
)
|
|
}
|
|
|
|
// --- ValidateUsingKnownGoodVersion benchmarks ---
|
|
console.log('='.repeat(70))
|
|
console.log('INCREMENTAL VALIDATION (validateUsingKnownGoodVersion)')
|
|
console.log('='.repeat(70))
|
|
console.log('')
|
|
|
|
// Scenario 1: No changes (should be fast - identity check)
|
|
{
|
|
const knownGood = stringToNumberDict.validate(smallDict)
|
|
printResult(
|
|
'Small dict - no changes (same reference)',
|
|
runBenchmark('SmallDictNoChange', () => {
|
|
stringToNumberDict.validateUsingKnownGoodVersion(knownGood, knownGood)
|
|
})
|
|
)
|
|
}
|
|
|
|
// Scenario 2: Same values, different reference
|
|
{
|
|
const knownGood = stringToNumberDict.validate(smallDict)
|
|
const newValue = { ...smallDict } // Same values, new object
|
|
printResult(
|
|
'Small dict - same values, different reference',
|
|
runBenchmark('SmallDictSameValues', () => {
|
|
stringToNumberDict.validateUsingKnownGoodVersion(knownGood, newValue)
|
|
})
|
|
)
|
|
}
|
|
|
|
// Scenario 3: One value changed
|
|
{
|
|
const knownGood = stringToNumberDict.validate(smallDict)
|
|
const changed = { ...smallDict, width: 150 }
|
|
printResult(
|
|
'Small dict - one value changed',
|
|
runBenchmark('SmallDictOneChange', () => {
|
|
stringToNumberDict.validateUsingKnownGoodVersion(knownGood, changed)
|
|
})
|
|
)
|
|
}
|
|
|
|
// Scenario 4: Key added
|
|
{
|
|
const knownGood = stringToNumberDict.validate(smallDict)
|
|
const added = { ...smallDict, newKey: 999 }
|
|
printResult(
|
|
'Small dict - key added',
|
|
runBenchmark('SmallDictKeyAdded', () => {
|
|
stringToNumberDict.validateUsingKnownGoodVersion(knownGood, added)
|
|
})
|
|
)
|
|
}
|
|
|
|
// Scenario 5: Key removed
|
|
{
|
|
const { width: _, ...rest } = smallDict
|
|
const knownGood = stringToNumberDict.validate(smallDict)
|
|
printResult(
|
|
'Small dict - key removed',
|
|
runBenchmark('SmallDictKeyRemoved', () => {
|
|
stringToNumberDict.validateUsingKnownGoodVersion(knownGood, rest)
|
|
})
|
|
)
|
|
}
|
|
|
|
// Medium dict scenarios
|
|
{
|
|
const knownGood = stringToNumberDict.validate(mediumDict)
|
|
const newValue = { ...mediumDict }
|
|
printResult(
|
|
'Medium dict - same values, different reference',
|
|
runBenchmark('MediumDictSameValues', () => {
|
|
stringToNumberDict.validateUsingKnownGoodVersion(knownGood, newValue)
|
|
})
|
|
)
|
|
}
|
|
|
|
{
|
|
const knownGood = stringToNumberDict.validate(mediumDict)
|
|
const changed = { ...mediumDict, key5: 999 }
|
|
printResult(
|
|
'Medium dict - one value changed',
|
|
runBenchmark('MediumDictOneChange', () => {
|
|
stringToNumberDict.validateUsingKnownGoodVersion(knownGood, changed)
|
|
})
|
|
)
|
|
}
|
|
|
|
// Line points incremental
|
|
{
|
|
const knownGood = linePointsDict.validate(linePoints)
|
|
const newValue = { ...linePoints }
|
|
printResult(
|
|
'Line points - same values, different reference',
|
|
runBenchmark('LinePointsSameValues', () => {
|
|
linePointsDict.validateUsingKnownGoodVersion(knownGood, newValue)
|
|
})
|
|
)
|
|
}
|
|
|
|
{
|
|
const knownGood = linePointsDict.validate(linePoints)
|
|
const changed = {
|
|
...linePoints,
|
|
point2: { ...linePoints.point2, x: 999 },
|
|
}
|
|
printResult(
|
|
'Line points - one point changed',
|
|
runBenchmark('LinePointsOneChange', () => {
|
|
linePointsDict.validateUsingKnownGoodVersion(knownGood, changed)
|
|
})
|
|
)
|
|
}
|
|
|
|
console.log('='.repeat(70))
|
|
console.log('')
|