You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
snabbdom/test/benchmark/core.ts

161 lines
5.0 KiB
TypeScript

import 'core-js/stable/array/fill.js'
import faker from 'faker'
import { VNode } from '../../package/vnode'
import { h } from '../../package/h'
import { init as curInit } from '../../package/init'
import { init as refInit } from 'latest-snabbdom-release/init'
import { assert } from 'chai'
import pReduce from 'p-reduce'
import pMapSeries from 'p-map-series'
import { std, mean } from 'mathjs'
const RUNS = 5
const PATCHES_PER_RUN = 100
const WARM_UP_RUNS = 1
const REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES = 1
const BENCHMARK_TIMEOUT_MINUTES = 10
const REQUIRED_PRECISION = 0.02
/* eslint-disable @typescript-eslint/no-unused-vars */
declare global {
// eslint-disable-next-line @typescript-eslint/naming-convention
const __karma__: {
info(info: unknown): void
}
}
/* eslint-enable @typescript-eslint/no-unused-vars */
const ALLOWED_REGRESSION = 0.03
describe('core benchmark', () => {
it('does not regress', async function Benchmark () {
this.timeout(BENCHMARK_TIMEOUT_MINUTES * 1000 * 60)
faker.seed(0)
const inputs = Array(PATCHES_PER_RUN).fill(null).map(() => {
return new Array(faker.random.number(20))
.fill(null)
.map(() => ({
name: faker.company.companyName(),
catchPhrase: faker.company.catchPhrase(),
suffix: faker.company.companySuffix(),
products: Array(faker.random.number(3))
.fill(null)
.map(() => ({
name: faker.commerce.productName(),
color: faker.commerce.color(),
price: faker.commerce.price() + faker.finance.currencySymbol(),
})),
founded: faker.date.past()
}))
})
type Input = (typeof inputs)[0]
const view = (companies: Input): VNode => h('table', [
h('caption', ['Companies']),
h('thead', [
h('tr', [
'Details',
'Products',
].map((th) => h('th', [th])))
]),
h('tbody', companies.map(function companyView (company) {
return h('tr', [
h('td', [
h('div', [
h('b', [company.name]),
company.suffix && `\xa0${company.suffix}`
]),
h('div', h('i', [company.catchPhrase])),
h('td', [
h('dt', ['Founded']),
h('dd', [company.founded.toLocaleDateString()])
])
]),
h('td', [h('ul', company.products.map(function productView (product) {
return h('li', [h('dl', [
h('dt', ['Name']),
h('dd', [product.name]),
h('dt', ['Color']),
h('dd', [product.color]),
h('dt', ['Price']),
h('dd', [product.price]),
])])
}))])
])
}))
])
type Patcher = ReturnType<typeof refInit | typeof curInit>
interface SingleRunResult {
i: number
cur: number
ref: number
}
const subjectToResult = async (subject: Patcher, subjectId: string): Promise<number> => {
await new Promise((resolve) => {
requestAnimationFrame(resolve)
})
const markName = `mark:${subjectId}`
const measureName = `measure:${subjectId}`
performance.mark(markName)
const lastVnode = await pReduce(
inputs,
async function subjectToResultReducer (acc: HTMLElement | VNode, input, i) {
const vnode = view(input)
subject(acc, vnode)
if (i % REQUEST_ANIMATION_FRAME_EVERY_N_PATCHES === 0) {
await new Promise((resolve) => {
requestAnimationFrame(resolve)
})
}
return vnode
},
document.body.appendChild(document.createElement('section')),
)
performance.measure(measureName, markName)
if (!('elm' in lastVnode)) throw new Error()
if (!lastVnode.elm) throw new Error()
document.body.removeChild(lastVnode.elm)
const measure = performance.getEntriesByName(measureName)[0]
performance.clearMarks(markName)
performance.clearMeasures(measureName)
return measure.duration
}
const singleRun = async (_: null, runI: number): Promise<SingleRunResult> => {
const cur = await subjectToResult(curInit([]), `cur:${runI}`)
const ref = await subjectToResult(refInit([]), `ref:${runI}`)
return { i: runI, cur, ref }
}
const runResults = (await pMapSeries(Array(RUNS + WARM_UP_RUNS).fill(null), singleRun))
.slice(WARM_UP_RUNS)
__karma__.info({ benchmark: runResults })
const results = {
ref: runResults.map((result) => result.ref),
cur: runResults.map((result) => result.cur),
}
const means = {
ref: mean(results.ref),
cur: mean(results.cur),
}
const stds = {
ref: std(results.ref, 'uncorrected'),
cur: std(results.cur, 'uncorrected'),
}
;(['ref', 'cur'] as const).forEach((subject) => {
const stdRatio = stds[subject] / means[subject]
assert.isAtMost(stdRatio, REQUIRED_PRECISION, `${subject} not precise enough`)
})
assert.isAtMost(means.cur, means.ref * (1 + ALLOWED_REGRESSION))
})
})