Skip to content

Commit

Permalink
add OT b4
Browse files Browse the repository at this point in the history
  • Loading branch information
dmonad committed Aug 20, 2020
1 parent d7f4d77 commit 8112369
Show file tree
Hide file tree
Showing 5 changed files with 203 additions and 45 deletions.
215 changes: 176 additions & 39 deletions benchmarks/b4.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,95 @@
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
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')
Expand Down Expand Up @@ -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')
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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)
})()
}
}
6 changes: 3 additions & 3 deletions benchmarks/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
7 changes: 4 additions & 3 deletions benchmarks/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}

Expand Down
18 changes: 18 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down

0 comments on commit 8112369

Please sign in to comment.