Skip to content
This repository was archived by the owner on Dec 15, 2022. It is now read-only.

Commit a9d81d3

Browse files
Antonio ScandurraNathan Sobo
authored andcommitted
Store segment splits in a splay tree
Signed-off-by: Nathan Sobo <[email protected]>
1 parent d6d23d0 commit a9d81d3

File tree

5 files changed

+145
-59
lines changed

5 files changed

+145
-59
lines changed

lib/document-replica.js

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,22 @@
11
const assert = require('assert')
22
const DocumentTree = require('./document-tree')
3+
const SplitTree = require('./split-tree')
34

45
module.exports =
56
class DocumentReplica {
67
constructor (siteId) {
78
assert(siteId !== 0, 'siteId 0 is reserved')
89
this.siteId = siteId
910
this.nextSequenceNumber = 0
10-
this.insertionStartsByOpId = new Map()
11+
this.splitTreesByOpId = new Map()
1112
this.deletedOffsetRangesByOpId = new Map()
1213
this.undoCountsByOpId = new Map()
1314

1415
const firstSegment = {opId: {site: 0, seq: 0}, offset: 0, pos: 0, text: '', nextSplit: null, deletions: new Set()}
15-
this.insertionStartsByOpId.set(opIdToString(firstSegment.opId), firstSegment)
16+
this.splitTreesByOpId.set(opIdToString(firstSegment.opId), new SplitTree(firstSegment))
1617

1718
const lastSegment = {opId: {site: 0, seq: 1}, offset: 0, pos: 1, text: '', nextSplit: null, deletions: new Set()}
18-
this.insertionStartsByOpId.set(opIdToString(lastSegment.opId), lastSegment)
19+
this.splitTreesByOpId.set(opIdToString(lastSegment.opId), new SplitTree(lastSegment))
1920

2021
this.documentTree = new DocumentTree(
2122
firstSegment,
@@ -48,7 +49,7 @@ class DocumentReplica {
4849
deletions: new Set()
4950
}
5051
this.documentTree.insertBetween(left, right, newSegment)
51-
this.insertionStartsByOpId.set(opIdToString(opId), newSegment)
52+
this.splitTreesByOpId.set(opIdToString(opId), new SplitTree(newSegment))
5253

5354
return {
5455
type: 'insert',
@@ -83,7 +84,7 @@ class DocumentReplica {
8384

8485
segment.deletions.add(opIdString)
8586
this.documentTree.splayNode(segment)
86-
this.documentTree.updateDocumentSubtreeExtent(segment)
87+
this.documentTree.updateSubtreeExtent(segment)
8788
if (segment === right) break
8889
segment = this.documentTree.getSuccessor(segment)
8990
}
@@ -107,9 +108,9 @@ class DocumentReplica {
107108
if (newUndoCount <= undoCount) return []
108109

109110
let segmentRangesToUpdate
110-
let insertionStartSegment = this.insertionStartsByOpId.get(opIdString)
111-
if (insertionStartSegment) {
112-
segmentRangesToUpdate = [{startSegment: insertionStartSegment, endSegment: null}]
111+
let insertionSplitTree = this.splitTreesByOpId.get(opIdString)
112+
if (insertionSplitTree) {
113+
segmentRangesToUpdate = [{startSegment: insertionSplitTree.getStart(), endSegment: null}]
113114
} else {
114115
segmentRangesToUpdate = []
115116
const deletedOffsetRanges = this.deletedOffsetRangesByOpId.get(opIdString)
@@ -160,7 +161,7 @@ class DocumentReplica {
160161
this.undoCountsByOpId.set(opIdString, newUndoCount)
161162
for (let i = segmentsToUpdate.length - 1; i >= 0; i--) {
162163
this.documentTree.splayNode(segmentsToUpdate[i])
163-
this.documentTree.updateDocumentSubtreeExtent(segmentsToUpdate[i])
164+
this.documentTree.updateSubtreeExtent(segmentsToUpdate[i])
164165
}
165166

166167
return opsToApply.sort((a, b) => a.pos - b.pos)
@@ -170,21 +171,21 @@ class DocumentReplica {
170171
switch (op.type) {
171172
case 'insert':
172173
return (
173-
this.insertionStartsByOpId.has(opIdToString(op.leftDependencyId)) &&
174-
this.insertionStartsByOpId.has(opIdToString(op.rightDependencyId))
174+
this.splitTreesByOpId.has(opIdToString(op.leftDependencyId)) &&
175+
this.splitTreesByOpId.has(opIdToString(op.rightDependencyId))
175176
)
176177
case 'delete':
177178
for (let i = 0; i < op.offsetRanges.length; i++) {
178179
const insertionIdString = opIdToString(op.offsetRanges[i].opId)
179-
if (!this.insertionStartsByOpId.has(insertionIdString)) {
180+
if (!this.splitTreesByOpId.has(insertionIdString)) {
180181
return false
181182
}
182183
}
183184
return true
184185
case 'undo':
185186
const opIdString = opIdToString(op.opId)
186187
return (
187-
this.insertionStartsByOpId.has(opIdString) ||
188+
this.splitTreesByOpId.has(opIdString) ||
188189
this.deletedOffsetRangesByOpId.has(opIdString)
189190
)
190191
default:
@@ -236,7 +237,7 @@ class DocumentReplica {
236237
deletions: new Set()
237238
}
238239
this.documentTree.insertBetween(leftDependency, rightDependency, newSegment)
239-
this.insertionStartsByOpId.set(opIdToString(opId), newSegment)
240+
this.splitTreesByOpId.set(opIdToString(opId), new SplitTree(newSegment))
240241

241242
if (this.isSegmentVisible(newSegment)) {
242243
return [{
@@ -290,7 +291,7 @@ class DocumentReplica {
290291
while (true) {
291292
node.deletions.add(deletionOpIdString)
292293
this.documentTree.splayNode(node)
293-
this.documentTree.updateDocumentSubtreeExtent(node)
294+
this.documentTree.updateSubtreeExtent(node)
294295
if (node === right) break
295296
node = node.nextSplit
296297
}
@@ -306,54 +307,43 @@ class DocumentReplica {
306307
findLocalSegmentBoundary (position) {
307308
const {segment, start, end} = this.documentTree.findSegmentContainingPosition(position)
308309
if (position < end) {
309-
return this.splitSegment(segment, position - start)
310+
const splitTree = this.splitTreesByOpId.get(opIdToString(segment.opId))
311+
return this.splitSegment(splitTree, segment, position - start)
310312
} else {
311313
return [segment, this.documentTree.getSuccessor(segment)]
312314
}
313315
}
314316

315-
splitSegment (segment, offset) {
316-
const suffix = Object.assign({}, segment)
317-
suffix.text = segment.text.slice(offset)
318-
suffix.opId = Object.assign({}, segment.opId)
319-
suffix.offset += offset
317+
splitSegment (splitTree, segment, offset) {
318+
const suffix = splitTree.splitSegment(segment, offset)
320319
suffix.pos = (segment.pos + this.documentTree.getSuccessor(segment).pos) / 2
321-
suffix.deletions = new Set(suffix.deletions)
322-
segment.text = segment.text.slice(0, offset)
323-
segment.nextSplit = suffix
324320
this.documentTree.splitSegment(segment, suffix)
325321
return [segment, suffix]
326322
}
327323

328324
findSegmentStart (opId, offset) {
329-
let segment = this.insertionStartsByOpId.get(opIdToString(opId))
330-
while (segment) {
331-
const segmentEndOffset = segment.offset + segment.text.length
332-
if (segment.offset === offset) {
333-
return segment
334-
} else if (segmentEndOffset > offset) {
335-
assert(segment.offset < offset)
336-
const [prefix, suffix] = this.splitSegment(segment, offset - segment.offset)
337-
return suffix
338-
}
339-
340-
segment = segment.nextSplit
325+
const splitTree = this.splitTreesByOpId.get(opIdToString(opId))
326+
const segment = splitTree.findSegmentContainingOffset(offset)
327+
const segmentEndOffset = segment.offset + segment.text.length
328+
if (segment.offset === offset) {
329+
return segment
330+
} else if (segmentEndOffset === offset) {
331+
return segment.nextSplit
332+
} else {
333+
const [prefix, suffix] = this.splitSegment(splitTree, segment, offset - segment.offset)
334+
return suffix
341335
}
342336
}
343337

344338
findSegmentEnd (opId, offset) {
345-
let segment = this.insertionStartsByOpId.get(opIdToString(opId))
346-
while (segment) {
347-
const segmentEndOffset = segment.offset + segment.text.length
348-
if (segmentEndOffset === offset) {
349-
return segment
350-
} else if (segmentEndOffset > offset) {
351-
assert(segment.offset < offset)
352-
const [prefix, suffix] = this.splitSegment(segment, offset - segment.offset)
353-
return prefix
354-
}
355-
356-
segment = segment.nextSplit
339+
const splitTree = this.splitTreesByOpId.get(opIdToString(opId))
340+
const segment = splitTree.findSegmentContainingOffset(offset)
341+
const segmentEndOffset = segment.offset + segment.text.length
342+
if (segmentEndOffset === offset) {
343+
return segment
344+
} else {
345+
const [prefix, suffix] = this.splitSegment(splitTree, segment, offset - segment.offset)
346+
return prefix
357347
}
358348
}
359349

lib/document-tree.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ class DocumentTree extends SplayTree {
6767
newSegment.documentRight = next
6868
next.documentParent = newSegment
6969
next.documentLeft = null
70-
this.updateDocumentSubtreeExtent(next)
71-
this.updateDocumentSubtreeExtent(newSegment)
70+
this.updateSubtreeExtent(next)
71+
this.updateSubtreeExtent(newSegment)
7272
newSegment.pos = (prev.pos + next.pos) / 2
7373
}
7474

@@ -83,11 +83,11 @@ class DocumentTree extends SplayTree {
8383
if (suffix.documentRight) suffix.documentRight.documentParent = suffix
8484
prefix.documentRight = null
8585

86-
this.updateDocumentSubtreeExtent(prefix)
87-
this.updateDocumentSubtreeExtent(suffix)
86+
this.updateSubtreeExtent(prefix)
87+
this.updateSubtreeExtent(suffix)
8888
}
8989

90-
updateDocumentSubtreeExtent (node) {
90+
updateSubtreeExtent (node) {
9191
node.documentSubtreeExtent = 0
9292
if (node.documentLeft) node.documentSubtreeExtent += node.documentLeft.documentSubtreeExtent
9393
if (this.isSegmentVisible(node)) node.documentSubtreeExtent += node.text.length

lib/splay-tree.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ class SplayTree {
4141
}
4242
this.setParent(pivot, this.getParent(root))
4343

44-
this.setRight(root, pivot.documentLeft)
44+
this.setRight(root, this.getLeft(pivot))
4545
if (this.getRight(root)) this.setParent(this.getRight(root), root)
4646

4747
this.setLeft(pivot, root)
4848
this.setParent(this.getLeft(pivot), pivot)
4949

50-
this.updateDocumentSubtreeExtent(root)
51-
this.updateDocumentSubtreeExtent(pivot)
50+
this.updateSubtreeExtent(root)
51+
this.updateSubtreeExtent(pivot)
5252
}
5353

5454
rotateNodeRight (pivot) {
@@ -70,8 +70,8 @@ class SplayTree {
7070
this.setRight(pivot, root)
7171
this.setParent(this.getRight(pivot), pivot)
7272

73-
this.updateDocumentSubtreeExtent(root)
74-
this.updateDocumentSubtreeExtent(pivot)
73+
this.updateSubtreeExtent(root)
74+
this.updateSubtreeExtent(pivot)
7575
}
7676

7777
isNodeLeftChild (node) {

lib/split-tree.js

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
const assert = require('assert')
2+
const SplayTree = require('./splay-tree')
3+
4+
module.exports =
5+
class SplitTree extends SplayTree {
6+
constructor (segment) {
7+
super()
8+
this.startSegment = segment
9+
this.startSegment.splitLeft = null
10+
this.startSegment.splitRight = null
11+
this.startSegment.splitParent = null
12+
this.startSegment.splitSubtreeExtent = this.startSegment.text.length
13+
this.root = this.startSegment
14+
}
15+
16+
getStart () {
17+
return this.startSegment
18+
}
19+
20+
getParent (node) {
21+
return node.splitParent
22+
}
23+
24+
setParent (node, value) {
25+
node.splitParent = value
26+
}
27+
28+
getLeft (node) {
29+
return node.splitLeft
30+
}
31+
32+
setLeft (node, value) {
33+
node.splitLeft = value
34+
}
35+
36+
getRight (node) {
37+
return node.splitRight
38+
}
39+
40+
setRight (node, value) {
41+
node.splitRight = value
42+
}
43+
44+
updateSubtreeExtent (node) {
45+
node.splitSubtreeExtent = 0
46+
if (node.splitLeft) node.splitSubtreeExtent += node.splitLeft.splitSubtreeExtent
47+
node.splitSubtreeExtent += node.text.length
48+
if (node.splitRight) node.splitSubtreeExtent += node.splitRight.splitSubtreeExtent
49+
}
50+
51+
findSegmentContainingOffset (offset) {
52+
let segment = this.root
53+
let leftAncestorEnd = 0
54+
while (segment) {
55+
let start = leftAncestorEnd
56+
if (segment.splitLeft) start += segment.splitLeft.splitSubtreeExtent
57+
const end = start + segment.text.length
58+
59+
if (offset <= start && segment.splitLeft) {
60+
segment = segment.splitLeft
61+
} else if (offset > end) {
62+
leftAncestorEnd = end
63+
segment = segment.splitRight
64+
} else {
65+
return segment
66+
}
67+
}
68+
69+
throw new Error('No segment found')
70+
}
71+
72+
splitSegment (segment, offset) {
73+
this.splayNode(segment)
74+
const suffix = Object.assign({}, segment)
75+
suffix.text = segment.text.slice(offset)
76+
suffix.opId = Object.assign({}, segment.opId)
77+
suffix.offset += offset
78+
suffix.deletions = new Set(suffix.deletions)
79+
segment.text = segment.text.slice(0, offset)
80+
segment.nextSplit = suffix
81+
82+
this.root = suffix
83+
suffix.splitParent = null
84+
suffix.splitLeft = segment
85+
segment.splitParent = suffix
86+
suffix.splitRight = segment.splitRight
87+
if (suffix.splitRight) suffix.splitRight.splitParent = suffix
88+
segment.splitRight = null
89+
90+
this.updateSubtreeExtent(segment)
91+
this.updateSubtreeExtent(suffix)
92+
93+
return suffix
94+
}
95+
}

test/document-replica.test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,11 +204,12 @@ suite('DocumentReplica', () => {
204204
assert.equal(replica.getText(), 'A***G')
205205
})
206206

207-
test('replica convergence with random operations', function () {
207+
test.only('replica convergence with random operations', function () {
208208
this.timeout(Infinity)
209209
const initialSeed = Date.now()
210210
const peerCount = 5
211211
for (var i = 0; i < 1000; i++) {
212+
console.log(i);
212213
const peers = Peer.buildNetwork(peerCount, '')
213214
let seed = initialSeed + i
214215
// seed = 1496346683429

0 commit comments

Comments
 (0)