Skip to content

Commit

Permalink
⚡ Add benchmarks (#309)
Browse files Browse the repository at this point in the history
* ♻️ Refactor benchmark suite in order to add more tests

* ⚡ Add set prop benchmark

* ⚡ Add benchmark score

* ➕ Add seamless in benchmark

* ⚡ Add missing benchmarks
  • Loading branch information
nlepage authored Jul 3, 2018
1 parent 1220b9d commit 8801223
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 70 deletions.
11 changes: 6 additions & 5 deletions packages/immutadot-benchmark/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
"immutadot": "~2.0.0",
"jest": "~21.2.1",
"lerna": "~2.11.0",
"qim": "~0.0.52"
"qim": "~0.0.52",
"seamless-immutable": "^7.1.3"
},
"scripts": {
"prebenchmark": "lerna run --scope immutadot build",
"benchmark": "jest -i",
"prebenchmark:fast": "lerna run --scope immutadot build",
"benchmark:fast": "cross-env FAST=true jest -i",
"prestart": "lerna run --scope immutadot build",
"start": "jest -i",
"prefast": "lerna run --scope immutadot build",
"fast": "cross-env FAST=true jest -i",
"test": "echo No tests"
}
}
125 changes: 85 additions & 40 deletions packages/immutadot-benchmark/src/benchmark.js
Original file line number Diff line number Diff line change
@@ -1,56 +1,101 @@
export function createBenchmark(title, testResult, pMaxTime = 30, pMaxOperations = 1000) {
const fast = Boolean(process.env.FAST)

const fast = Boolean(process.env.FAST)
const maxTime = fast ? pMaxTime / 3 : pMaxTime
const maxOperations = fast ? Math.round(pMaxOperations / 3) : pMaxOperations
export class BenchmarkSuite {
constructor(reference, contestants) {
this.reference = reference
this.contestants = contestants
this.benchmarks = []
}

createBenchmark(title, testResult, pMaxTime = 30, pMaxOperations = 1000) {
const benchmark = {
title,
runs: {},
}
this.benchmarks.push(benchmark)

const maxTime = fast ? pMaxTime / 3 : pMaxTime
const maxOperations = fast ? Math.round(pMaxOperations / 3) : pMaxOperations

function run(key, operation) {
const startTime = Date.now()
const maxTimeMs = Math.round(maxTime * 1000)
const maxRunTime = Math.round(maxTimeMs / 10) // Max run time is a tenth of max time
const limitEndTime = startTime + maxTimeMs

let iterations = 1 // Start with 1 iteration
let nbOperations = 0
let totalTime = 0

const runs = []
while (iterations > 0) {
nbOperations += iterations

function run(key, opTitle, operation) {
const startTime = Date.now()
const maxTimeMs = Math.round(maxTime * 1000)
const maxRunTime = Math.round(maxTimeMs / 10) // Max run time is a tenth of max time
const limitEndTime = startTime + maxTimeMs
const runStartTime = Date.now()
while (iterations--) operation()
totalTime += Date.now() - runStartTime

let iterations = 1 // Start with 1 iteration
let nbOperations = 0
let totalTime = 0
const tempMeanTime = totalTime / nbOperations

while (iterations > 0) {
nbOperations += iterations
iterations = Math.min(
// Either enough operations to consume max run time or remaining time
Math.ceil(Math.min(maxRunTime, Math.max(limitEndTime - Date.now(), 0)) / tempMeanTime),
// Or enough operations to reach max operations
maxOperations - nbOperations,
)
}

const runStartTime = Date.now()
while (iterations--) operation()
totalTime += Date.now() - runStartTime
if (typeof testResult === 'function') testResult(key, operation())

const tempMeanTime = totalTime / nbOperations
benchmark.runs[key] = {
totalTime,
nbOperations,
}
}

return run
}

iterations = Math.min(
// Either enough operations to consume max run time or remaining time
Math.ceil(Math.min(maxRunTime, Math.max(limitEndTime - Date.now(), 0)) / tempMeanTime),
// Or enough operations to reach max operations
maxOperations - nbOperations,
)
printRun(run) {
if (run === undefined) return 'No run'
const { totalTime, nbOperations } = run
const opTime = totalTime / nbOperations

let formattedOpTime
if (opTime < 0.001) {
const nanoTime = opTime * 1000000
formattedOpTime = `${(nanoTime).toFixed(3 - Math.ceil(Math.log10(nanoTime)))}ns`
} else if (opTime < 1) {
const microTime = opTime * 1000
formattedOpTime = `${(microTime).toFixed(3 - Math.ceil(Math.log10(microTime)))}µs`
} else {
formattedOpTime = `${(opTime).toFixed(3 - Math.ceil(Math.log10(opTime)))}ms`
}

if (typeof testResult === 'function') testResult(key, operation())
return `${Math.round(nbOperations * 1000 / totalTime)}ops/s (${formattedOpTime}/op)`
}

runs.push({
title: opTitle,
totalTime,
nbOperations,
})
printBenchmark({ title, runs }) {
return `| ${title} | ${this.contestants.map(([key]) => runs[key]).map(run => this.printRun(run)).join(' | ')} |`
}

run.log = function() {
console.log( // eslint-disable-line no-console
`${title}:\n${
runs
.map(({ title, totalTime, nbOperations }) => ` ${title}: ~${Math.round(nbOperations * 1000 / totalTime)}ops/s (${(totalTime / nbOperations).toFixed(2)}ms/op) on ${nbOperations}ops`)
.join('\n')
}`,
)
printScore(key) {
if (key === this.reference) return 100
const scores = this.benchmarks.map(({ runs }) => {
const reference = runs[this.reference]
const run = runs[key]
if (run === undefined) return undefined
return run.nbOperations * 100 * reference.totalTime / run.totalTime / reference.nbOperations
}).filter(score => score !== undefined)
return Math.round(scores.reduce((sum, score) => sum + score, 0) / scores.length)
}

return run
log() {
// eslint-disable-next-line
console.log([
`| | ${this.contestants.map(([, title]) => title).join(' | ')} |`,
`| --- | ${this.contestants.map(() => '---').join(' | ')} |`,
this.benchmarks.map(benchmark => this.printBenchmark(benchmark)).join('\n'),
`| Final score | ${this.contestants.map(([key]) => this.printScore(key)).join(' | ')} |`,
].join('\n'))
}
}
25 changes: 25 additions & 0 deletions packages/immutadot-benchmark/src/benchmark.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/* eslint-env jest */
import { BenchmarkSuite } from './benchmark'
import { setProp } from './setProp'
import { updateTodos } from './updateTodos'

const benchmarkSuite = new BenchmarkSuite(
'es2015',
[
['es2015', 'ES2015 destructuring'],
['immutable', 'immutable 3.8.2'],
['seamless', 'seamless-immutable 7.1.3'],
['immer', 'immer 1.2.0'],
['qim', 'qim 0.0.52'],
['immutadot', 'immutad●t 2.0.0'],
],
)

describe('Benchmark suite', () => {
describe('Set a property', () => setProp(benchmarkSuite))
describe('Update small todos list', () => updateTodos(benchmarkSuite, 'Update small todos list (1000 items)', 1000, 100, 30, 50000))
describe('Update medium todos list', () => updateTodos(benchmarkSuite, 'Update medium todos list (10000 items)', 10000, 1000, 30, 5000))
describe('Update large todos list', () => updateTodos(benchmarkSuite, 'Update large todos list (100000 items)', 100000, 10000, 30, 500))

afterAll(() => benchmarkSuite.log())
})
86 changes: 86 additions & 0 deletions packages/immutadot-benchmark/src/setProp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* eslint-env jest */
import * as qim from 'qim'
import immer, { setAutoFreeze } from 'immer'
import Immutable from 'immutable'
import Seamless from 'seamless-immutable/seamless-immutable.production.min'
import { set } from 'immutadot/core'

export function setProp(benchmarkSuite) {
// Prepare base state
const baseState = {
nested: {
prop: 'foo',
otherProp: 'aze',
},
other: { prop: 'baz' },
}

// Prepare immutable state
const immutableState = Immutable.fromJS(baseState)

// Prepare seamless state
const seamlessState = Seamless.from(baseState)

// Disable immer auto freeze
setAutoFreeze(false)

const benchmark = benchmarkSuite.createBenchmark(
'Set a property',
(key, result) => {
if (key === 'immutable') return
expect(result).toEqual({
nested: {
prop: 'bar',
otherProp: 'aze',
},
other: { prop: 'baz' },
})
},
10,
5000000,
)

it('es2015', () => {
benchmark('es2015', () => {
return {
...baseState,
nested: {
...baseState.nested,
prop: 'bar',
},
}
})
})

it('immutable', () => {
benchmark('immutable', () => {
immutableState.setIn(['nested', 'prop'], 'bar')
})
})

it('seamless', () => {
benchmark('seamless', () => {
return Seamless.setIn(seamlessState, ['nested', 'prop'], 'bar')
})
})

it('immer', () => {
benchmark('immer', () => {
return immer(baseState, draft => {
draft.nested.prop = 'bar'
})
})
})

it('qim', () => {
benchmark('qim', () => {
return qim.set(['nested', 'prop'], 'bar', baseState)
})
})

it('immutad●t', () => {
benchmark('immutadot', () => {
return set(baseState, 'nested.prop', 'bar')
})
})
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
/* eslint-env jest */
import { $each, $slice, set as qimSet } from 'qim'

import { List, Record } from 'immutable'

import * as qim from 'qim'
import immer, { setAutoFreeze } from 'immer'

import { createBenchmark } from './benchmark'

import Immutable from 'immutable'
import Seamless from 'seamless-immutable/seamless-immutable.production.min'
import { set } from 'immutadot/core'

function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) {
export function updateTodos(benchmarkSuite, title, listSize, modifySize, maxTime, maxOperations) {
// Prepare base state
const baseState = []
for (let i = 0; i < listSize; i++) {
Expand All @@ -21,12 +17,15 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) {
}

// Prepare immutable state
const todoRecord = Record({
const todoRecord = Immutable.Record({
todo: '',
done: false,
someThingCompletelyIrrelevant: [],
})
const immutableState = List(baseState.map(todo => todoRecord(todo)))
const immutableState = Immutable.List(baseState.map(todo => todoRecord(todo)))

// Prepare seamless state
const seamlessState = Seamless.from(baseState)

// Disable immer auto freeze
setAutoFreeze(false)
Expand All @@ -37,7 +36,7 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) {
return [start, start + modifySize]
}

const benchmark = createBenchmark(
const benchmark = benchmarkSuite.createBenchmark(
title,
(key, result) => {
if (key === 'immutable') return
Expand All @@ -54,8 +53,8 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) {
maxOperations,
)

it('ES2015', () => {
benchmark('es2015', 'ES2015 destructuring', () => {
it('es2015', () => {
benchmark('es2015', () => {
const [start, end] = randomBounds()
return baseState
.slice(0, start)
Expand All @@ -71,16 +70,29 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) {
})

it('immutable', () => {
benchmark('immutable', 'immutable 3.8.2 (w/o conversion to plain JS objects)', () => {
benchmark('immutable', () => {
const [start, end] = randomBounds()
immutableState.withMutations(state => {
for (let i = start; i < end; i++) state.setIn([i, 'done'], true)
})
})
})

it('immer proxy', () => {
benchmark('immer-proxy', 'immer 1.2.0 (proxy implementation w/o autofreeze)', () => {
it('seamless', () => {
benchmark('seamless', () => {
const [start, end] = randomBounds()
return seamlessState
.slice(0, start)
.concat(
seamlessState.slice(start, end)
.map(todo => todo.set('done', true)),
seamlessState.slice(end),
)
})
})

it('immer', () => {
benchmark('immer', () => {
const [start, end] = randomBounds()
return immer(baseState, draft => {
for (let i = start; i < end; i++) draft[i].done = true
Expand All @@ -89,22 +101,16 @@ function updateTodosList(title, listSize, modifySize, maxTime, maxOperations) {
})

it('qim', () => {
benchmark('qim', 'qim 0.0.52', () => {
benchmark('qim', () => {
const [start, end] = randomBounds()
return qimSet([$slice(start, end), $each, 'done'], true, baseState)
return qim.set([qim.$slice(start, end), qim.$each, 'done'], true, baseState)
})
})

it('immutad●t', () => {
benchmark('immutadot', 'immutad●t 2.0.0', () => {
benchmark('immutadot', () => {
const [start, end] = randomBounds()
return set(baseState, `[${start}:${end}].done`, true)
})
})

afterAll(benchmark.log)
}

describe('Update small todos list', () => updateTodosList('Update small todos list (1000 items)', 1000, 100, 30, 50000))
describe('Update medium todos list', () => updateTodosList('Update medium todos list (10000 items)', 10000, 1000, 30, 5000))
describe('Update large todos list', () => updateTodosList('Update large todos list (100000 items)', 100000, 10000, 30, 500))
Loading

0 comments on commit 8801223

Please sign in to comment.