Skip to content

Commit

Permalink
Use Topolist for tip ordering (#53)
Browse files Browse the repository at this point in the history
* add topolist

* flush is user facing api

* update linearizer update format to track undo directly

* linearizer uses topolist

* linearizer test fixture needs reordering

* use Topolist compare method wherever we compare nodes

---------

Co-authored-by: Mathias Buus <[email protected]>
  • Loading branch information
chm-diederichs and mafintosh authored Feb 21, 2024
1 parent ca62333 commit a37732a
Show file tree
Hide file tree
Showing 6 changed files with 690 additions and 180 deletions.
2 changes: 1 addition & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1334,7 +1334,7 @@ module.exports = class Autobase extends ReadyResource {
async _applyUpdate (u) {
await this._viewStore.flush()

if (u.popped) this._undo(u.popped)
if (u.undo) this._undo(u.undo)

// if anything was indexed reset the ticks
if (u.indexed.length) this._resetAckTick()
Expand Down
124 changes: 5 additions & 119 deletions lib/linearizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const assert = require('nanoassert')

const Clock = require('./clock')
const Consensus = require('./consensus')
const Topolist = require('./topolist')

class Node {
constructor (writer, length, value, heads, batch, dependencies, version) {
Expand Down Expand Up @@ -70,7 +71,7 @@ module.exports = class Linearizer {
constructor (indexers, { heads = [], writers = new Map() } = {}) {
this.heads = new Set()
this.tails = new Set()
this.tip = []
this.tip = new Topolist()
this.size = 0 // useful for debugging
this.updated = false
this.indexersUpdated = false
Expand Down Expand Up @@ -117,18 +118,12 @@ module.exports = class Linearizer {
}
}

this.tip.add(node)
if (node.writer.isIndexer) this.consensus.addHead(node)

this.size++
this.heads.add(node)

if (this.heads.size === 1 && (this.updated === false || this._strictlyAdded !== null)) {
if (this._strictlyAdded === null) this._strictlyAdded = []
this._strictlyAdded.push(node)
} else {
this._strictlyAdded = null
}

this.updated = true

return node
Expand All @@ -153,77 +148,7 @@ module.exports = class Linearizer {
}
}

const diff = this._maybeStrictlyAdded(indexed)
if (diff !== null) return diff

let pushed = 0
let popped = 0

const list = this._orderTip()

const dirtyList = indexed.length ? indexed.concat(list) : list
const min = Math.min(dirtyList.length, this.tip.length)

let same = true
let shared = 0

for (; shared < min; shared++) {
if (dirtyList[shared] === this.tip[shared]) {
continue
}

same = false
popped = this.tip.length - shared
pushed = dirtyList.length - shared
break
}

if (same) {
pushed = dirtyList.length - this.tip.length
}

this.tip = list

const update = {
shared,
popped,
pushed,
length: shared + pushed,
indexed,
tip: list
}

return update
}

_maybeStrictlyAdded (indexed) {
if (this._strictlyAdded === null) return null

const added = this._strictlyAdded
this._strictlyAdded = null

for (let i = 0; i < indexed.length; i++) {
const node = indexed[i]
const other = i < this.tip.length ? this.tip[i] : added[i - this.tip.length]
if (node !== other) return null
}

const shared = this.tip.length

this.tip.push(...added)

const length = this.tip.length

if (indexed.length) this.tip = this.tip.slice(indexed.length)

return {
shared,
popped: 0,
pushed: added.length,
length,
indexed,
tip: this.tip
}
return this.tip.flush(indexed)
}

_updateInitialHeads (node) {
Expand All @@ -236,39 +161,6 @@ module.exports = class Linearizer {
}
}

/* Tip ordering methods */

_orderTip () {
const tip = []
const stack = [...this.tails]

while (stack.length) {
const node = stack.pop()
if (node.ordering) continue

node.ordering = node.dependencies.size
stack.push(...node.dependents)
}

stack.push(...this.tails)
stack.sort(keySort)

while (stack.length) {
const node = stack.pop()
tip.push(node)

const batch = []

for (const dep of node.dependents) {
if (--dep.ordering === 0) batch.push(dep)
}

if (batch.length > 0) stack.push(...batch.sort(keySort))
}

return tip
}

/* Ack methods */

shouldAck (writer, pending = false) {
Expand Down Expand Up @@ -465,20 +357,14 @@ module.exports = class Linearizer {
}
}

// if same key, earlier node is first
function tieBreak (a, b) {
return keySort(a, b) > 0 // keySort sorts high to low
return Topolist.compare(a, b) < 0 // lowest key wis
}

function getFirst (set) {
return set[Symbol.iterator]().next().value
}

function keySort (a, b) {
const cmp = b4a.compare(a.writer.core.key, b.writer.core.key)
return cmp === 0 ? b.length - a.length : -cmp
}

function sameNode (a, b) {
return b4a.equals(a.key, b.writer.core.key) && a.length === b.length
}
108 changes: 108 additions & 0 deletions lib/topolist.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
const b4a = require('b4a')
const assert = require('nanoassert')

module.exports = class TopoList {
constructor () {
this.tip = []
this.undo = 0
this.shared = 0
}

static compare (a, b) {
return cmp(a, b)
}

mark () {
this.shared = this.tip.length
this.undo = 0
}

// todo: bump to new api that just tracks undo
flush (indexed = []) {
if (indexed.length) this._applyIndexed(indexed)

const u = {
shared: this.shared,
undo: this.undo,
length: indexed.length + this.tip.length,
indexed,
tip: this.tip
}

this.mark()

return u
}

print () {
return this.tip.map(n => n.writer.core.key.toString() + n.length)
}

_applyIndexed (nodes) {
assert(nodes.length <= this.tip.length, 'Indexed batch cannot exceed tip')

let shared = 0

for (; shared < nodes.length; shared++) {
if (this.tip[shared] !== nodes[shared]) break
}

// reordering
if (shared < nodes.length) this._track(shared)

let j = 0
for (let i = shared; i < this.tip.length; i++) {
const node = this.tip[i]
if (node.yielded) continue

this.tip[j++] = node
}

this.tip.splice(j, this.tip.length - j)
}

add (node) {
const shared = addSorted(node, this.tip)
this._track(shared)
}

_track (shared) {
if (shared < this.shared) {
this.undo += this.shared - shared
this.shared = shared
}
}
}

function addSorted (node, list) {
list.push(node)

let i = list.length - 1

while (i >= 1) {
const prev = list[i - 1]
if (links(node, prev)) break
list[i] = prev
list[--i] = node
}

while (i < list.length - 1) {
const next = list[i + 1]
const c = cmp(node, next)
if (c <= 0) break
list[i] = next
list[++i] = node
}

return i
}

function links (a, b) {
if (a.dependencies.has(b)) return true
return a.length > 0 && b.length === a.length - 1 && a.writer === b.writer
}

function cmp (a, b) {
const c = b4a.compare(a.writer.core.key, b.writer.core.key)
return c === 0 ? a.length < b.length ? -1 : 1 : c
}
Loading

0 comments on commit a37732a

Please sign in to comment.