From 8112369878111e781fa119577d69a1a7cf972f69 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 20 Aug 2020 21:09:31 +0200 Subject: [PATCH 1/3] add OT b4 --- benchmarks/b4.js | 215 ++++++++++++++++++++++++++++++++++++-------- benchmarks/run.js | 6 +- benchmarks/utils.js | 7 +- package-lock.json | 18 ++++ package.json | 2 + 5 files changed, 203 insertions(+), 45 deletions(-) diff --git a/benchmarks/b4.js b/benchmarks/b4.js index a28b2dc..1d59dac 100644 --- a/benchmarks/b4.js +++ b/benchmarks/b4.js @@ -1,5 +1,5 @@ import * as Y from 'yjs' -import { setBenchmarkResult, N, benchmarkTime, disableAutomergeBenchmarks, disablePeersCrdtsBenchmarks, disableYjsBenchmarks, logMemoryUsed, getMemUsed, tryGc } from './utils.js' +import { setBenchmarkResult, N, benchmarkTime, disableAutomergeBenchmarks, disableOTBenchmarks, disablePeersCrdtsBenchmarks, disableYjsBenchmarks, logMemoryUsed, getMemUsed, tryGc } from './utils.js' import * as math from 'lib0/math.js' import * as t from 'lib0/testing.js' // @ts-ignore @@ -7,9 +7,89 @@ import { edits, finalText } from './b4-editing-trace.js' import Automerge from 'automerge' import DeltaCRDT from 'delta-crdts' import deltaCodec from 'delta-crdts-msgpack-codec' +import OtText from 'ot-text-unicode' +import Rope from 'jumprope' + +const { makeType, insert, remove } = OtText const DeltaRGA = DeltaCRDT('rga') +const myRopeFns = { + create (str) { return new Rope(str) }, + toString (rope) { return rope.toString() }, + slice: (str, start, end) => str.slice(start, end), + builder (rope) { + // Used for applying operations + let pos = 0 // character position in unicode code points + + return { + skip (n) { pos += n }, + + append (s) { // Insert s at the current position + rope.insert(pos, s) + pos += s.length // in ASCII, no need to find unicode position. TODO: where to get unicodeLength? + }, + + del (n) { // Delete n characters at the current position + rope.del(pos, n) + }, + + build () { // Finish! + return rope + } + } + } +} + +const RopeType = makeType(myRopeFns) + +class OTDoc { + constructor (dir = 'left') { + this.type = RopeType.create() + this.dir = dir + /** + * applied operations to this document. + */ + this.ops = [] + } + + insert (index, text) { + const op = insert(index, text) + RopeType.apply(this.type, op) + this.ops.push(op) + } + + delete (index, length) { + const op = remove(index, length) + RopeType.apply(this.type, op) + this.ops.push(op) + } + + transformOpsAndApply (ops) { + for (let i = 0; i < this.ops.length; i++) { + const myOp = this.ops[i] + for (let j = 0; j < ops.length; j++) { + const theirOp = ops[j] + RopeType.transform(theirOp, myOp, /** @type {any} */ (this.dir)) + RopeType.apply(this.type, theirOp) + } + } + this.ops.push(...ops) + } + + updatesLen () { + return JSON.stringify(this.ops).length + } + + docSize () { + return JSON.stringify(insert(0, this.docContent())).length + } + + docContent () { + return this.type.toString() + } +} + const benchmarkYjs = (id, inputData, changeFunction, check) => { if (disableYjsBenchmarks) { setBenchmarkResult('yjs', id, 'skipping') @@ -53,6 +133,45 @@ const benchmarkYjs = (id, inputData, changeFunction, check) => { })() } +const benchmarkOT = (id, inputData, changeFunction, check) => { + if (disableOTBenchmarks) { + setBenchmarkResult('OT', id, 'skipping') + return + } + + let encodedState + ;(() => { + // We scope the creation of doc1 so we can gc it before we parse it again. + const doc1 = new OTDoc() + benchmarkTime('OT', `${id} (time)`, () => { + for (let i = 0; i < inputData.length; i++) { + changeFunction(doc1, inputData[i], i) + } + }) + check(doc1) + setBenchmarkResult('OT', `${id} (avgUpdateSize)`, `${math.round(doc1.updatesLen() / inputData.length)} bytes`) + /** + * @type {any} + */ + benchmarkTime('OT', `${id} (encodeTime)`, () => { + encodedState = insert(0, doc1.docContent()) + }) + const documentSize = doc1.docSize() + setBenchmarkResult('OT', `${id} (docSize)`, `${documentSize} bytes`) + })() + tryGc() + ;(() => { + const startHeapUsed = getMemUsed() + // @ts-ignore we only store doc so it is not garbage collected + let doc = null // eslint-disable-line + benchmarkTime('OT', `${id} (parseTime)`, () => { + doc = new OTDoc() + doc.transformOpsAndApply([encodedState]) + logMemoryUsed('OT', id, startHeapUsed) + }) + })() +} + const benchmarkDeltaCRDTs = (id, inputData, changeFunction, check) => { if (disablePeersCrdtsBenchmarks) { setBenchmarkResult('delta-crdts', id, 'skipping') @@ -163,6 +282,20 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { t.assert(doc1.getText('text').toString() === finalText) } ) + benchmarkOT( + benchmarkName, + edits, + (doc, edit) => { + if (edit[1] > 0) { + doc.delete(edit[0], edit[1]) + } else { + doc.insert(edit[0], edit[2]) + } + }, + doc1 => { + t.assert(doc1.docContent() === finalText) + } + ) benchmarkDeltaCRDTs( benchmarkName, edits, @@ -208,47 +341,51 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { const multiplicator = 100 let encodedState = /** @type {any} */ (null) - ;(() => { - const doc = new Y.Doc() - const ytext = doc.getText('text') - benchmarkTime('yjs', `${benchmarkName} (time)`, () => { - for (let iterations = 0; iterations < multiplicator; iterations++) { - if (iterations > 0 && iterations % 5 === 0) { - console.log(`Finished ${iterations}%`) - } - for (let i = 0; i < edits.length; i++) { - const edit = edits[i] - if (edit[1] > 0) { - ytext.delete(edit[0], edit[1]) + if (disableYjsBenchmarks) { + setBenchmarkResult('yjs', benchmarkName, 'skipping') + } else { + ;(() => { + const doc = new Y.Doc() + const ytext = doc.getText('text') + benchmarkTime('yjs', `${benchmarkName} (time)`, () => { + for (let iterations = 0; iterations < multiplicator; iterations++) { + if (iterations > 0 && iterations % 5 === 0) { + console.log(`Finished ${iterations}%`) } - if (edit[2]) { - ytext.insert(edit[0], edit[2]) + for (let i = 0; i < edits.length; i++) { + const edit = edits[i] + if (edit[1] > 0) { + ytext.delete(edit[0], edit[1]) + } + if (edit[2]) { + ytext.insert(edit[0], edit[2]) + } } } - } - }) - /** - * @type {any} - */ - benchmarkTime('yjs', `${benchmarkName} (encodeTime)`, () => { - encodedState = Y.encodeStateAsUpdateV2(doc) - }) - })() + }) + /** + * @type {any} + */ + benchmarkTime('yjs', `${benchmarkName} (encodeTime)`, () => { + encodedState = Y.encodeStateAsUpdateV2(doc) + }) + })() - ;(() => { - const documentSize = encodedState.byteLength - setBenchmarkResult('yjs', `${benchmarkName} (docSize)`, `${documentSize} bytes`) - tryGc() - })() + ;(() => { + const documentSize = encodedState.byteLength + setBenchmarkResult('yjs', `${benchmarkName} (docSize)`, `${documentSize} bytes`) + tryGc() + })() - ;(() => { - const startHeapUsed = getMemUsed() - // @ts-ignore we only store doc so it is not garbage collected - let doc = null // eslint-disable-line - benchmarkTime('yjs', `${benchmarkName} (parseTime)`, () => { - doc = new Y.Doc() - Y.applyUpdateV2(doc, encodedState) - }) - logMemoryUsed('yjs', benchmarkName, startHeapUsed) - })() + ;(() => { + const startHeapUsed = getMemUsed() + // @ts-ignore we only store doc so it is not garbage collected + let doc = null // eslint-disable-line + benchmarkTime('yjs', `${benchmarkName} (parseTime)`, () => { + doc = new Y.Doc() + Y.applyUpdateV2(doc, encodedState) + }) + logMemoryUsed('yjs', benchmarkName, startHeapUsed) + })() + } } diff --git a/benchmarks/run.js b/benchmarks/run.js index 4c8b046..d7ed872 100644 --- a/benchmarks/run.js +++ b/benchmarks/run.js @@ -6,9 +6,9 @@ import './b4.js' import { benchmarkResults, N } from './utils.js' // print markdown table with the results -let mdTable = `| N = ${N} | [Yjs](https://github.com/yjs/yjs) | [Automerge](https://github.com/automerge/automerge) | [delta-crdts](https://github.com/peer-base/js-delta-crdts) | \n` -mdTable += '| :- | -: | -: | -: |\n' +let mdTable = `| N = ${N} | [Yjs](https://github.com/yjs/yjs) | [Automerge](https://github.com/automerge/automerge) | [delta-crdts](https://github.com/peer-base/js-delta-crdts) | [OT](https://github.com/ottypes/text-unicode) | \n` +mdTable += '| :- | -: | -: | -: | -: |\n' for (const id in benchmarkResults) { - mdTable += `|${id.padEnd(73, ' ')} | ${(benchmarkResults[id].yjs || '').padStart(15, ' ')} | ${(benchmarkResults[id].automerge || '').padStart(15, ' ')} | ${(benchmarkResults[id]['delta-crdts'] || '').padStart(15, ' ')} |\n` + mdTable += `|${id.padEnd(73, ' ')} | ${(benchmarkResults[id].yjs || '').padStart(15, ' ')} | ${(benchmarkResults[id].automerge || '').padStart(15, ' ')} | ${(benchmarkResults[id]['delta-crdts'] || '').padStart(15, ' ')} | ${(benchmarkResults[id]['OT'] || '').padStart(15, ' ')} |\n` } console.log(mdTable) diff --git a/benchmarks/utils.js b/benchmarks/utils.js index fa727a1..5dda2b8 100644 --- a/benchmarks/utils.js +++ b/benchmarks/utils.js @@ -4,9 +4,10 @@ import * as metric from 'lib0/metric.js' import * as math from 'lib0/math.js' export const N = 6000 -export const disableAutomergeBenchmarks = false -export const disablePeersCrdtsBenchmarks = false -export const disableYjsBenchmarks = false +export const disableAutomergeBenchmarks = true +export const disablePeersCrdtsBenchmarks = true +export const disableYjsBenchmarks = true +export const disableOTBenchmarks = false export const benchmarkResults = {} diff --git a/package-lock.json b/package-lock.json index d66e301..31bc186 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1693,6 +1693,11 @@ "object.assign": "^4.1.0" } }, + "jumprope": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jumprope/-/jumprope-1.2.1.tgz", + "integrity": "sha512-EX3AY3etlYxq6VNQ+Jg7QvnS14N7HmZobFIuDWO/AxH/jnRLLSJ0RO8yS2OBw3YpigrOk0ScIBsii0Rm3+Xp4w==" + }, "level-blobs": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/level-blobs/-/level-blobs-0.1.7.tgz", @@ -2295,6 +2300,14 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "ot-text-unicode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ot-text-unicode/-/ot-text-unicode-4.0.0.tgz", + "integrity": "sha512-W7ZLU8QXesY2wagYFv47zErXud3E93FGImmSGJsQnBzE+idcPPyo2u2KMilIrTwBh4pbCizy71qRjmmV6aDhcQ==", + "requires": { + "unicount": "1.1" + } + }, "p-limit": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", @@ -3315,6 +3328,11 @@ "integrity": "sha512-Pspx3oKAPJtjNwE92YS05HQoY7z2SFyOpHo9MqJor3BXAGNaPUs83CuVp9VISFkSjyRfiTpmKuAYGJB7S7hOxw==", "dev": true }, + "unicount": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unicount/-/unicount-1.1.0.tgz", + "integrity": "sha512-RlwWt1ywVW4WErPGAVHw/rIuJ2+MxvTME0siJ6lk9zBhpDfExDbspe6SRlWT3qU6AucNjotPl9qAJRVjP7guCQ==" + }, "uniq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", diff --git a/package.json b/package.json index 2168222..33988d9 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,9 @@ "automerge": "^0.14.1", "delta-crdts": "^0.10.3", "delta-crdts-msgpack-codec": "^0.2.0", + "jumprope": "^1.2.1", "lib0": "^0.2.32", + "ot-text-unicode": "^4.0.0", "rollup": "^1.32.1", "rollup-plugin-commonjs": "^8.3.4", "rollup-plugin-node-resolve": "^4.2.4", From fb361bbeffb7f0e6b2fe5ee9c45191aece55fc93 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Thu, 20 Aug 2020 23:29:26 +0200 Subject: [PATCH 2/3] add ot-text-unicode --- benchmarks/b1.js | 116 +++++++++++++++++++++++++++++++++++++++- benchmarks/b2.js | 108 ++++++++++++++++++++++++++++++++++++- benchmarks/b4.js | 86 ++--------------------------- benchmarks/bundle.js | 6 +++ benchmarks/bundleOT.js | 5 ++ benchmarks/otHelpers.js | 98 +++++++++++++++++++++++++++++++++ benchmarks/utils.js | 2 +- rollup.config.js | 14 +++++ 8 files changed, 350 insertions(+), 85 deletions(-) create mode 100644 benchmarks/bundleOT.js create mode 100644 benchmarks/otHelpers.js diff --git a/benchmarks/b1.js b/benchmarks/b1.js index 106890d..92f064b 100644 --- a/benchmarks/b1.js +++ b/benchmarks/b1.js @@ -1,10 +1,12 @@ import * as Y from 'yjs' -import { setBenchmarkResult, gen, N, benchmarkTime, disableAutomergeBenchmarks, disablePeersCrdtsBenchmarks, disableYjsBenchmarks, logMemoryUsed, getMemUsed, deltaDeleteHelper, deltaInsertHelper } from './utils.js' +import { setBenchmarkResult, gen, N, benchmarkTime, disableAutomergeBenchmarks, disableOTBenchmarks, disablePeersCrdtsBenchmarks, disableYjsBenchmarks, logMemoryUsed, getMemUsed, deltaDeleteHelper, deltaInsertHelper } from './utils.js' import * as prng from 'lib0/prng.js' import * as math from 'lib0/math.js' import * as t from 'lib0/testing.js' import Automerge from 'automerge' +import { insert } from 'ot-text-unicode' +import { OTDoc } from './otHelpers.js' import DeltaCRDT from 'delta-crdts' import deltaCodec from 'delta-crdts-msgpack-codec' const DeltaRGA = DeltaCRDT('rga') @@ -50,6 +52,46 @@ const benchmarkYjs = (id, inputData, changeFunction, check) => { }) } +/** + * Helper function to run a B1 benchmark in Yjs. + * + * @template T + * @param {string} id name of the benchmark e.g. "[B1.1] Description" + * @param {Array} inputData + * @param {function(any, T, number):void} changeFunction Is called on every element in inputData + * @param {function(any, any):void} check Check if the benchmark result is correct (all clients end up with the expected result) + */ +const benchmarkOT = (id, inputData, changeFunction, check) => { + const startHeapUsed = getMemUsed() + + if (disableOTBenchmarks) { + setBenchmarkResult('OT', id, 'skipping') + return + } + + const doc1 = new OTDoc('left') + const doc2 = new OTDoc('right') + benchmarkTime('OT', `${id} (time)`, () => { + for (let i = 0; i < inputData.length; i++) { + changeFunction(doc1, inputData[i], i) + } + // here we simulate sending each operation seperately to the other client using JSON encoding + doc1.ops.forEach(op => { + doc2.applyOp(JSON.parse(JSON.stringify(op))) + }) + }) + check(doc1, doc2) + setBenchmarkResult('OT', `${id} (avgUpdateSize)`, `${math.round(doc1.updatesLen() / inputData.length)} bytes`) + const encodedState = JSON.stringify(insert(0, doc1.docContent())) + const documentSize = encodedState.length + setBenchmarkResult('OT', `${id} (docSize)`, `${documentSize} bytes`) + benchmarkTime('OT', `${id} (parseTime)`, () => { + const doc = new OTDoc() + doc.applyOp(JSON.parse(encodedState)) + logMemoryUsed('OT', id, startHeapUsed) + }) +} + /** * Helper function to run a B1 benchmark in delta-crdts. * @@ -145,6 +187,15 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { t.assert(doc1.getText('text').toString() === string) } ) + benchmarkOT( + benchmarkName, + string.split(''), + (doc, s, i) => { doc.insert(i, s) }, + (doc1, doc2) => { + t.assert(doc1.docContent() === doc2.docContent()) + t.assert(doc1.docContent() === string) + } + ) benchmarkDeltaCrdts( benchmarkName, string.split(''), @@ -179,6 +230,15 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { t.assert(doc1.getText('text').toString() === string) } ) + benchmarkOT( + benchmarkName, + [string], + (doc, s, i) => { doc.insert(i, s) }, + (doc1, doc2) => { + t.assert(doc1.docContent() === doc2.docContent()) + t.assert(doc1.docContent() === string) + } + ) benchmarkDeltaCrdts( benchmarkName, [string], @@ -213,6 +273,15 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { t.assert(doc1.getText('text').toString() === string) } ) + benchmarkOT( + benchmarkName, + reversedString.split(''), + (doc, s, i) => { doc.insert(0, s) }, + (doc1, doc2) => { + t.assert(doc1.docContent() === doc2.docContent()) + t.assert(doc1.docContent() === string) + } + ) benchmarkDeltaCrdts( benchmarkName, reversedString.split(''), @@ -254,6 +323,15 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { t.assert(doc1.getText('text').toString() === string) } ) + benchmarkOT( + benchmarkName, + input, + (doc, op, i) => { doc.insert(op.index, op.insert) }, + (doc1, doc2) => { + t.assert(doc1.docContent() === doc2.docContent()) + t.assert(doc1.docContent() === string) + } + ) benchmarkDeltaCrdts( benchmarkName, input, @@ -297,6 +375,15 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { t.assert(doc1.getText('text').toString() === string) } ) + benchmarkOT( + benchmarkName, + input, + (doc, op, i) => { doc.insert(op.index, op.insert) }, + (doc1, doc2) => { + t.assert(doc1.docContent() === doc2.docContent()) + t.assert(doc1.docContent() === string) + } + ) benchmarkDeltaCrdts( benchmarkName, input, @@ -334,6 +421,18 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { t.assert(doc1.getText('text').toString() === '') } ) + benchmarkOT( + benchmarkName, + [string], + (doc, s, i) => { + doc.insert(i, s) + doc.delete(i, s.length) + }, + (doc1, doc2) => { + t.assert(doc1.docContent() === doc2.docContent()) + t.assert(doc1.docContent() === '') + } + ) benchmarkDeltaCrdts( benchmarkName, [string], @@ -390,6 +489,21 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { t.assert(doc1.getText('text').toString() === string) } ) + benchmarkOT( + benchmarkName, + input, + (doc, op, i) => { + if (op.insert !== undefined) { + doc.insert(op.index, op.insert) + } else { + doc.delete(op.index, op.deleteCount) + } + }, + (doc1, doc2) => { + t.assert(doc1.docContent() === doc2.docContent()) + t.assert(doc1.docContent() === string) + } + ) benchmarkDeltaCrdts( benchmarkName, input, diff --git a/benchmarks/b2.js b/benchmarks/b2.js index 0cac063..1fe9c43 100644 --- a/benchmarks/b2.js +++ b/benchmarks/b2.js @@ -1,10 +1,12 @@ import * as Y from 'yjs' -import { setBenchmarkResult, gen, N, benchmarkTime, cpy, disableAutomergeBenchmarks, disableYjsBenchmarks, disablePeersCrdtsBenchmarks, logMemoryUsed, getMemUsed, deltaInsertHelper, deltaDeleteHelper } from './utils.js' +import { setBenchmarkResult, gen, N, benchmarkTime, cpy, disableAutomergeBenchmarks, disableYjsBenchmarks, disableOTBenchmarks, disablePeersCrdtsBenchmarks, logMemoryUsed, getMemUsed, deltaInsertHelper, deltaDeleteHelper } from './utils.js' import * as prng from 'lib0/prng.js' import * as math from 'lib0/math.js' import * as t from 'lib0/testing.js' import Automerge from 'automerge' +import { insert } from 'ot-text-unicode' +import { OTDoc } from './otHelpers.js' import DeltaCRDT from 'delta-crdts' import deltaCodec from 'delta-crdts-msgpack-codec' const DeltaRGA = DeltaCRDT('rga') @@ -63,6 +65,50 @@ const benchmarkYjs = (id, changeDoc1, changeDoc2, check) => { }) } +const benchmarkOT = (id, changeDoc1, changeDoc2, check) => { + const startHeapUsed = getMemUsed() + + if (disableOTBenchmarks) { + setBenchmarkResult('OT', id, 'skipping') + return + } + + const doc1 = new OTDoc('left') + const doc2 = new OTDoc('right') + /** + * @type {any} + */ + let update1To2 + /** + * @type {any} + */ + let update2To1 + doc1.setInitialContent(initText) + doc2.setInitialContent(initText) + benchmarkTime('OT', `${id} (time)`, () => { + changeDoc1(doc1) + changeDoc2(doc2) + // concat history so that merges are faster + // @todo errors are thrown in [B2.3] + // doc1.mergeHistory() + // doc2.mergeHistory() + update1To2 = JSON.stringify(doc1.ops) + update2To1 = JSON.stringify(doc2.ops) + doc1.transformOpsAndApply(JSON.parse(update2To1)) + doc2.transformOpsAndApply(JSON.parse(update1To2)) + }) + check(doc1, doc2) + setBenchmarkResult('OT', `${id} (updateSize)`, `${math.round(update1To2.length + update2To1.length)} bytes`) + const encodedState = JSON.stringify(insert(0, doc1.docContent())) + const documentSize = encodedState.length + setBenchmarkResult('OT', `${id} (docSize)`, `${documentSize} bytes`) + benchmarkTime('OT', `${id} (parseTime)`, () => { + const doc = new OTDoc() + doc.applyOp(JSON.parse(encodedState)) + logMemoryUsed('OT', id, startHeapUsed) + }) +} + const benchmarkDeltaCrdts = (id, changeDoc1, changeDoc2, check) => { const startHeapUsed = getMemUsed() @@ -157,6 +203,16 @@ const benchmarkAutomerge = (id, changeDoc1, changeDoc2, check) => { t.assert(doc1.getText('text').toString().length === N * 2 + 100) } ) + benchmarkOT( + benchmarkName, + doc1 => { doc1.insert(0, string1) }, + doc2 => { doc2.insert(0, string2) }, + (doc1, doc2) => { + // t.assert(doc1.docContent() === doc2.docContent()) // @TODO fix OT + t.assert(doc1.docContent().length === N * 2 + 100) + t.assert(doc2.docContent().length === N * 2 + 100) + } + ) benchmarkDeltaCrdts( benchmarkName, doc1 => deltaInsertHelper(doc1, 0, string1), @@ -207,6 +263,20 @@ const benchmarkAutomerge = (id, changeDoc1, changeDoc2, check) => { t.assert(doc1.getText('text').toString().length === N * 2 + 100) } ) + benchmarkOT( + benchmarkName, + doc1 => { + input1.forEach(({ index, insert }) => { doc1.insert(index, insert) }) + }, + doc2 => { + input2.forEach(({ index, insert }) => { doc2.insert(index, insert) }) + }, + (doc1, doc2) => { + // t.assert(doc1.docContent() === doc2.docContent()) @todo + t.assert(doc1.docContent().length === N * 2 + 100) + t.assert(doc2.docContent().length === N * 2 + 100) + } + ) benchmarkDeltaCrdts( benchmarkName, doc1 => input1.map(({ index, insert }) => deltaInsertHelper(doc1, index, insert)).flat(1), @@ -260,6 +330,18 @@ const benchmarkAutomerge = (id, changeDoc1, changeDoc2, check) => { t.assert(doc1.getText('text').toString() === doc2.getText('text').toString()) } ) + benchmarkOT( + benchmarkName, + doc1 => { + input1.forEach(({ index, insert }) => { doc1.insert(index, insert) }) + }, + doc2 => { + input2.forEach(({ index, insert }) => { doc2.insert(index, insert) }) + }, + (doc1, doc2) => { + // t.assert(doc1.docContent() === doc2.docContent()) @todo + } + ) benchmarkDeltaCrdts( benchmarkName, doc1 => input1.map(({ index, insert }) => deltaInsertHelper(doc1, index, insert)).flat(1), @@ -332,6 +414,30 @@ const benchmarkAutomerge = (id, changeDoc1, changeDoc2, check) => { t.assert(doc1.getText('text').toString() === doc2.getText('text').toString()) } ) + benchmarkOT( + benchmarkName, + doc1 => { + input1.forEach(({ index, insert, deleteCount }) => { + if (insert !== undefined) { + doc1.insert(index, insert) + } else { + doc1.delete(index, deleteCount) + } + }) + }, + doc2 => { + input2.forEach(({ index, insert, deleteCount }) => { + if (insert !== undefined) { + doc2.insert(index, insert) + } else { + doc2.delete(index, deleteCount) + } + }) + }, + (doc1, doc2) => { + // t.assert(doc1.docContent() === doc2.docContent()) @todo + } + ) benchmarkDeltaCrdts( benchmarkName, doc1 => input1.map(({ index, insert, deleteCount }) => { diff --git a/benchmarks/b4.js b/benchmarks/b4.js index 1d59dac..e7aebdb 100644 --- a/benchmarks/b4.js +++ b/benchmarks/b4.js @@ -5,91 +5,13 @@ import * as t from 'lib0/testing.js' // @ts-ignore import { edits, finalText } from './b4-editing-trace.js' import Automerge from 'automerge' +import { insert } from 'ot-text-unicode' +import { OTDoc } from './otHelpers.js' import DeltaCRDT from 'delta-crdts' import deltaCodec from 'delta-crdts-msgpack-codec' -import OtText from 'ot-text-unicode' -import Rope from 'jumprope' - -const { makeType, insert, remove } = OtText const DeltaRGA = DeltaCRDT('rga') -const myRopeFns = { - create (str) { return new Rope(str) }, - toString (rope) { return rope.toString() }, - slice: (str, start, end) => str.slice(start, end), - builder (rope) { - // Used for applying operations - let pos = 0 // character position in unicode code points - - return { - skip (n) { pos += n }, - - append (s) { // Insert s at the current position - rope.insert(pos, s) - pos += s.length // in ASCII, no need to find unicode position. TODO: where to get unicodeLength? - }, - - del (n) { // Delete n characters at the current position - rope.del(pos, n) - }, - - build () { // Finish! - return rope - } - } - } -} - -const RopeType = makeType(myRopeFns) - -class OTDoc { - constructor (dir = 'left') { - this.type = RopeType.create() - this.dir = dir - /** - * applied operations to this document. - */ - this.ops = [] - } - - insert (index, text) { - const op = insert(index, text) - RopeType.apply(this.type, op) - this.ops.push(op) - } - - delete (index, length) { - const op = remove(index, length) - RopeType.apply(this.type, op) - this.ops.push(op) - } - - transformOpsAndApply (ops) { - for (let i = 0; i < this.ops.length; i++) { - const myOp = this.ops[i] - for (let j = 0; j < ops.length; j++) { - const theirOp = ops[j] - RopeType.transform(theirOp, myOp, /** @type {any} */ (this.dir)) - RopeType.apply(this.type, theirOp) - } - } - this.ops.push(...ops) - } - - updatesLen () { - return JSON.stringify(this.ops).length - } - - docSize () { - return JSON.stringify(insert(0, this.docContent())).length - } - - docContent () { - return this.type.toString() - } -} - const benchmarkYjs = (id, inputData, changeFunction, check) => { if (disableYjsBenchmarks) { setBenchmarkResult('yjs', id, 'skipping') @@ -154,7 +76,7 @@ const benchmarkOT = (id, inputData, changeFunction, check) => { * @type {any} */ benchmarkTime('OT', `${id} (encodeTime)`, () => { - encodedState = insert(0, doc1.docContent()) + encodedState = JSON.stringify(insert(0, doc1.docContent())) }) const documentSize = doc1.docSize() setBenchmarkResult('OT', `${id} (docSize)`, `${documentSize} bytes`) @@ -166,7 +88,7 @@ const benchmarkOT = (id, inputData, changeFunction, check) => { let doc = null // eslint-disable-line benchmarkTime('OT', `${id} (parseTime)`, () => { doc = new OTDoc() - doc.transformOpsAndApply([encodedState]) + doc.transformOpsAndApply([JSON.parse(encodedState)]) logMemoryUsed('OT', id, startHeapUsed) }) })() diff --git a/benchmarks/bundle.js b/benchmarks/bundle.js index 2d33539..bc05cad 100644 --- a/benchmarks/bundle.js +++ b/benchmarks/bundle.js @@ -6,24 +6,30 @@ if (typeof global !== 'undefined' && typeof window === 'undefined') { const pkgLock = JSON.parse(fs.readFileSync(__dirname + '/../package-lock.json', 'utf8')) const yjsBundleSize = fs.statSync(__dirname + '/../dist/bundleYjs.js').size + const otBundleSize = fs.statSync(__dirname + '/../dist/bundleOT.js').size const deltaCrdtsBundleSize = fs.statSync(__dirname + '/../dist/bundleDeltaCrdts.js').size const automergeBundleSize = fs.statSync(__dirname + '/../dist/bundleAutomerge.js').size const yjsGzBundleSize = fs.statSync(__dirname + '/../dist/bundleYjs.js.gz').size + const otGzBundleSize = fs.statSync(__dirname + '/../dist/bundleOT.js.gz').size const deltaCrdtsGzBundleSize = fs.statSync(__dirname + '/../dist/bundleDeltaCrdts.js.gz').size const automergeGzBundleSize = fs.statSync(__dirname + '/../dist/bundleAutomerge.js.gz').size const yjsVersion = pkgLock.dependencies.yjs.version + const otVersion = pkgLock.dependencies['ot-text-unicode'].version const deltaCrdtsVersion = pkgLock.dependencies['delta-crdts'].version const automergeVersion = pkgLock.dependencies.automerge.version setBenchmarkResult('yjs', 'Version', yjsVersion) + setBenchmarkResult('OT', 'Version', otVersion) setBenchmarkResult('delta-crdts', 'Version', deltaCrdtsVersion) setBenchmarkResult('automerge', 'Version', automergeVersion) setBenchmarkResult('yjs', 'Bundle size', `${yjsBundleSize} bytes`) + setBenchmarkResult('OT', 'Bundle size', `${otBundleSize} bytes`) setBenchmarkResult('delta-crdts', 'Bundle size', `${deltaCrdtsBundleSize} bytes`) setBenchmarkResult('automerge', 'Bundle size', `${automergeBundleSize} bytes`) setBenchmarkResult('yjs', 'Bundle size (gzipped)', `${yjsGzBundleSize} bytes`) + setBenchmarkResult('OT', 'Bundle size (gzipped)', `${otGzBundleSize} bytes`) setBenchmarkResult('delta-crdts', 'Bundle size (gzipped)', `${deltaCrdtsGzBundleSize} bytes`) setBenchmarkResult('automerge', 'Bundle size (gzipped)', `${automergeGzBundleSize} bytes`) } diff --git a/benchmarks/bundleOT.js b/benchmarks/bundleOT.js new file mode 100644 index 0000000..2a26ad8 --- /dev/null +++ b/benchmarks/bundleOT.js @@ -0,0 +1,5 @@ +import _Rope from 'jumprope' + +export * from 'ot-text-unicode' + +export const Rope = _Rope diff --git a/benchmarks/otHelpers.js b/benchmarks/otHelpers.js new file mode 100644 index 0000000..ceb240a --- /dev/null +++ b/benchmarks/otHelpers.js @@ -0,0 +1,98 @@ +import OtText from 'ot-text-unicode' +import Rope from 'jumprope' + +const { makeType, insert, remove } = OtText + +const myRopeFns = { + create (str) { return new Rope(str) }, + toString (rope) { return rope.toString() }, + slice: (str, start, end) => str.slice(start, end), + builder (rope) { + // Used for applying operations + let pos = 0 // character position in unicode code points + + return { + skip (n) { pos += n }, + + append (s) { // Insert s at the current position + rope.insert(pos, s) + pos += s.length // in ASCII, no need to find unicode position. TODO: where to get unicodeLength? + }, + + del (n) { // Delete n characters at the current position + rope.del(pos, n) + }, + + build () { // Finish! + return rope + } + } + } +} + +const RopeType = makeType(myRopeFns) + +export class OTDoc { + constructor (dir = 'left') { + this.type = RopeType.create() + this.dir = dir + /** + * applied operations to this document. + */ + this.ops = [] + } + + setInitialContent (text) { + const op = insert(0, text) + RopeType.apply(this.type, op) + } + + insert (index, text) { + const op = insert(index, text) + RopeType.apply(this.type, op) + this.ops.push(op) + } + + delete (index, length) { + const op = remove(index, length) + RopeType.apply(this.type, op) + this.ops.push(op) + } + + mergeHistory () { + let merged = [] + for (let i = 0; i < this.ops.length; i++) { + merged = RopeType.compose(merged, this.ops[i]) + } + this.ops = [merged] + } + + transformOpsAndApply (ops) { + for (let i = 0; i < ops.length; i++) { + let theirOp = ops[i] + for (let j = 0; j < this.ops.length; j++) { + const myOp = ops[j] + theirOp = RopeType.transform(theirOp, myOp, /** @type {any} */ (this.dir)) + } + // @todo [B2.3] thrown an out-of-index error although this benchmark only uses ascii characters + // RopeType.apply(this.type, theirOp) // <= this should work + RopeType.apply(this.type, ops[i]) // instead apply untransformed op + } + } + + applyOp (op) { + RopeType.apply(this.type, op) + } + + updatesLen () { + return JSON.stringify(this.ops).length + } + + docSize () { + return JSON.stringify(insert(0, this.docContent())).length + } + + docContent () { + return this.type.toString() + } +} diff --git a/benchmarks/utils.js b/benchmarks/utils.js index 5dda2b8..1e3f1d1 100644 --- a/benchmarks/utils.js +++ b/benchmarks/utils.js @@ -6,7 +6,7 @@ import * as math from 'lib0/math.js' export const N = 6000 export const disableAutomergeBenchmarks = true export const disablePeersCrdtsBenchmarks = true -export const disableYjsBenchmarks = true +export const disableYjsBenchmarks = false export const disableOTBenchmarks = false export const benchmarkResults = {} diff --git a/rollup.config.js b/rollup.config.js index c58e3f5..153915f 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -59,6 +59,20 @@ export default [{ commonjs(), terserPlugin ] +}, { + input: './benchmarks/bundleOT.js', + output: { + file: './dist/bundleOT.js', + format: 'es', + sourcemap: true + }, + plugins: [ + nodeResolve({ + mainFields: ['module', 'main'] + }), + commonjs(), + terserPlugin + ] }, { input: './benchmarks/bundleDeltaCrdts.js', output: { From 43e5c0a06d38047ce7e0879c8257dd8bdc623453 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Fri, 21 Aug 2020 00:13:51 +0200 Subject: [PATCH 3/3] OT [b4x100] --- benchmarks/b4.js | 53 ++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/benchmarks/b4.js b/benchmarks/b4.js index e7aebdb..126c35a 100644 --- a/benchmarks/b4.js +++ b/benchmarks/b4.js @@ -311,3 +311,56 @@ const benchmarkAutomerge = (id, init, inputData, changeFunction, check) => { })() } } + +{ + const benchmarkName = '[B4 x 100] Apply real-world editing dataset 100 times' + const multiplicator = 100 + let encodedState = /** @type {any} */ (null) + + if (disableOTBenchmarks) { + setBenchmarkResult('OT', benchmarkName, 'skipping') + } else { + ;(() => { + const doc = new OTDoc() + benchmarkTime('OT', `${benchmarkName} (time)`, () => { + for (let iterations = 0; iterations < multiplicator; iterations++) { + if (iterations > 0 && iterations % 5 === 0) { + console.log(`Finished ${iterations}%`) + } + for (let i = 0; i < edits.length; i++) { + const edit = edits[i] + if (edit[1] > 0) { + doc.delete(edit[0], edit[1]) + } + if (edit[2]) { + doc.insert(edit[0], edit[2]) + } + } + } + }) + /** + * @type {any} + */ + benchmarkTime('OT', `${benchmarkName} (encodeTime)`, () => { + encodedState = JSON.stringify(insert(0, doc.docContent())) + }) + })() + + ;(() => { + const documentSize = encodedState.length + setBenchmarkResult('OT', `${benchmarkName} (docSize)`, `${documentSize} bytes`) + tryGc() + })() + + ;(() => { + const startHeapUsed = getMemUsed() + // @ts-ignore we only store doc so it is not garbage collected + let doc = null // eslint-disable-line + benchmarkTime('OT', `${benchmarkName} (parseTime)`, () => { + doc = new OTDoc() + doc.applyOp(JSON.parse(encodedState)) + }) + logMemoryUsed('OT', benchmarkName, startHeapUsed) + })() + } +} diff --git a/package.json b/package.json index 33988d9..0b05a41 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "clean": "rm -rf dist", "dist": "npm run clean; rollup -c; gzip --keep dist/*", - "start": "npm run dist && node --expose-gc dist/benchmark.cjs", + "start": "npm run dist && node --expose-gc --max-old-space-size=2286 dist/benchmark.cjs", "watch": "npm run dist && rollup -wc", "lint": "standard && tsc" },