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",