-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* ♻️ 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
Showing
6 changed files
with
237 additions
and
70 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.