Performance measurement tool (for unit tests) (#3447)
This PR adds a micro benchmarking utility. We can use it in our jest tests or in random scripts, though given the other requirements of our library, benchmarking. <img width="750" alt="Screenshot 2024-04-11 at 2 44 23 PM" src="https://github.com/tldraw/tldraw/assets/23072548/6bba07eb-65fd-45a2-abd8-ddd0e206b9fa"> ## What this isn't This is not benchmarking. The speeds etc are based on your machine. ## What this is This is a tool for measuring / comparing different implementations etc. Some things run much faster than others. ### Change Type - [x] `sdk` - [x] `internal`
This commit is contained in:
parent
a18525ea78
commit
b5c87ab876
2 changed files with 347 additions and 0 deletions
156
packages/tldraw/src/test/perf/PerformanceMeasurer.ts
Normal file
156
packages/tldraw/src/test/perf/PerformanceMeasurer.ts
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
const now = () => {
|
||||||
|
const hrTime = process.hrtime()
|
||||||
|
return hrTime[0] * 1000 + hrTime[1] / 1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PerformanceMeasurer {
|
||||||
|
private setupFn?: () => void
|
||||||
|
private beforeFns: (() => void)[] = []
|
||||||
|
private fns: (() => void)[] = []
|
||||||
|
private afterFns: (() => void)[] = []
|
||||||
|
private teardownFn?: () => void
|
||||||
|
private warmupIterations = 0
|
||||||
|
private iterations = 0
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
average = 0
|
||||||
|
cold = 0
|
||||||
|
fastest = Infinity
|
||||||
|
slowest = -Infinity
|
||||||
|
|
||||||
|
totalStart = 0
|
||||||
|
totalEnd = 0
|
||||||
|
totalTime = 0
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public name: string,
|
||||||
|
opts = {} as {
|
||||||
|
warmupIterations?: number
|
||||||
|
iterations?: number
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { warmupIterations = 0, iterations = 10 } = opts
|
||||||
|
this.warmupIterations = warmupIterations
|
||||||
|
this.iterations = iterations
|
||||||
|
}
|
||||||
|
|
||||||
|
setup(cb: () => void) {
|
||||||
|
this.setupFn = cb
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
teardown(cb: () => void) {
|
||||||
|
this.teardownFn = cb
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
add(cb: () => void) {
|
||||||
|
this.fns.push(cb)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
before(cb: () => void) {
|
||||||
|
this.beforeFns.push(cb)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
after(cb: () => void) {
|
||||||
|
this.afterFns.push(cb)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
run() {
|
||||||
|
const { fns, beforeFns, afterFns, warmupIterations, iterations } = this
|
||||||
|
|
||||||
|
// Run the cold run
|
||||||
|
this.setupFn?.()
|
||||||
|
const a = now()
|
||||||
|
for (let k = 0; k < beforeFns.length; k++) {
|
||||||
|
beforeFns[k]()
|
||||||
|
}
|
||||||
|
for (let j = 0; j < fns.length; j++) {
|
||||||
|
fns[j]()
|
||||||
|
}
|
||||||
|
for (let l = 0; l < afterFns.length; l++) {
|
||||||
|
afterFns[l]()
|
||||||
|
}
|
||||||
|
const duration = now() - a
|
||||||
|
this.cold = duration
|
||||||
|
this.teardownFn?.()
|
||||||
|
|
||||||
|
// Run all of the warmup iterations
|
||||||
|
if (this.warmupIterations > 0) {
|
||||||
|
this.setupFn?.()
|
||||||
|
for (let i = 0; i < warmupIterations; i++) {
|
||||||
|
for (let k = 0; k < beforeFns.length; k++) {
|
||||||
|
beforeFns[k]()
|
||||||
|
}
|
||||||
|
const _a = now()
|
||||||
|
for (let j = 0; j < fns.length; j++) {
|
||||||
|
fns[j]()
|
||||||
|
}
|
||||||
|
const _duration = now() - a
|
||||||
|
for (let l = 0; l < afterFns.length; l++) {
|
||||||
|
afterFns[l]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.teardownFn?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.totalStart = now()
|
||||||
|
|
||||||
|
// Run all of the iterations and calculate average
|
||||||
|
if (this.iterations > 0) {
|
||||||
|
this.setupFn?.()
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
for (let k = 0; k < beforeFns.length; k++) {
|
||||||
|
beforeFns[k]()
|
||||||
|
}
|
||||||
|
const a = now()
|
||||||
|
for (let j = 0; j < fns.length; j++) {
|
||||||
|
fns[j]()
|
||||||
|
}
|
||||||
|
const duration = now() - a
|
||||||
|
this.total += duration
|
||||||
|
this.fastest = Math.min(duration, this.fastest)
|
||||||
|
this.slowest = Math.max(duration, this.fastest)
|
||||||
|
for (let l = 0; l < afterFns.length; l++) {
|
||||||
|
afterFns[l]()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.teardownFn?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.totalTime = now() - this.totalStart
|
||||||
|
if (iterations > 0) {
|
||||||
|
this.average = this.total / iterations
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
report() {
|
||||||
|
return PerformanceMeasurer.Table(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
static Table(...ps: PerformanceMeasurer[]) {
|
||||||
|
const table: Record<string, Record<string, number | string>> = {}
|
||||||
|
const fastest = ps.map((p) => p.average).reduce((a, b) => Math.min(a, b))
|
||||||
|
const totalFastest = ps.map((p) => p.totalTime).reduce((a, b) => Math.min(a, b))
|
||||||
|
|
||||||
|
ps.forEach(
|
||||||
|
(p) =>
|
||||||
|
(table[p.name] = {
|
||||||
|
['Runs']: p.warmupIterations + p.iterations,
|
||||||
|
['Cold']: Number(p.cold.toFixed(2)),
|
||||||
|
['Slowest']: Number(p.slowest.toFixed(2)),
|
||||||
|
['Fastest']: Number(p.fastest.toFixed(2)),
|
||||||
|
['Average']: Number(p.average.toFixed(2)),
|
||||||
|
['Slower (Avg)']: Number((p.average / fastest).toFixed(2)),
|
||||||
|
['Slower (All)']: Number((p.totalTime / totalFastest).toFixed(2)),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.table(table)
|
||||||
|
}
|
||||||
|
}
|
191
packages/tldraw/src/test/perf/perf.test.ts
Normal file
191
packages/tldraw/src/test/perf/perf.test.ts
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
import { TLShapePartial, createShapeId } from '@tldraw/editor'
|
||||||
|
import { TestEditor } from '../TestEditor'
|
||||||
|
import { PerformanceMeasurer } from './PerformanceMeasurer'
|
||||||
|
|
||||||
|
let editor = new TestEditor()
|
||||||
|
|
||||||
|
jest.useRealTimers()
|
||||||
|
|
||||||
|
describe.skip('Example perf tests', () => {
|
||||||
|
it('measures Editor.createShape vs Editor.createShapes', () => {
|
||||||
|
const withCreateShape = new PerformanceMeasurer('Create 200 shapes using Editor.createShape', {
|
||||||
|
warmupIterations: 10,
|
||||||
|
iterations: 10,
|
||||||
|
})
|
||||||
|
.before(() => {
|
||||||
|
editor = new TestEditor()
|
||||||
|
})
|
||||||
|
.add(() => {
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
editor.createShape({
|
||||||
|
type: 'geo',
|
||||||
|
x: (i % 10) * 220,
|
||||||
|
y: Math.floor(i / 10) * 220,
|
||||||
|
props: { w: 200, h: 200 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const withCreateShapes = new PerformanceMeasurer(
|
||||||
|
'Create 200 shapes using Editor.createShapes',
|
||||||
|
{
|
||||||
|
warmupIterations: 10,
|
||||||
|
iterations: 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.before(() => {
|
||||||
|
editor = new TestEditor()
|
||||||
|
})
|
||||||
|
.add(() => {
|
||||||
|
const shapesToCreate: TLShapePartial[] = []
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
shapesToCreate.push({
|
||||||
|
id: createShapeId(),
|
||||||
|
type: 'geo',
|
||||||
|
x: (i % 10) * 220,
|
||||||
|
y: Math.floor(i / 10) * 220,
|
||||||
|
props: { w: 200, h: 200 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.createShapes(shapesToCreate)
|
||||||
|
})
|
||||||
|
|
||||||
|
withCreateShape.run()
|
||||||
|
withCreateShapes.run()
|
||||||
|
|
||||||
|
PerformanceMeasurer.Table(withCreateShape, withCreateShapes)
|
||||||
|
|
||||||
|
expect(withCreateShape.average).toBeGreaterThan(withCreateShapes.average)
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
it('measures Editor.updateShape vs Editor.updateShapes', () => {
|
||||||
|
const ids = Array.from(Array(200)).map(() => createShapeId())
|
||||||
|
|
||||||
|
const withUpdateShape = new PerformanceMeasurer('Update 200 shapes using Editor.updateShape', {
|
||||||
|
warmupIterations: 10,
|
||||||
|
iterations: 10,
|
||||||
|
})
|
||||||
|
.before(() => {
|
||||||
|
editor = new TestEditor()
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
editor.createShape({
|
||||||
|
type: 'geo',
|
||||||
|
id: ids[i],
|
||||||
|
x: (i % 10) * 220,
|
||||||
|
y: Math.floor(i / 10) * 220,
|
||||||
|
props: { w: 200, h: 200 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.add(() => {
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
editor.updateShape({
|
||||||
|
type: 'geo',
|
||||||
|
id: ids[i],
|
||||||
|
x: (i % 10) * 220 + 1,
|
||||||
|
props: {
|
||||||
|
w: 201,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const withUpdateShapes = new PerformanceMeasurer(
|
||||||
|
'Update 200 shapes using Editor.updateShapes',
|
||||||
|
{
|
||||||
|
warmupIterations: 10,
|
||||||
|
iterations: 10,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.before(() => {
|
||||||
|
editor = new TestEditor()
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
editor.createShape({
|
||||||
|
id: ids[i],
|
||||||
|
type: 'geo',
|
||||||
|
x: (i % 10) * 220,
|
||||||
|
y: Math.floor(i / 10) * 220,
|
||||||
|
props: { w: 200, h: 200 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.add(() => {
|
||||||
|
const shapesToUpdate: TLShapePartial[] = []
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
shapesToUpdate.push({
|
||||||
|
id: ids[i],
|
||||||
|
type: 'geo',
|
||||||
|
x: (i % 10) * 220 + 1,
|
||||||
|
props: {
|
||||||
|
w: 201,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
editor.updateShapes(shapesToUpdate)
|
||||||
|
})
|
||||||
|
|
||||||
|
withUpdateShape.run()
|
||||||
|
withUpdateShapes.run()
|
||||||
|
|
||||||
|
PerformanceMeasurer.Table(withUpdateShape, withUpdateShapes)
|
||||||
|
}, 10000)
|
||||||
|
|
||||||
|
it('Measures rendering shapes', () => {
|
||||||
|
const renderingShapes = new PerformanceMeasurer('Measure rendering bounds with 100 shapes', {
|
||||||
|
warmupIterations: 10,
|
||||||
|
iterations: 20,
|
||||||
|
})
|
||||||
|
.before(() => {
|
||||||
|
editor = new TestEditor()
|
||||||
|
const shapesToCreate: TLShapePartial[] = []
|
||||||
|
for (let i = 0; i < 100; i++) {
|
||||||
|
shapesToCreate.push({
|
||||||
|
id: createShapeId(),
|
||||||
|
type: 'geo',
|
||||||
|
x: (i % 10) * 220,
|
||||||
|
y: Math.floor(i / 10) * 220,
|
||||||
|
props: { w: 200, h: 200 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
editor.createShapes(shapesToCreate)
|
||||||
|
})
|
||||||
|
.add(() => {
|
||||||
|
editor.getRenderingShapes()
|
||||||
|
})
|
||||||
|
.after(() => {
|
||||||
|
const shape = editor.getCurrentPageShapes()[0]
|
||||||
|
editor.updateShape({ ...shape, x: shape.x + 1 })
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
|
||||||
|
const renderingShapes2 = new PerformanceMeasurer('Measure rendering bounds with 200 shapes', {
|
||||||
|
warmupIterations: 10,
|
||||||
|
iterations: 20,
|
||||||
|
})
|
||||||
|
.before(() => {
|
||||||
|
editor = new TestEditor()
|
||||||
|
const shapesToCreate: TLShapePartial[] = []
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
shapesToCreate.push({
|
||||||
|
id: createShapeId(),
|
||||||
|
type: 'geo',
|
||||||
|
x: (i % 10) * 220,
|
||||||
|
y: Math.floor(i / 10) * 220,
|
||||||
|
props: { w: 200, h: 200 },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
editor.createShapes(shapesToCreate)
|
||||||
|
})
|
||||||
|
.add(() => {
|
||||||
|
editor.getRenderingShapes()
|
||||||
|
})
|
||||||
|
.after(() => {
|
||||||
|
const shape = editor.getCurrentPageShapes()[0]
|
||||||
|
editor.updateShape({ ...shape, x: shape.x + 1 })
|
||||||
|
})
|
||||||
|
.run()
|
||||||
|
|
||||||
|
PerformanceMeasurer.Table(renderingShapes, renderingShapes2)
|
||||||
|
}, 10000)
|
||||||
|
})
|
Loading…
Reference in a new issue