diff --git a/.babelrc b/.babelrc index c6708204e..2c99e8cfd 100644 --- a/.babelrc +++ b/.babelrc @@ -23,7 +23,19 @@ }, "test": { "presets": [ - "env", + ["env", { + "exclude": ["transform-regenerator"] + }], + "react", + "stage-0" + ], + "plugins": ["transform-runtime"] + }, + "benchmark": { + "presets": [ + ["env", { + "exclude": ["transform-regenerator"] + }], "react", "stage-0" ], diff --git a/.gitignore b/.gitignore index 31a608d64..94b19ed8a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,6 @@ packages/*/yarn.lock .DS_Store .idea/ .vscode/ + +# Editor files +.tern-port diff --git a/Contributing.md b/Contributing.md index eb9fcb728..17a3a9f68 100644 --- a/Contributing.md +++ b/Contributing.md @@ -90,6 +90,16 @@ yarn benchmark There will be some subtle changes in iteration speed always, but the comparison reporter will highlight any changes that seem meaningful. You can run `benchmark` multiple times to ensure the speed up persists. +### Run Selected Benchmarks + +To run selected benchmarks, create `tmp/benchmark-config.js` with `module.exports.include`. For example, to run slate-core benchmarks only with `get-*`, we can create a `tmp/benchmark-config.js` as + +``` +module.exports.include = { + slate: /^get/ +} +``` + ## Adding Browser Support Slate aims to targeted all of the modern browsers, and eventually the modern mobile platforms. Right now browser support is limited to the latest versions of [Chrome](https://www.google.com/chrome/browser/desktop/), [Firefox](https://www.mozilla.org/en-US/firefox/new/), and [Safari](http://www.apple.com/safari/), but if you are interested in adding support for another modern platform, that is welcomed! diff --git a/benchmark/compare.js b/benchmark/compare.js new file mode 100644 index 000000000..7a9e87c20 --- /dev/null +++ b/benchmark/compare.js @@ -0,0 +1,115 @@ +/* eslint-disable no-console */ + +const chalk = require('chalk') +const figures = require('figures') +const emojis = require('emojis') +const { resolve } = require('path') + +const baseline = require(resolve(process.cwd(), 'tmp/benchmark-baseline')) +const comparison = require(resolve(process.cwd(), 'tmp/benchmark-comparison')) +const { existsSync } = require('fs') + +/** + * Constants. + */ + +let THRESHOLD = 0.333 +const configPath = '../../tmp/benchmark-config.js' + +if (existsSync(configPath)) { + const alternative = require(configPath).THRESHOLD + + if (typeof alternative === 'number' && alternative > 0) { + THRESHOLD = alternative + } +} + +/** + * Print. + */ + +console.log() +console.log(` benchmarks`) + +baseline.forEach((suite, i) => { + console.log(` ${suite.name}`) + + suite.benchmarks.forEach((base, j) => { + const compared = { user: {}, hr: {} } + + for (const key of Object.keys(compared)) { + const comp = comparison[i].benchmarks[j] + if (!comp) return + const b = base.iterations / base[key] * 1000 + const c = comp.iterations / comp[key] * 1000 + const balancePercent = + b > c ? Math.round(Math.abs(b - c) / c * 100) : (c - b) / b * 100 + + const output = `${b.toFixed(2)} -> ${c.toFixed(2)} ops/sec` + compared[key].baseOutput = output + + compared[key].percentOutput = `${balancePercent.toFixed(2)}% ${ + c > b ? 'faster' : 'slower' + }` + + compared[key].percentValue = balancePercent + compared[key].b = b + compared[key].c = c + compared[key].isFaster = c > b + + if (balancePercent > 1000) { + compared[key].percentOutput += emojis.unicode(' :scream: ') + } else if (balancePercent > 100) { + if (c > b) { + compared[key].percentOutput += emojis.unicode(' :raised_hands: ') + } else { + compared[key].percentOutput += emojis.unicode(' :worried: ') + } + } + } + + const { user, hr } = compared + + if ( + user.percentValue < THRESHOLD * 100 && + hr.percentValue < THRESHOLD * 100 + ) { + console.log( + chalk.grey( + ` ${figures.tick} ${base.name}: ${user.baseOutput} (${ + user.percentOutput + })` + ) + ) + return + } + + if (user.isFaster === hr.isFaster) { + if (user.isFaster) { + console.log(chalk.green(` ${figures.star} ${base.name}:`)) + + console.log( + ` user: ${user.baseOutput} (${user.percentOutput})` + ) + + console.log(` real: ${hr.baseOutput} (${hr.percentOutput})`) + return + } + + console.log(chalk.red(` ${figures.cross} ${base.name}:`)) + + console.log( + ` user: ${user.baseOutput} (${user.percentOutput})` + ) + + console.log(` real: ${hr.baseOutput} (${hr.percentOutput})`) + return + } + + console.log(chalk.red(` ${figures.questionMarkPrefix} ${base.name}:`)) + console.log(` user: ${user.baseOutput} (${user.percentOutput})`) + console.log(` real: ${hr.baseOutput} (${hr.percentOutput})`) + }) +}) + +console.log() diff --git a/benchmark/config.js b/benchmark/config.js new file mode 100644 index 000000000..3179a415c --- /dev/null +++ b/benchmark/config.js @@ -0,0 +1,39 @@ +const { resolve } = require('path') +const { existsSync } = require('fs') +const program = require('commander') + +program + .option('-g, --grep []', 'Add grep pattern to filter running benchmarks') + .option('-c, --config [file]', 'Add config to filter running benchmarks') + .parse(process.argv) + +const { grep } = program + +if (grep) { + const pattern = new RegExp(grep) + + module.exports.include = { + slate: pattern, + 'slate-html-serializer': pattern, + 'slate-plain-serializer': pattern, + 'slate-react': pattern, + } +} else { + let { config = 'tmp/benchmark-config.js' } = program + config = resolve(config) + + const userConfig = existsSync(config) ? require(config) : {} + + if (userConfig.include) { + module.exports.include = userConfig.include + } else if (userConfig.default) { + module.exports.inlcude = userConfig.default + } else { + module.exports.include = { + slate: /^/, + 'slate-html-serializer': /^/, + 'slate-plain-serializer': /^/, + 'slate-react': /^/, + } + } +} diff --git a/benchmark/generate-report.js b/benchmark/generate-report.js new file mode 100644 index 000000000..aac443d4a --- /dev/null +++ b/benchmark/generate-report.js @@ -0,0 +1,57 @@ +const { writeFileSync } = require('fs') + +function convertRepo(report) { + const result = [] + + for (const name in report) { + const suite = report[name] + + result.push({ + name, + type: 'suite', + benchmarks: convertSuite(suite), + }) + } + return result +} + +function convertSuite(suite) { + const result = [] + + for (const name in suite) { + const bench = suite[name] + const { user, cycles } = bench + + result.push({ + name, + type: 'bench', + elapsed: user, + iterations: cycles, + ops: 1000 * cycles / user, + ...bench, + }) + } + return result +} + +const IS_COMPARE = process.env.COMPARE +const filePath = IS_COMPARE + ? './tmp/benchmark-comparison.json' + : './tmp/benchmark-baseline.json' + +function generateReport(repo) { + repo + .run() + .then(report => { + const data = JSON.stringify(convertRepo(report)) + writeFileSync(filePath, data) + return report + }) + .then(report => { + if (IS_COMPARE) { + require('./compare') + } + }) +} + +module.exports = { generateReport } diff --git a/benchmark/helpers/h.js b/benchmark/helpers/h.js new file mode 100644 index 000000000..14e7f67c9 --- /dev/null +++ b/benchmark/helpers/h.js @@ -0,0 +1,45 @@ +/* eslint-disable import/no-extraneous-dependencies */ +const { createHyperscript } = require('slate-hyperscript') + +/** + * Define a hyperscript. + * + * @type {Function} + */ + +const h = createHyperscript({ + blocks: { + line: 'line', + paragraph: 'paragraph', + quote: 'quote', + code: 'code', + list: 'list', + item: 'item', + image: { + type: 'image', + isVoid: true, + }, + }, + inlines: { + link: 'link', + hashtag: 'hashtag', + comment: 'comment', + emoji: { + type: 'emoji', + isVoid: true, + }, + }, + marks: { + b: 'bold', + i: 'italic', + u: 'underline', + }, +}) + +/** + * Export. + * + * @type {Function} + */ + +module.exports = h diff --git a/benchmark/index.js b/benchmark/index.js new file mode 100644 index 000000000..4c02159eb --- /dev/null +++ b/benchmark/index.js @@ -0,0 +1,20 @@ +const { repo } = require('slate-dev-benchmark') +const { resolve } = require('path') +const { readdirSync } = require('fs') +const { generateReport } = require('./generate-report') +const { include } = require('./config') + +const categoryDir = resolve(__dirname) + +const categories = readdirSync(categoryDir).filter( + c => c[0] != '.' && c.match(/^slate/) +) + +categories.forEach(dir => { + if (include && include[dir]) { + const { run } = require(`./${dir}`) + run(include[dir]) + } +}) + +generateReport(repo) diff --git a/benchmark/slate-html-serializer/html-serializer/deserialize.js b/benchmark/slate-html-serializer/html-serializer/deserialize.js new file mode 100644 index 000000000..43c3e1ce0 --- /dev/null +++ b/benchmark/slate-html-serializer/html-serializer/deserialize.js @@ -0,0 +1,58 @@ +/** @jsx h */ +/* eslint-disable react/jsx-key */ + +const Html = require('slate-html-serializer').default +const { JSDOM } = require('jsdom') // eslint-disable-line import/no-extraneous-dependencies + +const html = new Html({ + parseHtml: JSDOM.fragment, + rules: [ + { + deserialize(el, next) { + switch (el.tagName.toLowerCase()) { + case 'blockquote': + return { + object: 'block', + type: 'quote', + nodes: next(el.childNodes), + } + case 'p': { + return { + object: 'block', + type: 'paragraph', + nodes: next(el.childNodes), + } + } + case 'strong': { + return { + object: 'mark', + type: 'bold', + nodes: next(el.childNodes), + } + } + case 'em': { + return { + object: 'mark', + type: 'italic', + nodes: next(el.childNodes), + } + } + } + }, + }, + ], +}) + +module.exports.default = function(string) { + html.deserialize(string) +} + +module.exports.input = ` +
+

+ This is editable rich text, much better than a textarea! +

+
+` + .trim() + .repeat(10) diff --git a/packages/slate-html-serializer/benchmark/html-serializer/serialize.js b/benchmark/slate-html-serializer/html-serializer/serialize.js similarity index 79% rename from packages/slate-html-serializer/benchmark/html-serializer/serialize.js rename to benchmark/slate-html-serializer/html-serializer/serialize.js index b0a91ec4c..f2d3ea76a 100644 --- a/packages/slate-html-serializer/benchmark/html-serializer/serialize.js +++ b/benchmark/slate-html-serializer/html-serializer/serialize.js @@ -1,10 +1,10 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import Html from '../..' -import React from 'react' -import h from '../../test/helpers/h' -import { JSDOM } from 'jsdom' // eslint-disable-line import/no-extraneous-dependencies +const Html = require('slate-html-serializer').default +const React = require('react') +const h = require('../../helpers/h') +const { JSDOM } = require('jsdom') // eslint-disable-line import/no-extraneous-dependencies const html = new Html({ parseHtml: JSDOM.fragment, @@ -34,11 +34,11 @@ const html = new Html({ ], }) -export default function(state) { +module.exports.default = function(state) { html.serialize(state) } -export const input = ( +module.exports.input = ( {Array.from(Array(10)).map(() => ( diff --git a/benchmark/slate-html-serializer/index.js b/benchmark/slate-html-serializer/index.js new file mode 100644 index 000000000..84255e1ba --- /dev/null +++ b/benchmark/slate-html-serializer/index.js @@ -0,0 +1,37 @@ +const { readdirSync } = require('fs') +const { basename, extname, resolve } = require('path') +const { resetMemoization } = require('slate') +const { Suite, Bench } = require('slate-dev-benchmark') + +/** + * Benchmarks. + */ + +module.exports.run = function(include) { + const categoryDir = resolve(__dirname) + const categories = readdirSync(categoryDir).filter( + c => c[0] != '.' && c != 'index.js' + ) + + categories.forEach(category => { + const suite = new Suite(category, { minTries: 100, minTime: 1000 }) + const benchmarkDir = resolve(categoryDir, category) + const benchmarks = readdirSync(benchmarkDir) + .filter(b => b[0] != '.' && !!~b.indexOf('.js')) + .map(b => basename(b, extname(b))) + + benchmarks.forEach(benchmark => { + if (include && !benchmark.match(include)) return + const bench = new Bench(suite, benchmark) + const dir = resolve(benchmarkDir, benchmark) + const module = require(dir) + const fn = module.default + bench.input(() => module.input) + + bench.run(input => { + fn(input) + resetMemoization() + }) + }) + }) +} diff --git a/benchmark/slate-plain-serializer/index.js b/benchmark/slate-plain-serializer/index.js new file mode 100644 index 000000000..1958e5814 --- /dev/null +++ b/benchmark/slate-plain-serializer/index.js @@ -0,0 +1,40 @@ +const { basename, extname, resolve } = require('path') +const { readdirSync } = require('fs') +const { resetMemoization } = require('slate') +const { Suite, Bench } = require('slate-dev-benchmark') + +/** + * Benchmarks. + */ + +module.exports.run = function(include) { + const categoryDir = resolve(__dirname) + const categories = readdirSync(categoryDir).filter( + c => c[0] != '.' && c != 'index.js' + ) + + categories.forEach(category => { + const suite = new Suite(category, { + minTries: 100, + minTime: 1000, + }) + const benchmarkDir = resolve(categoryDir, category) + const benchmarks = readdirSync(benchmarkDir) + .filter(b => b[0] != '.' && !!~b.indexOf('.js')) + .map(b => basename(b, extname(b))) + + benchmarks.forEach(benchmark => { + if (include && !benchmark.match(include)) return + const bench = new Bench(suite, benchmark) + const dir = resolve(benchmarkDir, benchmark) + const module = require(dir) + const fn = module.default + bench.input(() => module.input) + + bench.run(input => { + fn(input) + resetMemoization() + }) + }) + }) +} diff --git a/packages/slate-plain-serializer/benchmark/plain-serializer/deserialize.js b/benchmark/slate-plain-serializer/plain-serializer/deserialize.js similarity index 52% rename from packages/slate-plain-serializer/benchmark/plain-serializer/deserialize.js rename to benchmark/slate-plain-serializer/plain-serializer/deserialize.js index cdcab1bf9..d8e4a724a 100644 --- a/packages/slate-plain-serializer/benchmark/plain-serializer/deserialize.js +++ b/benchmark/slate-plain-serializer/plain-serializer/deserialize.js @@ -1,14 +1,16 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import Plain from '../..' +const Plain = require('slate-plain-serializer').default -export default function(string) { - Plain.deserialize(string) -} - -export const input = ` +const input = ` This is editable plain text, just like a text area. ` .trim() .repeat(10) + +module.exports.input = input + +module.exports.default = function(string) { + Plain.deserialize(string) +} diff --git a/packages/slate-plain-serializer/benchmark/plain-serializer/serialize.js b/benchmark/slate-plain-serializer/plain-serializer/serialize.js similarity index 71% rename from packages/slate-plain-serializer/benchmark/plain-serializer/serialize.js rename to benchmark/slate-plain-serializer/plain-serializer/serialize.js index c1f2a60fe..eb70eab30 100644 --- a/packages/slate-plain-serializer/benchmark/plain-serializer/serialize.js +++ b/benchmark/slate-plain-serializer/plain-serializer/serialize.js @@ -1,14 +1,14 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import Plain from '../..' -import h from '../../test/helpers/h' +const Plain = require('slate-plain-serializer').default +const h = require('../../helpers/h') -export default function(state) { +module.exports.default = function(state) { Plain.serialize(state) } -export const input = ( +module.exports.input = ( {Array.from(Array(10)).map(() => ( diff --git a/benchmark/slate-react/index.js b/benchmark/slate-react/index.js new file mode 100644 index 000000000..1958e5814 --- /dev/null +++ b/benchmark/slate-react/index.js @@ -0,0 +1,40 @@ +const { basename, extname, resolve } = require('path') +const { readdirSync } = require('fs') +const { resetMemoization } = require('slate') +const { Suite, Bench } = require('slate-dev-benchmark') + +/** + * Benchmarks. + */ + +module.exports.run = function(include) { + const categoryDir = resolve(__dirname) + const categories = readdirSync(categoryDir).filter( + c => c[0] != '.' && c != 'index.js' + ) + + categories.forEach(category => { + const suite = new Suite(category, { + minTries: 100, + minTime: 1000, + }) + const benchmarkDir = resolve(categoryDir, category) + const benchmarks = readdirSync(benchmarkDir) + .filter(b => b[0] != '.' && !!~b.indexOf('.js')) + .map(b => basename(b, extname(b))) + + benchmarks.forEach(benchmark => { + if (include && !benchmark.match(include)) return + const bench = new Bench(suite, benchmark) + const dir = resolve(benchmarkDir, benchmark) + const module = require(dir) + const fn = module.default + bench.input(() => module.input) + + bench.run(input => { + fn(input) + resetMemoization() + }) + }) + }) +} diff --git a/benchmark/slate-react/rendering/decoration.js b/benchmark/slate-react/rendering/decoration.js new file mode 100644 index 000000000..40cd4651f --- /dev/null +++ b/benchmark/slate-react/rendering/decoration.js @@ -0,0 +1,46 @@ +/** @jsx h */ +/* eslint-disable react/jsx-key */ + +const React = require('react') +const ReactDOM = require('react-dom/server') +const h = require('../../helpers/h') +const { Editor } = require('slate-react') + +module.exports.default = function(value) { + const el = React.createElement(Editor, { value }) + ReactDOM.renderToStaticMarkup(el) +} + +const value = ( + + + {Array.from(Array(10)).map(() => ( + + + + This is editable rich text, much better than a + textarea! + + + + ))} + + +) + +const texts = value.document.getTexts() +const decorations = texts.flatMap((t, index) => { + if (index % 4 !== 0) return [] + if (t.length === 0) return [] + return [ + { + anchorKey: t.key, + anchorOffset: 0, + focusKey: t.key, + focusOffset: 1, + marks: [{ type: 'underline' }], + }, + ] +}) + +module.exports.input = value.change().setValue({ decorations }).value diff --git a/packages/slate-react/benchmark/rendering/normal.js b/benchmark/slate-react/rendering/normal.js similarity index 67% rename from packages/slate-react/benchmark/rendering/normal.js rename to benchmark/slate-react/rendering/normal.js index 2dda5dc3b..42d9244b9 100644 --- a/packages/slate-react/benchmark/rendering/normal.js +++ b/benchmark/slate-react/rendering/normal.js @@ -1,17 +1,17 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import React from 'react' -import ReactDOM from 'react-dom/server' -import h from '../../test/helpers/h' -import { Editor } from '../..' +const React = require('react') +const ReactDOM = require('react-dom/server') +const h = require('../../helpers/h') +const { Editor } = require('slate-react') -export default function(value) { +module.exports.default = function(value) { const el = React.createElement(Editor, { value }) ReactDOM.renderToStaticMarkup(el) } -export const input = ( +module.exports.input = ( {Array.from(Array(10)).map(() => ( diff --git a/packages/slate/benchmark/changes/delete-backward.js b/benchmark/slate/changes/delete-backward.js similarity index 72% rename from packages/slate/benchmark/changes/delete-backward.js rename to benchmark/slate/changes/delete-backward.js index 5a698882c..db67adf99 100644 --- a/packages/slate/benchmark/changes/delete-backward.js +++ b/benchmark/slate/changes/delete-backward.js @@ -1,18 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(change) { +module.exports.default = function(change) { change.deleteBackward() } -export function before(value) { - const change = value.change() - return change -} - -export const input = ( +const value = ( {Array.from(Array(10)).map((v, i) => ( @@ -29,3 +24,7 @@ export const input = ( ) + +module.exports.input = () => { + return value.change() +} diff --git a/packages/slate/benchmark/changes/delete-forward.js b/benchmark/slate/changes/delete-forward.js similarity index 72% rename from packages/slate/benchmark/changes/delete-forward.js rename to benchmark/slate/changes/delete-forward.js index a5fe3e995..fb1d665da 100644 --- a/packages/slate/benchmark/changes/delete-forward.js +++ b/benchmark/slate/changes/delete-forward.js @@ -1,18 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(change) { +module.exports.default = function(change) { change.deleteForward() } -export function before(value) { - const change = value.change() - return change -} - -export const input = ( +const value = ( {Array.from(Array(10)).map((v, i) => ( @@ -29,3 +24,7 @@ export const input = ( ) + +module.exports.input = () => { + return value.change() +} diff --git a/benchmark/slate/changes/insert-node-by-key.js b/benchmark/slate/changes/insert-node-by-key.js new file mode 100644 index 000000000..68642148e --- /dev/null +++ b/benchmark/slate/changes/insert-node-by-key.js @@ -0,0 +1,32 @@ +/** @jsx h */ +/* eslint-disable react/jsx-key */ + +const h = require('../../helpers/h') + +module.exports.default = function({ change, block }) { + change.insertNodeByKey(block.key, 0, Hello world) +} + +const value = ( + + + {Array.from(Array(10)).map((v, i) => ( + + + + This is editable rich text, much better than a + textarea! + {i == 0 ? : ''} + + + + ))} + + +) +const block = value.document.getBlocks().last() + +module.exports.input = function() { + const change = value.change() + return { change, block } +} diff --git a/packages/slate/benchmark/changes/insert-text-by-key-multiple.js b/benchmark/slate/changes/insert-text-by-key-multiple.js similarity index 66% rename from packages/slate/benchmark/changes/insert-text-by-key-multiple.js rename to benchmark/slate/changes/insert-text-by-key-multiple.js index 8fff7ce6e..9817670b0 100644 --- a/packages/slate/benchmark/changes/insert-text-by-key-multiple.js +++ b/benchmark/slate/changes/insert-text-by-key-multiple.js @@ -1,26 +1,15 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' -import { resetMemoization } from '../..' +const h = require('../../helpers/h') -export default function({ change, keys }) { +module.exports.default = function({ change, keys }) { for (const key of keys) { change.insertTextByKey(key, 0, 'a') } } -export function before(value) { - const change = value.change() - const keys = value.document - .getTexts() - .toArray() - .map(t => t.key) - resetMemoization() - return { change, keys } -} - -export const input = ( +const value = ( {Array.from(Array(10)).map((v, i) => ( @@ -37,3 +26,12 @@ export const input = ( ) +const keys = value.document + .getTexts() + .toArray() + .map(t => t.key) + +module.exports.input = function() { + const change = value.change() + return { change, keys } +} diff --git a/packages/slate/benchmark/changes/insert-text-by-key.js b/benchmark/slate/changes/insert-text-by-key.js similarity index 68% rename from packages/slate/benchmark/changes/insert-text-by-key.js rename to benchmark/slate/changes/insert-text-by-key.js index 807d07667..f0d975091 100644 --- a/packages/slate/benchmark/changes/insert-text-by-key.js +++ b/benchmark/slate/changes/insert-text-by-key.js @@ -1,21 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' -import { resetMemoization } from '../..' +const h = require('../../helpers/h') -export default function({ change, text }) { +module.exports.default = function({ change, text }) { change.insertTextByKey(text.key, 0, 'a') } -export function before(value) { - const change = value.change() - const text = value.document.getLastText() - resetMemoization() - return { change, text } -} - -export const input = ( +const value = ( {Array.from(Array(10)).map((v, i) => ( @@ -32,3 +24,9 @@ export const input = ( ) +const text = value.document.getLastText() + +module.exports.input = function() { + const change = value.change() + return { change, text } +} diff --git a/packages/slate/benchmark/changes/insert-text.js b/benchmark/slate/changes/insert-text.js similarity index 72% rename from packages/slate/benchmark/changes/insert-text.js rename to benchmark/slate/changes/insert-text.js index 66e3ea0f8..58a00f604 100644 --- a/packages/slate/benchmark/changes/insert-text.js +++ b/benchmark/slate/changes/insert-text.js @@ -1,18 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(change) { +module.exports.default = function(change) { change.insertText('a') } -export function before(value) { - const change = value.change() - return change -} - -export const input = ( +const value = ( {Array.from(Array(10)).map((v, i) => ( @@ -29,3 +24,7 @@ export const input = ( ) + +module.exports.input = function() { + return value.change() +} diff --git a/packages/slate/benchmark/changes/normalize.js b/benchmark/slate/changes/normalize.js similarity index 78% rename from packages/slate/benchmark/changes/normalize.js rename to benchmark/slate/changes/normalize.js index 2497fee16..a16d6a09c 100644 --- a/packages/slate/benchmark/changes/normalize.js +++ b/benchmark/slate/changes/normalize.js @@ -1,17 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(change) { +module.exports.default = function(change) { change.normalize() } -export function before(value) { - return value.change() -} - -export const input = ( +const value = ( {Array.from(Array(10)).map((v, i) => ( @@ -28,3 +24,7 @@ export const input = ( ) + +module.exports.input = function() { + return value.change() +} diff --git a/benchmark/slate/changes/remove-node-by-key.js b/benchmark/slate/changes/remove-node-by-key.js new file mode 100644 index 000000000..a9c316637 --- /dev/null +++ b/benchmark/slate/changes/remove-node-by-key.js @@ -0,0 +1,32 @@ +/** @jsx h */ +/* eslint-disable react/jsx-key */ + +const h = require('../../helpers/h') + +module.exports.default = function({ change, text }) { + change.removeNodeByKey(text.key) +} + +const value = ( + + + {Array.from(Array(10)).map((v, i) => ( + + + + This is editable rich text, much better than a + textarea! + {i == 0 ? : ''} + + + + ))} + + +) +const text = value.document.getLastText() + +module.exports.input = function() { + const change = value.change() + return { change, text } +} diff --git a/packages/slate/benchmark/changes/split-block.js b/benchmark/slate/changes/split-block.js similarity index 72% rename from packages/slate/benchmark/changes/split-block.js rename to benchmark/slate/changes/split-block.js index 060568690..b125d2e32 100644 --- a/packages/slate/benchmark/changes/split-block.js +++ b/benchmark/slate/changes/split-block.js @@ -1,18 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(change) { +module.exports.default = function(change) { change.splitBlock() } -export function before(value) { - const change = value.change() - return change -} - -export const input = ( +const value = ( {Array.from(Array(10)).map((v, i) => ( @@ -29,3 +24,7 @@ export const input = ( ) + +module.exports.input = function() { + return value.change() +} diff --git a/benchmark/slate/index.js b/benchmark/slate/index.js new file mode 100644 index 000000000..3f6c47844 --- /dev/null +++ b/benchmark/slate/index.js @@ -0,0 +1,37 @@ +const { readdirSync } = require('fs') +const { basename, extname, resolve } = require('path') +const { resetMemoization } = require('slate') +const { Suite, Bench } = require('slate-dev-benchmark') + +/** + * Benchmarks. + */ + +module.exports.run = function(include) { + const categoryDir = resolve(__dirname) + const categories = readdirSync(categoryDir).filter( + c => c[0] != '.' && c != 'index.js' + ) + + categories.forEach(category => { + const suite = new Suite(category, { minTries: 100, minTime: 1000 }) + const benchmarkDir = resolve(categoryDir, category) + const benchmarks = readdirSync(benchmarkDir) + .filter(b => b[0] != '.' && !!~b.indexOf('.js')) + .map(b => basename(b, extname(b))) + + benchmarks.forEach(benchmark => { + if (include && !benchmark.match(include)) return + const bench = new Bench(suite, benchmark) + const dir = resolve(benchmarkDir, benchmark) + const module = require(dir) + const fn = module.default + bench.input(module.input) + + bench.run(input => { + fn(input) + resetMemoization() + }) + }) + }) +} diff --git a/packages/slate/benchmark/models/from-json-big.js b/benchmark/slate/models/from-json-big.js similarity index 89% rename from packages/slate/benchmark/models/from-json-big.js rename to benchmark/slate/models/from-json-big.js index a3d902c8a..d5bd1ccfa 100644 --- a/packages/slate/benchmark/models/from-json-big.js +++ b/benchmark/slate/models/from-json-big.js @@ -1,12 +1,12 @@ /* eslint-disable react/jsx-key */ -import { Value } from '../..' +const { Value } = require('slate') -export default function(json) { +module.exports.default = function(json) { Value.fromJSON(json) } -export const input = { +const input = { document: { nodes: Array.from(Array(100)).map(() => ({ type: 'list', @@ -64,3 +64,7 @@ export const input = { })), }, } + +module.exports.input = function() { + return input +} diff --git a/packages/slate/benchmark/models/from-json.js b/benchmark/slate/models/from-json.js similarity index 85% rename from packages/slate/benchmark/models/from-json.js rename to benchmark/slate/models/from-json.js index 661773253..71b2c21ce 100644 --- a/packages/slate/benchmark/models/from-json.js +++ b/benchmark/slate/models/from-json.js @@ -1,12 +1,12 @@ /* eslint-disable react/jsx-key */ -import { Value } from '../..' +const { Value } = require('slate') -export default function(json) { +module.exports.default = function(json) { Value.fromJSON(json) } -export const input = { +const input = { document: { nodes: Array.from(Array(10)).map(() => ({ object: 'block', @@ -44,3 +44,7 @@ export const input = { })), }, } + +module.exports.input = function() { + return input +} diff --git a/packages/slate/benchmark/models/get-characters-at-range.js b/benchmark/slate/models/get-active-marks-at-range.js similarity index 62% rename from packages/slate/benchmark/models/get-characters-at-range.js rename to benchmark/slate/models/get-active-marks-at-range.js index 4c34adae7..057f1ced7 100644 --- a/packages/slate/benchmark/models/get-characters-at-range.js +++ b/benchmark/slate/models/get-active-marks-at-range.js @@ -1,17 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { - value.document.getCharactersAtRange(value.selection) +module.exports.default = function(value) { + value.document.getActiveMarksAtRange(value.selection) } -export function before(value) { - return value.change().selectAll().value -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -27,3 +23,9 @@ export const input = ( ) + .change() + .selectAll().value + +module.exports.input = function() { + return value +} diff --git a/benchmark/slate/models/get-ancestors.js b/benchmark/slate/models/get-ancestors.js new file mode 100644 index 000000000..fbb39c743 --- /dev/null +++ b/benchmark/slate/models/get-ancestors.js @@ -0,0 +1,30 @@ +/** @jsx h */ +/* eslint-disable react/jsx-key */ + +const h = require('../../helpers/h') + +module.exports.default = function({ value, text }) { + value.document.getAncestors(text.key) +} + +const value = ( + + + {Array.from(Array(10)).map(() => ( + + + + This is editable rich text, much better than a + textarea! + + + + ))} + + +) +const text = value.document.getLastText() + +module.exports.input = function() { + return { value, text } +} diff --git a/packages/slate/benchmark/models/get-blocks-at-range.js b/benchmark/slate/models/get-blocks-at-range.js similarity index 71% rename from packages/slate/benchmark/models/get-blocks-at-range.js rename to benchmark/slate/models/get-blocks-at-range.js index d07561873..ccbcdfe1c 100644 --- a/packages/slate/benchmark/models/get-blocks-at-range.js +++ b/benchmark/slate/models/get-blocks-at-range.js @@ -1,17 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.document.getBlocksAtRange(value.selection) } -export function before(value) { - return value.change().selectAll().value -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -27,3 +23,9 @@ export const input = ( ) + .change() + .selectAll().value + +module.exports.input = () => { + return value +} diff --git a/packages/slate/benchmark/models/get-blocks.js b/benchmark/slate/models/get-blocks.js similarity index 73% rename from packages/slate/benchmark/models/get-blocks.js rename to benchmark/slate/models/get-blocks.js index dfe4ddfe1..dca5c8929 100644 --- a/packages/slate/benchmark/models/get-blocks.js +++ b/benchmark/slate/models/get-blocks.js @@ -1,13 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.document.getBlocks() } -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -23,3 +23,7 @@ export const input = ( ) + +module.exports.input = function() { + return value +} diff --git a/packages/slate/benchmark/models/get-leaves.js b/benchmark/slate/models/get-common-ancestor.js similarity index 54% rename from packages/slate/benchmark/models/get-leaves.js rename to benchmark/slate/models/get-common-ancestor.js index 243eb7a5d..08d170415 100644 --- a/packages/slate/benchmark/models/get-leaves.js +++ b/benchmark/slate/models/get-common-ancestor.js @@ -1,20 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' -import { resetMemoization } from '../..' +const h = require('../../helpers/h') -export default function(text) { - text.getLeaves() +module.exports.default = function({ value, first, last }) { + value.document.getCommonAncestor(first.key, last.key) } -export function before(value) { - const text = value.document.getFirstText() - resetMemoization() - return text -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -30,3 +23,10 @@ export const input = ( ) + +const first = value.document.getFirstText() +const last = value.document.getLastText() + +module.exports.input = function() { + return { value, first, last } +} diff --git a/benchmark/slate/models/get-furthest-ancestor.js b/benchmark/slate/models/get-furthest-ancestor.js new file mode 100644 index 000000000..e9f33bae7 --- /dev/null +++ b/benchmark/slate/models/get-furthest-ancestor.js @@ -0,0 +1,30 @@ +/** @jsx h */ +/* eslint-disable react/jsx-key */ + +const h = require('../../helpers/h') + +module.exports.default = function({ value, text }) { + value.document.getFurthestAncestor(text.key) +} + +const value = ( + + + {Array.from(Array(10)).map(() => ( + + + + This is editable rich text, much better than a + textarea! + + + + ))} + + +) +const text = value.document.getLastText() + +module.exports.input = function() { + return { value, text } +} diff --git a/packages/slate/benchmark/models/get-inlines-at-range.js b/benchmark/slate/models/get-inlines-at-range.js similarity index 70% rename from packages/slate/benchmark/models/get-inlines-at-range.js rename to benchmark/slate/models/get-inlines-at-range.js index b31dfe4a8..d8d6b7a3d 100644 --- a/packages/slate/benchmark/models/get-inlines-at-range.js +++ b/benchmark/slate/models/get-inlines-at-range.js @@ -1,17 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.document.getInlinesAtRange(value.selection) } -export function before(value) { - return value.change().selectAll().value -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -27,3 +23,9 @@ export const input = ( ) + .change() + .selectAll().value + +module.exports.input = function() { + return value +} diff --git a/packages/slate/benchmark/models/get-inlines.js b/benchmark/slate/models/get-inlines.js similarity index 73% rename from packages/slate/benchmark/models/get-inlines.js rename to benchmark/slate/models/get-inlines.js index cef280292..04bc7c4e1 100644 --- a/packages/slate/benchmark/models/get-inlines.js +++ b/benchmark/slate/models/get-inlines.js @@ -1,13 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.document.getInlines() } -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -23,3 +23,7 @@ export const input = ( ) + +module.exports.input = function() { + return value +} diff --git a/packages/slate/benchmark/models/get-characters.js b/benchmark/slate/models/get-leaves.js similarity index 64% rename from packages/slate/benchmark/models/get-characters.js rename to benchmark/slate/models/get-leaves.js index 6e194313a..d91ad2c1b 100644 --- a/packages/slate/benchmark/models/get-characters.js +++ b/benchmark/slate/models/get-leaves.js @@ -1,13 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { - value.document.getCharacters() +module.exports.default = function(text) { + text.getLeaves() } -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -23,3 +23,8 @@ export const input = ( ) +const text = value.document.getFirstText() + +module.exports.input = function() { + return text +} diff --git a/packages/slate/benchmark/models/get-marks-at-range.js b/benchmark/slate/models/get-marks-at-range.js similarity index 70% rename from packages/slate/benchmark/models/get-marks-at-range.js rename to benchmark/slate/models/get-marks-at-range.js index 701d58d84..389f1e940 100644 --- a/packages/slate/benchmark/models/get-marks-at-range.js +++ b/benchmark/slate/models/get-marks-at-range.js @@ -1,17 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.document.getMarksAtRange(value.selection) } -export function before(value) { - return value.change().selectAll().value -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -27,3 +23,9 @@ export const input = ( ) + .change() + .selectAll().value + +module.exports.input = function() { + return value +} diff --git a/packages/slate/benchmark/models/get-marks.js b/benchmark/slate/models/get-marks.js similarity index 73% rename from packages/slate/benchmark/models/get-marks.js rename to benchmark/slate/models/get-marks.js index 99fa6e0d8..530eda9bf 100644 --- a/packages/slate/benchmark/models/get-marks.js +++ b/benchmark/slate/models/get-marks.js @@ -1,13 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.document.getMarks() } -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -23,3 +23,7 @@ export const input = ( ) + +module.exports.input = function() { + return value +} diff --git a/benchmark/slate/models/get-parent.js b/benchmark/slate/models/get-parent.js new file mode 100644 index 000000000..98159a19b --- /dev/null +++ b/benchmark/slate/models/get-parent.js @@ -0,0 +1,30 @@ +/** @jsx h */ +/* eslint-disable react/jsx-key */ + +const h = require('../../helpers/h') + +module.exports.default = function({ value, text }) { + value.document.getParent(text.key) +} + +const value = ( + + + {Array.from(Array(10)).map(() => ( + + + + This is editable rich text, much better than a + textarea! + + + + ))} + + +) +const text = value.document.getLastText() + +module.exports.input = () => { + return { value, text } +} diff --git a/packages/slate/benchmark/models/get-path.js b/benchmark/slate/models/get-path.js similarity index 65% rename from packages/slate/benchmark/models/get-path.js rename to benchmark/slate/models/get-path.js index 8f06f7910..20c81c6dc 100644 --- a/packages/slate/benchmark/models/get-path.js +++ b/benchmark/slate/models/get-path.js @@ -1,20 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' -import { resetMemoization } from '../..' +const h = require('../../helpers/h') -export default function({ value, text }) { +module.exports.default = function({ value, text }) { value.document.getPath(text.key) } -export function before(value) { - const text = value.document.getLastText() - resetMemoization() - return { value, text } -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -30,3 +23,8 @@ export const input = ( ) +const text = value.document.getLastText() + +module.exports.input = () => { + return { value, text } +} diff --git a/packages/slate/benchmark/models/get-texts-at-range.js b/benchmark/slate/models/get-texts-at-range.js similarity index 70% rename from packages/slate/benchmark/models/get-texts-at-range.js rename to benchmark/slate/models/get-texts-at-range.js index 250c600f1..a1902e2a1 100644 --- a/packages/slate/benchmark/models/get-texts-at-range.js +++ b/benchmark/slate/models/get-texts-at-range.js @@ -1,17 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.document.getTextsAtRange(value.selection) } -export function before(value) { - return value.change().selectAll().value -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -27,3 +23,9 @@ export const input = ( ) + .change() + .selectAll().value + +module.exports.input = function() { + return value +} diff --git a/packages/slate/benchmark/models/get-texts.js b/benchmark/slate/models/get-texts.js similarity index 73% rename from packages/slate/benchmark/models/get-texts.js rename to benchmark/slate/models/get-texts.js index ead94832e..223ae1421 100644 --- a/packages/slate/benchmark/models/get-texts.js +++ b/benchmark/slate/models/get-texts.js @@ -1,13 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.document.getTexts() } -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -23,3 +23,7 @@ export const input = ( ) + +module.exports.input = function() { + return value +} diff --git a/benchmark/slate/models/getDescendant.js b/benchmark/slate/models/getDescendant.js new file mode 100644 index 000000000..ab2253511 --- /dev/null +++ b/benchmark/slate/models/getDescendant.js @@ -0,0 +1,30 @@ +/** @jsx h */ +/* eslint-disable react/jsx-key */ + +const h = require('../../helpers/h') + +module.exports.default = function({ value, text }) { + value.document.getDescendant(text.key) +} + +const value = ( + + + {Array.from(Array(10)).map(() => ( + + + + This is editable rich text, much better than a + textarea! + + + + ))} + + +) +const text = value.document.getLastText() + +module.exports.input = function() { + return { value, text } +} diff --git a/packages/slate/benchmark/models/has-node-multiple.js b/benchmark/slate/models/has-node-multiple.js similarity index 62% rename from packages/slate/benchmark/models/has-node-multiple.js rename to benchmark/slate/models/has-node-multiple.js index 30bf728a8..fb9ad0f68 100644 --- a/packages/slate/benchmark/models/has-node-multiple.js +++ b/benchmark/slate/models/has-node-multiple.js @@ -1,25 +1,15 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' -import { resetMemoization } from '../..' +const h = require('../../helpers/h') -export default function({ value, keys }) { +module.exports.default = function({ value, keys }) { keys.forEach(key => { value.document.hasNode(key) }) } -export function before(value) { - const keys = value.document - .getTexts() - .toArray() - .map(t => t.key) - resetMemoization() - return { value, keys } -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -35,3 +25,11 @@ export const input = ( ) +const keys = value.document + .getTexts() + .toArray() + .map(t => t.key) + +module.exports.input = function() { + return { value, keys } +} diff --git a/packages/slate/benchmark/models/has-node.js b/benchmark/slate/models/has-node.js similarity index 65% rename from packages/slate/benchmark/models/has-node.js rename to benchmark/slate/models/has-node.js index 4c4253c88..a6d28f958 100644 --- a/packages/slate/benchmark/models/has-node.js +++ b/benchmark/slate/models/has-node.js @@ -1,20 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' -import { resetMemoization } from '../..' +const h = require('../../helpers/h') -export default function({ value, text }) { +module.exports.default = function({ value, text }) { value.document.hasNode(text.key) } -export function before(value) { - const text = value.document.getLastText() - resetMemoization() - return { value, text } -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -30,3 +23,8 @@ export const input = ( ) +const text = value.document.getLastText() + +module.exports.input = function() { + return { value, text } +} diff --git a/packages/slate/benchmark/models/to-json.js b/benchmark/slate/models/to-json.js similarity index 72% rename from packages/slate/benchmark/models/to-json.js rename to benchmark/slate/models/to-json.js index 9a3b05e70..9400b7552 100644 --- a/packages/slate/benchmark/models/to-json.js +++ b/benchmark/slate/models/to-json.js @@ -1,13 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' +const h = require('../../helpers/h') -export default function(value) { +module.exports.default = function(value) { value.toJSON() } -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -23,3 +23,7 @@ export const input = ( ) + +module.exports.input = function() { + return value +} diff --git a/packages/slate/benchmark/models/update-node.js b/benchmark/slate/models/update-node.js similarity index 55% rename from packages/slate/benchmark/models/update-node.js rename to benchmark/slate/models/update-node.js index 7b924b4af..a8a9666f6 100644 --- a/packages/slate/benchmark/models/update-node.js +++ b/benchmark/slate/models/update-node.js @@ -1,23 +1,13 @@ /** @jsx h */ /* eslint-disable react/jsx-key */ -import h from '../../test/helpers/h' -import { resetMemoization } from '../..' +const h = require('../../helpers/h') -export default function({ value, next }) { +module.exports.default = function({ value, next }) { value.document.updateNode(next) } -export function before(value) { - const texts = value.document.getTexts() - const { size } = texts - const text = texts.get(Math.round(size / 2)) - const next = text.insertText(0, 'some text') - resetMemoization() - return { value, next } -} - -export const input = ( +const value = ( {Array.from(Array(10)).map(() => ( @@ -33,3 +23,12 @@ export const input = ( ) + +const texts = value.document.getTexts() +const { size } = texts +const text = texts.get(Math.round(size / 2)) +const next = text.insertText(0, 'some text') + +module.exports.input = function() { + return { value, next } +} diff --git a/package.json b/package.json index bfdcc516a..cb6295319 100644 --- a/package.json +++ b/package.json @@ -16,17 +16,20 @@ "babel-preset-stage-0": "^6.24.1", "babel-runtime": "^6.26.0", "chalk": "^1.1.3", + "commander": "^2.15.1", "copy-webpack-plugin": "^4.4.1", "cross-env": "^5.1.3", "css-loader": "^0.28.9", "emotion": "^9.2.4", "eslint": "^4.19.1", + "emojis": "^1.0.10", "eslint-config-prettier": "^2.9.0", "eslint-plugin-import": "^2.8.0", "eslint-plugin-prettier": "^2.5.0", "eslint-plugin-react": "^7.6.0", "extract-text-webpack-plugin": "^3.0.2", "faker": "^3.1.0", + "figures": "^2.0.0", "fs-promise": "^1.0.0", "gh-pages": "^0.11.0", "html-webpack-plugin": "^2.30.1", @@ -62,6 +65,7 @@ "rollup-plugin-sourcemaps": "^0.4.2", "rollup-plugin-uglify": "^3.0.0", "slate-collapse-on-escape": "^0.6.0", + "slate-dev-benchmark": "*", "slate-soft-break": "^0.6.0", "source-map-loader": "^0.2.3", "source-map-support": "^0.4.0", @@ -79,13 +83,15 @@ "slate-schema-violations": "*" }, "scripts": { - "benchmark": "mkdir -p ./tmp && cross-env BABEL_ENV=test babel-node ./node_modules/.bin/_matcha --reporter ./support/benchmark/reporter ./packages/*/benchmark/index.js > ./tmp/benchmark-comparison.json && cross-env BABEL_ENV=test babel-node ./support/benchmark/compare", - "benchmark:save": "mkdir -p ./tmp && cross-env BABEL_ENV=test babel-node ./node_modules/.bin/_matcha --reporter ./support/benchmark/reporter ./packages/*/benchmark/index.js > ./tmp/benchmark-baseline.json", + "benchmark": "cross-env COMPARE=compare node --expose-gc ./tmp/benchmark/index.js", + "benchmark:save": " yarn benchmark:prepare && node --expose-gc ./tmp/benchmark/index.js", + "benchmark:prepare": "mkdir -p ./tmp && cross-env BABEL_ENV=benchmark babel benchmark --out-dir tmp/benchmark/", + "bootstrap": "lerna bootstrap && yarn build", "build": "rollup --config ./support/rollup/config.js", "build:production": "cross-env NODE_ENV=production rollup --config ./support/rollup/config.js && cross-env NODE_ENV=production webpack --config support/webpack/config.js", "clean": "lerna run clean && rm -rf ./node_modules ./dist ./build", "gh-pages": "gh-pages --dist ./build", - "lint": "eslint packages/*/src packages/*/test examples/*/*.js examples/dev/*/*.js && prettier --list-different '**/*.{js,jsx,md,json,css}'", + "lint": "eslint benchmark packages/*/src packages/*/test examples/*/*.js examples/dev/*/*.js && prettier --list-different '**/*.{js,jsx,md,json,css}'", "open": "open http://localhost:8080", "prettier": "prettier --write '**/*.{js,jsx,md,json,css}'", "release": "yarn build:production && yarn test && yarn lint && lerna publish && yarn gh-pages", diff --git a/packages/slate-dev-benchmark/Readme.md b/packages/slate-dev-benchmark/Readme.md new file mode 100644 index 000000000..46021a7ce --- /dev/null +++ b/packages/slate-dev-benchmark/Readme.md @@ -0,0 +1 @@ +This package contains 'Suite' and 'Bench' that Slate uses to compare computation efficiency under nodeJS. diff --git a/packages/slate-dev-benchmark/package.json b/packages/slate-dev-benchmark/package.json new file mode 100644 index 000000000..9beecb2af --- /dev/null +++ b/packages/slate-dev-benchmark/package.json @@ -0,0 +1,15 @@ +{ + "name": "slate-dev-benchmark", + "description": "A simple, development-only benchmark for Slate", + "version": "0.0.1", + "license": "MIT", + "repository": "git://github.com/ianstormtaylor/slate.git", + "main": "src/index.js", + "devDependencies": { + "mocha": "^2.5.3" + }, + "scripts": { + "clean": "rm -rf ./dist ./lib ./node_modules" + }, + "keywords": ["slate"] +} diff --git a/packages/slate-dev-benchmark/src/Bench.js b/packages/slate-dev-benchmark/src/Bench.js new file mode 100644 index 000000000..4bffc1cb3 --- /dev/null +++ b/packages/slate-dev-benchmark/src/Bench.js @@ -0,0 +1,227 @@ +/* global Promise */ +const { BenchType } = require('./types') +const { makeOptions } = require('./makeOptions') +const { Timer } = require('./Timer') +const { logger } = require('./logger') + +const errorReport = { + cycles: NaN, + user: NaN, + system: NaN, + all: NaN, +} + +/** + * Run a task and calculate the time consuming of tasks + */ + +class Bench { + /** + * Construct a bench and register it to a Suite + * @param {Suite} suite + * @param {string} name + * @param {Object} options + */ + + constructor(suite, name, options = {}) { + this.name = name + this.options = makeOptions({ ...suite.options, ...options }) + this.isFinished = false + this.inputer = () => undefined + this.runner = () => {} + this.report = { ...errorReport } + suite.addBench(this) + } + + /** + * Is a Bench? + * @param {any} obj + * @return {boolean} + */ + + isBench(obj) { + return obj && obj[BenchType] + } + + /** + * Set the method to generate (different} inputs for each run + * @param {Array|Function|Scalar} inputer + * @return {void} + */ + + input(inputer) { + if (Array.isArray(inputer)) { + this.inputer = index => inputer[index % inputer.length] + return + } + + if (typeof inputer === 'function') { + this.inputer = inputer + return + } + + this.inputer = () => inputer + } + + /** + * Set the task runner + * @param {Function} runner + * @return {void} + */ + + run(runner) { + this.runner = runner + } + + /** + * Tries to run tasks in `times`, if the time consuming excedes the max-time, then stop; + * After run, generate report and return + * If initial is the initial index to run the task, for continueing a task in adaptive mode + * @param {number} times + * @param {number} initial + */ + + async compose(times, initial) { + times = Math.floor(times) + const isAsync = this.options.async + const { runner, inputer } = this + + const { maxTime } = this.options + let seq = Number.isFinite(this.options.maxTries) ? 1 : NaN + let nextCheckIndex = seq + const hrStart = process.hrtime() + + if (global.gc) { + global.gc() + } + + const report = { user: 0, system: 0, all: 0, hr: 0, cycles: 0 } + + for ( + let initialIndex = initial; + initialIndex < times; + initialIndex += this.options.allocationTries + ) { + const tries = Math.min(times - initialIndex, this.options.allocationTries) + const thisTryReport = await runBundleTasks.call(this, tries, initialIndex) + + if (global.gc) { + global.gc() + } + + for (const key in report) { + report[key] += thisTryReport[key] + } + } + return report + + /** + * Run a bundle of tasks; + * the Bench estimate the time consuming of every `tries` tasks, then explictly run gc, and caculate the time consuming of next bundle tasks + * @param {number} tries + * @param {number} initialIndex + * @return {Promise< Object , *>} + */ + + function runBundleTasks(tries, initialIndex) { + const inputs = Array.from({ length: tries }).map(index => + inputer(index + initialIndex) + ) + const timer = new Timer() + timer.start() + return runFrom(0).then(cycles => { + timer.end() + const { elapsed } = timer + return { ...elapsed, cycles } + }) + + /** + * Run a single task run; If the task is end, return a Promise with the index when the task ends + * @param {number} index + * @return {Promise} + */ + + function runFrom(index) { + if (index === tries) return Promise.resolve(tries) + + if (index === nextCheckIndex) { + const hrEnd = process.hrtime(hrStart) + const elapsed = hrEnd[0] * 1e3 + hrEnd[1] / 1e6 + + if (elapsed > maxTime) { + return Promise.resolve(index) + } else { + if (elapsed < maxTime / 20) { + seq *= 2 + } + + nextCheckIndex = seq + nextCheckIndex + } + } + + if (!isAsync) { + const inputVar = inputs[index] + runner(inputVar) + return runFrom(index + 1) + } else { + return Promise.resolve(runner(inputs[index])).then(() => + runFrom(index + 1) + ) + } + } + } + } + + /* + * Run the bench + * @return {void} + */ + + makeRun() { + if (this.isFinished) return true + logger(this) + const { options } = this + const { minTries, maxTime, maxTries } = options + + let { minTime } = options + if (minTime > maxTime) minTime = maxTime + + return this.compose(minTries, 0) + .then(report => { + if (this.options.mode === 'static') return report + const { all } = report + if (all > minTime) return report + const times = (minTime / all - 1) * minTries + return this.compose(Math.min(times, maxTries), minTries).then( + newReport => { + return mergeResults(report, newReport) + } + ) + }) + .then(report => { + this.report = report + this.isFinished = true + logger(this) + return true + }) + } +} + +Bench.prototype[BenchType] = true + +/* + * Merge two different report + * @param {Object} res1 + * @param {Object} res2 + */ + +function mergeResults(res1, res2) { + const result = {} + + for (const key in res1) { + result[key] = res1[key] + res2[key] + } + return result +} + +module.exports = { Bench } diff --git a/packages/slate-dev-benchmark/src/Repository.js b/packages/slate-dev-benchmark/src/Repository.js new file mode 100644 index 000000000..729f199c8 --- /dev/null +++ b/packages/slate-dev-benchmark/src/Repository.js @@ -0,0 +1,73 @@ +/* global Promise */ +const { RepositoryType } = require('./types') +const { logger } = require('./logger') +const { compose } = require('./compose') + +/** + * Repository Class for holding Suite + */ + +class Repository { + /** + * Construct a Repository with a name + * @param {string} name + */ + + constructor(name = 'default') { + this.name = name + this.suites = [] + this.report = {} + this.isFinished = false + } + + /** + * Check whether {obj} is repository + * @param {any} obj + * @return {boolean} + */ + + isRepository(obj) { + return obj && obj[RepositoryType] + } + + /** + * Register a suite to the repository + * @param {Suite} suite + * @return {void} + */ + + addSuite(suite) { + this.isFinished = false + this.suites.push(suite) + } + + /** + * Run all suites (and all benches under suites) and generate a report + * @return {Promise} + */ + + run() { + if (this.isFinished) return Promise.resolve(this.report) + logger(this) + return compose(this.suites).then(() => { + this.isFinished = true + const report = {} + + for (const suite of this.suites) { + report[suite.name] = suite.report + } + + this.report = report + return report + }) + } +} +Repository.prototype[RepositoryType] = true + +/** + * By default, all suites are registers to the following {repo} + */ + +const repo = new Repository() + +module.exports = { Repository, repo } diff --git a/packages/slate-dev-benchmark/src/Suite.js b/packages/slate-dev-benchmark/src/Suite.js new file mode 100644 index 000000000..ea44f637a --- /dev/null +++ b/packages/slate-dev-benchmark/src/Suite.js @@ -0,0 +1,85 @@ +/* global Promise */ +const { repo } = require('./Repository.js') +const { SuiteType } = require('./types') +const { logger } = require('./logger') +const { compose } = require('./compose') +const { makeOptions } = require('./makeOptions') + +/** + * Suite is for holding Benches + */ + +class Suite { + /** + * Construct a Suite and regiester it to repository + * @param {string} name + * @param {Object} options + * @property {void|Repository} repository + * @property {any} ...rest + */ + + constructor(name, options = {}) { + const { repository = repo } = options + + if (repository[name]) { + throw Error(`The suite name ${name} has benn occupied in repository`) + } + + if (typeof name !== 'string') { + throw Error(`The suite name must be a string`) + } + + this.name = name + this.options = makeOptions(options) + this.isFinished = false + this.benches = [] + this.report = {} + repository.addSuite(this) + } + + /** + * Whether it is a Suite + * @param {any} obj + * @return {boolean} + */ + + isSuite(obj) { + return obj && obj[SuiteType] + } + + /** + * Register an bench to the repository + * @param {Bench} bench + * @return {void} + */ + + addBench(bench) { + this.isFinished = false + this.benches.push(bench) + } + + /** + * Run all benches, and generate report for consumed time + * @return {Promise + */ + + makeRun() { + if (this.isFinished) return Promise.resolve(this.report) + logger(this) + return compose(this.benches).then(() => { + this.isFinished = true + const report = {} + + for (const bench of this.benches) { + report[bench.name] = bench.report + } + + this.report = report + return report + }) + } +} + +Suite.prototype[SuiteType] = true + +module.exports = { Suite } diff --git a/packages/slate-dev-benchmark/src/Timer.js b/packages/slate-dev-benchmark/src/Timer.js new file mode 100644 index 000000000..a3ae56cb8 --- /dev/null +++ b/packages/slate-dev-benchmark/src/Timer.js @@ -0,0 +1,66 @@ +const { TimerType } = require('./types') + +class Timer { + constructor() { + this.cpuStartTime = {} + this.hrStartTime = null + this.isStopped = false + this.elapsed = {} + } + + /** + * Whether it is a Timer + * @param {any} obj + */ + + isTimer(obj) { + return obj && obj[TimerType] + } + + /** + * Start the timer + * @return {void} + */ + + start() { + this.isStopped = false + this.cpuStartTime = process.cpuUsage() + this.hrStartTime = process.hrtime() + this.elapsed = {} + } + + /** + * Stop the timer and store restore in this.elapsed + * @return {Object} + */ + + end() { + if (this.isStopped) return this.elapsed + const cpuElapsed = process.cpuUsage(this.cpuStartTime) + const hrElapsed = process.hrtime(this.hrStartTime) + const { user, system } = cpuElapsed + const hr = hrElapsed[0] * 1000 + hrElapsed[1] / 1e6 + + /** + * user: cpu time consumed in user space + * system: cpu time consumed in system space + * all: user+system + * hr: real world time + * (unit): ms + */ + + this.elapsed = { + user: user / 1000, + system: system / 1000, + all: (user + system) / 1000, + hr, + } + + this.isStopped = true + return this.elapsed + } +} + +Timer.prototype[TimerType] = true + +module.exports = { Timer } diff --git a/packages/slate-dev-benchmark/src/compose.js b/packages/slate-dev-benchmark/src/compose.js new file mode 100644 index 000000000..146026b8c --- /dev/null +++ b/packages/slate-dev-benchmark/src/compose.js @@ -0,0 +1,22 @@ +/* global Promise */ +const { errorLog } = require('./logger') + +/** + * Run all benches/suites with Promise; Ensure an error would not block the whole process + * @param {Array|Array} + * @param {string} name; where to call the run method + */ + +function compose(list, name = 'makeRun') { + return dispatch(0) + + function dispatch(index) { + if (index === list.length) return Promise.resolve(true) + const node = list[index] + return new Promise(resolve => resolve(node[name]())) + .catch(err => errorLog(err)) + .then(() => dispatch(index + 1)) + } +} + +module.exports = { compose } diff --git a/packages/slate-dev-benchmark/src/index.js b/packages/slate-dev-benchmark/src/index.js new file mode 100644 index 000000000..4491bf79d --- /dev/null +++ b/packages/slate-dev-benchmark/src/index.js @@ -0,0 +1,5 @@ +const { Repository, repo } = require('./Repository') +const { Suite } = require('./Suite') +const { Bench } = require('./Bench') + +module.exports = { Repository, Suite, Bench, repo } diff --git a/packages/slate-dev-benchmark/src/logger.js b/packages/slate-dev-benchmark/src/logger.js new file mode 100644 index 000000000..57f1bdf62 --- /dev/null +++ b/packages/slate-dev-benchmark/src/logger.js @@ -0,0 +1,74 @@ +/* eslint-disable no-console */ + +/** + * IS in test + */ + +const IS_TEST = + typeof process !== 'undefined' && + process.env && + process.env.BABEL_ENV === 'test' + +/** + * Log a `message` + * + * @param {String} message + * @param {Any} ...args + * @retrun {void} + */ + +function log(message, ...args) { + if (IS_TEST) return + + return console.log(message, ...args) +} + +/* + * Log a error `message` +*/ + +function errorLog(message, ...args) { + console.error(message, ...args) +} + +/** + * Logging benchmark result + */ + +function logger(obj) { + const prefix = ' ' + + if (obj.isRepository) { + return log(`Repository ${obj.name} is running`) + } + + if (obj.isSuite) { + return log(`${prefix}- Suite ${obj.name} is running`) + } + + if (obj.isBench) { + if (!obj.isFinished) { + return log(`${prefix + prefix}- Bench ${obj.name} is running`) + } + + const { report } = obj + const { cycles } = report + + const header = { + user: 'user:', + hr: 'real:', + } + + for (const key of ['user', 'hr']) { + log( + `${prefix + prefix + prefix}${header[key]} * ${cycles} cycles: ${ + report[key] + } ms; ( ${cycles * 1000 / report[key]} ops/sec)` + ) + } + return log(`${prefix + prefix + prefix}cycles: ${cycles}`) + } + return log(obj) +} + +module.exports = { logger, errorLog, log } diff --git a/packages/slate-dev-benchmark/src/makeOptions.js b/packages/slate-dev-benchmark/src/makeOptions.js new file mode 100644 index 000000000..f9938fd90 --- /dev/null +++ b/packages/slate-dev-benchmark/src/makeOptions.js @@ -0,0 +1,40 @@ +const defaultOptions = { + minTime: 1000, + maxTime: 2000, + minTries: 100, + maxTries: Infinity, + allocationTries: 1000, + async: false, + mode: 'adaptive', +} + +/** + * Merge two options for configuring a bench run + * @param {Object} options + * @returns {Object} + * @property {number} minTime + * @property {number} maxTime + * @property {number} minTries + * @property {number} maxTries + * @property {number} allocationTries + * @property {boolean} async + * @property {"static"|"adaptive"} mode + */ + +function makeOptions(options) { + const result = { ...defaultOptions, ...options } + + for (const key in defaultOptions) { + const shallType = typeof defaultOptions[key] + const inputType = typeof result[key] + + if (shallType !== inputType) { + throw TypeError( + `Wrong Input in Config Suite, options[${key}] should be ${shallType}, but the input type is ${inputType}` + ) + } + } + return result +} + +module.exports = { makeOptions } diff --git a/packages/slate-dev-benchmark/src/types.js b/packages/slate-dev-benchmark/src/types.js new file mode 100644 index 000000000..f99e12f70 --- /dev/null +++ b/packages/slate-dev-benchmark/src/types.js @@ -0,0 +1,11 @@ +const RepositoryType = '@@__SLATE_REPOSITORY__@@' +const SuiteType = '@@__SLATE_SUITE__@@' +const BenchType = '@@__SLATE_BENCH__@@' +const TimerType = '@@__SLATE_BENCH_TIMER_@@' + +module.exports = { + RepositoryType, + SuiteType, + BenchType, + TimerType, +} diff --git a/packages/slate-dev-benchmark/test/index.js b/packages/slate-dev-benchmark/test/index.js new file mode 100644 index 000000000..26edceef7 --- /dev/null +++ b/packages/slate-dev-benchmark/test/index.js @@ -0,0 +1,12 @@ +/** + * Dependencies. + */ + +/** + * Tests. + */ + +describe('slate-dev-benchmark', () => { + require('./tries/') + require('./time/') +}) diff --git a/packages/slate-dev-benchmark/test/time/index.js b/packages/slate-dev-benchmark/test/time/index.js new file mode 100644 index 000000000..2c63270f5 --- /dev/null +++ b/packages/slate-dev-benchmark/test/time/index.js @@ -0,0 +1,22 @@ +import { repo, Suite } from '../..' +import fs from 'fs' +import { resolve } from 'path' + +describe('time', async () => { + const suite = new Suite('tries') + const testDir = resolve(__dirname) + const files = fs + .readdirSync(testDir) + .filter(x => x[0] !== '.' && x !== 'index.js') + + for (const file of files) { + const module = require(`./${file}`) + + it(module.experiment, () => { + module.default(suite) + const { expected } = module + repo.isFinished = false + return repo.run().then(() => expected()) + }) + } +}) diff --git a/packages/slate-dev-benchmark/test/time/max-time-async.js b/packages/slate-dev-benchmark/test/time/max-time-async.js new file mode 100644 index 000000000..519d321cd --- /dev/null +++ b/packages/slate-dev-benchmark/test/time/max-time-async.js @@ -0,0 +1,31 @@ +/* global Promise */ +import { Bench } from '../..' +import assert from 'assert' + +export const experiment = 'max-time-async' + +let index = 0 + +// A wider range than sync, becuase Promise intialization, babel-node takes time +export function expected() { + assert( + index > 5 && index < 12, + `index should be 10, but is actually ${index}` + ) + return true +} + +export default function(suite) { + const bench = new Bench(suite, experiment, { + mode: 'adaptive', + minTries: 100, + maxTries: 200, + minTime: 1, + maxTime: 100, + async: true, + }) + + bench.run( + () => new Promise(resolve => setTimeout(() => resolve(index++), 10)) + ) +} diff --git a/packages/slate-dev-benchmark/test/time/max-time-with-small-step.js b/packages/slate-dev-benchmark/test/time/max-time-with-small-step.js new file mode 100644 index 000000000..86bda26c6 --- /dev/null +++ b/packages/slate-dev-benchmark/test/time/max-time-with-small-step.js @@ -0,0 +1,31 @@ +import { Bench } from '../..' +import { syncSleep } from '../utils/sleep' +import assert from 'assert' + +export const experiment = 'max-time' + +let index = 0 + +export function expected() { + assert( + index > 85 && index < 115, + `index should be around 100, but is actually ${index}` + ) + return true +} + +export default function(suite) { + const bench = new Bench(suite, experiment, { + mode: 'adaptive', + minTries: 1000, + maxTries: 2000, + minTime: 1, + maxTime: 1000, + async: false, + }) + + bench.run(() => { + syncSleep(10) + index++ + }) +} diff --git a/packages/slate-dev-benchmark/test/time/max-time.js b/packages/slate-dev-benchmark/test/time/max-time.js new file mode 100644 index 000000000..3c9bb0639 --- /dev/null +++ b/packages/slate-dev-benchmark/test/time/max-time.js @@ -0,0 +1,28 @@ +import { Bench } from '../..' +import { syncSleep } from '../utils/sleep' +import assert from 'assert' + +export const experiment = 'max-time' + +let index = 0 + +export function expected() { + assert(index === 10, `index should be 10, but is actually ${index}`) + return true +} + +export default function(suite) { + const bench = new Bench(suite, experiment, { + mode: 'adaptive', + minTries: 100, + maxTries: 200, + minTime: 1, + maxTime: 100, + async: false, + }) + + bench.run(() => { + syncSleep(10) + index++ + }) +} diff --git a/packages/slate-dev-benchmark/test/tries/index.js b/packages/slate-dev-benchmark/test/tries/index.js new file mode 100644 index 000000000..801bb9c23 --- /dev/null +++ b/packages/slate-dev-benchmark/test/tries/index.js @@ -0,0 +1,25 @@ +import assert from 'assert' +import { repo, Suite } from '../..' +import fs from 'fs' +import { resolve } from 'path' + +describe('tries', async () => { + const suite = new Suite('tries') + const testDir = resolve(__dirname) + const files = fs + .readdirSync(testDir) + .filter(x => x[0] !== '.' && x !== 'index.js') + + for (const file of files) { + const module = require(`./${file}`) + + it(module.experiment, () => { + module.default(suite) + const { actual, expected } = module + repo.isFinished = false + return repo.run().then(() => { + assert.deepEqual(actual, expected) + }) + }) + } +}) diff --git a/packages/slate-dev-benchmark/test/tries/max-tries.js b/packages/slate-dev-benchmark/test/tries/max-tries.js new file mode 100644 index 000000000..e7a434d45 --- /dev/null +++ b/packages/slate-dev-benchmark/test/tries/max-tries.js @@ -0,0 +1,19 @@ +import { Bench } from '../..' + +export const experiment = 'max-tries adaptive mode' +export const actual = { index: 0 } +export const expected = { index: 200 } + +export default function(suite) { + const bench = new Bench(suite, experiment, { + mode: 'adaptive', + minTries: 100, + maxTries: 200, + minTime: 100, + maxTime: Infinity, + }) + + bench.run(() => { + actual.index++ + }) +} diff --git a/packages/slate-dev-benchmark/test/tries/min-tries.js b/packages/slate-dev-benchmark/test/tries/min-tries.js new file mode 100644 index 000000000..ae1996f86 --- /dev/null +++ b/packages/slate-dev-benchmark/test/tries/min-tries.js @@ -0,0 +1,18 @@ +import { Bench } from '../..' + +export const experiment = 'min-tries static mode' +export const actual = { index: 0 } +export const expected = { index: 100 } + +export default function(suite) { + const bench = new Bench(suite, experiment, { + mode: 'static', + minTries: 100, + maxTries: 200, + minTime: 100, + }) + + bench.run(() => { + actual.index++ + }) +} diff --git a/packages/slate-dev-benchmark/test/utils/sleep.js b/packages/slate-dev-benchmark/test/utils/sleep.js new file mode 100644 index 000000000..6cacce4c7 --- /dev/null +++ b/packages/slate-dev-benchmark/test/utils/sleep.js @@ -0,0 +1,10 @@ +function syncSleep(ms) { + const start = process.hrtime() + + while (true) { + const end = process.hrtime(start) + if (end[0] * 1000 + end[1] / 1e6 > ms) return undefined + } +} + +export { syncSleep } diff --git a/packages/slate-html-serializer/benchmark/html-serializer/deserialize.js b/packages/slate-html-serializer/benchmark/html-serializer/deserialize.js deleted file mode 100644 index e6e69d0ea..000000000 --- a/packages/slate-html-serializer/benchmark/html-serializer/deserialize.js +++ /dev/null @@ -1,48 +0,0 @@ -/** @jsx h */ -/* eslint-disable react/jsx-key */ - -import Html from '../..' -import React from 'react' -import { JSDOM } from 'jsdom' // eslint-disable-line import/no-extraneous-dependencies - -const html = new Html({ - parseHtml: JSDOM.fragment, - rules: [ - { - serialize(obj, children) { - switch (obj.object) { - case 'block': { - switch (obj.type) { - case 'paragraph': - return React.createElement('p', {}, children) - case 'quote': - return React.createElement('blockquote', {}, children) - } - } - case 'mark': { - switch (obj.type) { - case 'bold': - return React.createElement('strong', {}, children) - case 'italic': - return React.createElement('em', {}, children) - } - } - } - }, - }, - ], -}) - -export default function(string) { - html.deserialize(string) -} - -export const input = ` -
-

- This is editable rich text, much better than a textarea! -

-
-` - .trim() - .repeat(10) diff --git a/packages/slate-html-serializer/benchmark/index.js b/packages/slate-html-serializer/benchmark/index.js deleted file mode 100644 index ae766ba97..000000000 --- a/packages/slate-html-serializer/benchmark/index.js +++ /dev/null @@ -1,46 +0,0 @@ -/* global suite, set, bench */ - -import fs from 'fs' -import { basename, extname, resolve } from 'path' -import { resetMemoization } from 'slate' - -/** - * Benchmarks. - */ - -const categoryDir = resolve(__dirname) -const categories = fs - .readdirSync(categoryDir) - .filter(c => c[0] != '.' && c != 'index.js') - -categories.forEach(category => { - suite(category, () => { - set('iterations', 50) - set('mintime', 1000) - - if (category == 'models') { - after(() => { - resetMemoization() - }) - } - - const benchmarkDir = resolve(categoryDir, category) - const benchmarks = fs - .readdirSync(benchmarkDir) - .filter(b => b[0] != '.' && !!~b.indexOf('.js')) - .map(b => basename(b, extname(b))) - - benchmarks.forEach(benchmark => { - const dir = resolve(benchmarkDir, benchmark) - const module = require(dir) - const fn = module.default - let { input, before, after } = module - if (before) input = before(input) - - bench(benchmark, () => { - fn(input) - if (after) after() - }) - }) - }) -}) diff --git a/packages/slate-plain-serializer/benchmark/index.js b/packages/slate-plain-serializer/benchmark/index.js deleted file mode 100644 index ae766ba97..000000000 --- a/packages/slate-plain-serializer/benchmark/index.js +++ /dev/null @@ -1,46 +0,0 @@ -/* global suite, set, bench */ - -import fs from 'fs' -import { basename, extname, resolve } from 'path' -import { resetMemoization } from 'slate' - -/** - * Benchmarks. - */ - -const categoryDir = resolve(__dirname) -const categories = fs - .readdirSync(categoryDir) - .filter(c => c[0] != '.' && c != 'index.js') - -categories.forEach(category => { - suite(category, () => { - set('iterations', 50) - set('mintime', 1000) - - if (category == 'models') { - after(() => { - resetMemoization() - }) - } - - const benchmarkDir = resolve(categoryDir, category) - const benchmarks = fs - .readdirSync(benchmarkDir) - .filter(b => b[0] != '.' && !!~b.indexOf('.js')) - .map(b => basename(b, extname(b))) - - benchmarks.forEach(benchmark => { - const dir = resolve(benchmarkDir, benchmark) - const module = require(dir) - const fn = module.default - let { input, before, after } = module - if (before) input = before(input) - - bench(benchmark, () => { - fn(input) - if (after) after() - }) - }) - }) -}) diff --git a/packages/slate-react/benchmark/index.js b/packages/slate-react/benchmark/index.js deleted file mode 100644 index 912cad210..000000000 --- a/packages/slate-react/benchmark/index.js +++ /dev/null @@ -1,39 +0,0 @@ -/* global suite, set, bench */ - -import fs from 'fs' -import { basename, extname, resolve } from 'path' - -/** - * Benchmarks. - */ - -const categoryDir = resolve(__dirname) -const categories = fs - .readdirSync(categoryDir) - .filter(c => c[0] != '.' && c != 'index.js') - -categories.forEach(category => { - suite(category, () => { - set('iterations', 50) - set('mintime', 1000) - - const benchmarkDir = resolve(categoryDir, category) - const benchmarks = fs - .readdirSync(benchmarkDir) - .filter(b => b[0] != '.' && !!~b.indexOf('.js')) - .map(b => basename(b, extname(b))) - - benchmarks.forEach(benchmark => { - const dir = resolve(benchmarkDir, benchmark) - const module = require(dir) - const fn = module.default - let { input, before, after } = module - if (before) input = before(input) - - bench(benchmark, () => { - fn(input) - if (after) after() - }) - }) - }) -}) diff --git a/packages/slate/benchmark/index.js b/packages/slate/benchmark/index.js deleted file mode 100644 index 06c8af809..000000000 --- a/packages/slate/benchmark/index.js +++ /dev/null @@ -1,46 +0,0 @@ -/* global suite, set, bench */ - -import fs from 'fs' -import { basename, extname, resolve } from 'path' -import { resetMemoization } from '..' - -/** - * Benchmarks. - */ - -const categoryDir = resolve(__dirname) -const categories = fs - .readdirSync(categoryDir) - .filter(c => c[0] != '.' && c != 'index.js') - -categories.forEach(category => { - suite(category, () => { - set('iterations', 100) - set('mintime', 1000) - - if (category == 'models') { - after(() => { - resetMemoization() - }) - } - - const benchmarkDir = resolve(categoryDir, category) - const benchmarks = fs - .readdirSync(benchmarkDir) - .filter(b => b[0] != '.' && !!~b.indexOf('.js')) - .map(b => basename(b, extname(b))) - - benchmarks.forEach(benchmark => { - const dir = resolve(benchmarkDir, benchmark) - const module = require(dir) - const fn = module.default - let { input, before, after } = module - if (before) input = before(input) - - bench(benchmark, () => { - fn(input) - if (after) after() - }) - }) - }) -}) diff --git a/support/benchmark/compare.js b/support/benchmark/compare.js index 87d18fc9f..efe7215c0 100644 --- a/support/benchmark/compare.js +++ b/support/benchmark/compare.js @@ -1,14 +1,24 @@ /* eslint-disable no-console */ import chalk from 'chalk' +import figures from 'figures' +import emojis from 'emojis' import baseline from '../../tmp/benchmark-baseline' import comparison from '../../tmp/benchmark-comparison' +import { existsSync } from 'fs' /** * Constants. */ -const THRESHOLD = 0.333 +let THRESHOLD = 0.333 +const configPath = '../../tmp/benchmark-config.js' +if (existsSync(configPath)) { + const alternative = require(configPath).THRESHOLD + if (typeof alternative === 'number' && alternative > 0) { + THRESHOLD = alternative + } +} /** * Print. @@ -21,27 +31,72 @@ baseline.forEach((suite, i) => { console.log(` ${suite.name}`) suite.benchmarks.forEach((base, j) => { - const comp = comparison[i].benchmarks[j] - if (!comp) return + const compared = { user: {}, hr: {} } - const b = base.iterations / base.elapsed * 1000 - const c = comp.iterations / comp.elapsed * 1000 - const threshold = b * THRESHOLD - const slower = b - c > threshold - const faster = b - c < 0 - threshold - const percent = Math.round(Math.abs(b - c) / b * 100) + for (const key of Object.keys(compared)) { + const comp = comparison[i].benchmarks[j] + if (!comp) return + const b = base.iterations / base[key] * 1000 + const c = comp.iterations / comp[key] * 1000 + const balancePercent = + b > c ? Math.round(Math.abs(b - c) / c * 100) : (c - b) / b * 100 - let output = `${b.toFixed(2)} → ${c.toFixed(2)} ops/sec` - if (slower) output = chalk.red(`${output} (${percent}% slower)`) - else if (faster) output = chalk.green(`${output} (${percent}% faster)`) - else output = chalk.gray(output) + const output = `${b.toFixed(2)} -> ${c.toFixed(2)} ops/sec` + compared[key].baseOutput = output + compared[key].percentOutput = `${balancePercent.toFixed(2)}% ${ + c > b ? 'faster' : 'slower' + }` + compared[key].percentValue = balancePercent + compared[key].b = b + compared[key].c = c + compared[key].isFaster = c > b + if (balancePercent > 1000) { + compared[key].percentOutput += emojis.unicode(' :scream: ') + } else if (balancePercent > 100) { + if (c > b) { + compared[key].percentOutput += emojis.unicode(' :raised_hands: ') + } else { + compared[key].percentOutput += emojis.unicode(' :worried: ') + } + } + } - if (percent > 1000) output += ' 😱' - else if (faster && percent > 100) output += ' 🙌' - else if (slower && percent > 100) output += ' 😟' + const { user, hr } = compared - console.log(` ${base.title}`) - console.log(` ${output}`) + if ( + user.percentValue < THRESHOLD * 100 && + hr.percentValue < THRESHOLD * 100 + ) { + console.log( + chalk.grey( + ` ${figures.tick} ${base.name}: ${user.baseOutput} (${ + user.percentOutput + })` + ) + ) + return + } + + if (user.isFaster === hr.isFaster) { + if (user.isFaster) { + console.log(chalk.green(` ${figures.star} ${base.name}:`)) + console.log( + ` user: ${user.baseOutput} (${user.percentOutput})` + ) + console.log(` real: ${hr.baseOutput} (${hr.percentOutput})`) + return + } + console.log(chalk.red(` ${figures.cross} ${base.name}:`)) + console.log( + ` user: ${user.baseOutput} (${user.percentOutput})` + ) + console.log(` real: ${hr.baseOutput} (${hr.percentOutput})`) + return + } + + console.log(chalk.red(` ${figures.questionMarkPrefix} ${base.name}:`)) + console.log(` user: ${user.baseOutput} (${user.percentOutput})`) + console.log(` real: ${hr.baseOutput} (${hr.percentOutput})`) }) }) diff --git a/support/benchmark/reporter.js b/support/benchmark/reporter.js deleted file mode 100644 index ccfbf43b2..000000000 --- a/support/benchmark/reporter.js +++ /dev/null @@ -1,31 +0,0 @@ -const { stdout } = process - -module.exports = function(runner, utils) { - let hasSuite = false - let hasBench = false - - runner.on('start', () => { - stdout.write('[') - }) - - runner.on('end', () => { - stdout.write(']') - }) - - runner.on('suite start', suite => { - if (hasSuite) stdout.write(',') - stdout.write(`{"name":"${suite.title}","benchmarks":[`) - hasSuite = true - }) - - runner.on('suite end', suite => { - hasBench = false - stdout.write(']}') - }) - - runner.on('bench end', bench => { - if (hasBench) stdout.write(',') - stdout.write(JSON.stringify(bench)) - hasBench = true - }) -} diff --git a/support/rollup/config.js b/support/rollup/config.js index 1e52cf4f8..e88232b60 100644 --- a/support/rollup/config.js +++ b/support/rollup/config.js @@ -11,6 +11,7 @@ import slatePropTypes from '../../packages/slate-prop-types/package.json' import slateReact from '../../packages/slate-react/package.json' import slateSchemaViolations from '../../packages/slate-schema-violations/package.json' import slateSimulator from '../../packages/slate-simulator/package.json' +// Do not import slateDevBenchmark here. The benchmark shall be a pure nodeJS program and can be run without babel-node const configurations = [ ...factory(slate), diff --git a/yarn.lock b/yarn.lock index 8318025b4..58c883bbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1926,6 +1926,10 @@ commander@2.9.0: dependencies: graceful-readlink ">= 1.0.0" +commander@^2.15.1: + version "2.16.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" + commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" @@ -2796,6 +2800,10 @@ emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" +emojis@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/emojis/-/emojis-1.0.10.tgz#2558133df0dff13313c99531647f693d7adb57da" + emotion@^9.2.4: version "9.2.4" resolved "https://registry.yarnpkg.com/emotion/-/emotion-9.2.4.tgz#0139e7cc154b2845f4b9afaa996dd4de13bb90e3"