Skip to content

Commit 056e7d6

Browse files
authored
Merge pull request #16 from taylorsw04/master
feat: Add optimized diff routine
2 parents 79e9e95 + 9b9bf05 commit 056e7d6

File tree

5 files changed

+761
-425
lines changed

5 files changed

+761
-425
lines changed

b+tree.d.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,41 @@ export default class BTree<K = any, V = any> implements ISortedMapF<K, V>, ISort
254254
*/
255255
entriesReversed(highestKey?: K, reusedArray?: (K | V)[], skipHighest?: boolean): IterableIterator<[K, V]>;
256256
private findPath;
257+
/**
258+
* Computes the differences between `this` and `other`.
259+
* For efficiency, the diff is returned via invocations of supplied handlers.
260+
* The computation is optimized for the case in which the two trees have large amounts
261+
* of shared data (obtained by calling the `clone` or `with` APIs) and will avoid
262+
* any iteration of shared state.
263+
* The handlers can cause computation to early exit by returning {break: R}.
264+
* @param other The tree to compute a diff against.
265+
* @param onlyThis Callback invoked for all keys only present in `this`.
266+
* @param onlyOther Callback invoked for all keys only present in `other`.
267+
* @param different Callback invoked for all keys with differing values.
268+
*/
269+
diff<R>(other: BTree<K, V>, onlyThis?: (k: K, v: V) => {
270+
break?: R;
271+
} | void, onlyOther?: (k: K, v: V) => {
272+
break?: R;
273+
} | void, different?: (k: K, vThis: V, vOther: V) => {
274+
break?: R;
275+
} | void): R | undefined;
276+
private static finishCursorWalk;
277+
private static stepToEnd;
278+
private static makeDiffCursor;
279+
/**
280+
* Advances the cursor to the next step in the walk of its tree.
281+
* Cursors are walked backwards in sort order, as this allows them to leverage maxKey() in order to be compared in O(1).
282+
* @param cursor The cursor to step
283+
* @param stepToNode If true, the cursor will be advanced to the next node (skipping values)
284+
* @returns true if the step was completed and false if the step would have caused the cursor to move beyond the end of the tree.
285+
*/
286+
private static step;
287+
/**
288+
* Compares the two cursors. Returns a value indicating which cursor is ahead in a walk.
289+
* Note that cursors are advanced in reverse sorting order.
290+
*/
291+
private static compare;
257292
/** Returns a new iterator for iterating the keys of each pair in ascending order.
258293
* @param firstKey: Minimum key to include in the output. */
259294
keys(firstKey?: K): IterableIterator<K>;

b+tree.js

Lines changed: 243 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,242 @@ var BTree = /** @class */ (function () {
488488
}
489489
return { nodequeue: nodequeue, nodeindex: nodeindex, leaf: nextnode };
490490
};
491+
/**
492+
* Computes the differences between `this` and `other`.
493+
* For efficiency, the diff is returned via invocations of supplied handlers.
494+
* The computation is optimized for the case in which the two trees have large amounts
495+
* of shared data (obtained by calling the `clone` or `with` APIs) and will avoid
496+
* any iteration of shared state.
497+
* The handlers can cause computation to early exit by returning {break: R}.
498+
* @param other The tree to compute a diff against.
499+
* @param onlyThis Callback invoked for all keys only present in `this`.
500+
* @param onlyOther Callback invoked for all keys only present in `other`.
501+
* @param different Callback invoked for all keys with differing values.
502+
*/
503+
BTree.prototype.diff = function (other, onlyThis, onlyOther, different) {
504+
if (other._compare !== this._compare) {
505+
throw new Error("Tree comparators are not the same.");
506+
}
507+
if (this.isEmpty || other.isEmpty) {
508+
if (this.isEmpty && other.isEmpty)
509+
return undefined;
510+
// If one tree is empty, everything will be an onlyThis/onlyOther.
511+
if (this.isEmpty)
512+
return onlyOther === undefined ? undefined : BTree.stepToEnd(BTree.makeDiffCursor(other), onlyOther);
513+
return onlyThis === undefined ? undefined : BTree.stepToEnd(BTree.makeDiffCursor(this), onlyThis);
514+
}
515+
// Cursor-based diff algorithm is as follows:
516+
// - Until neither cursor has navigated to the end of the tree, do the following:
517+
// - If the `this` cursor is "behind" the `other` cursor (strictly <, via compare), advance it.
518+
// - Otherwise, advance the `other` cursor.
519+
// - Any time a cursor is stepped, perform the following:
520+
// - If either cursor points to a key/value pair:
521+
// - If thisCursor === otherCursor and the values differ, it is a Different.
522+
// - If thisCursor > otherCursor and otherCursor is at a key/value pair, it is an OnlyOther.
523+
// - If thisCursor < otherCursor and thisCursor is at a key/value pair, it is an OnlyThis as long as the most recent
524+
// cursor step was *not* otherCursor advancing from a tie. The extra condition avoids erroneous OnlyOther calls
525+
// that would occur due to otherCursor being the "leader".
526+
// - Otherwise, if both cursors point to nodes, compare them. If they are equal by reference (shared), skip
527+
// both cursors to the next node in the walk.
528+
// - Once one cursor has finished stepping, any remaining steps (if any) are taken and key/value pairs are logged
529+
// as OnlyOther (if otherCursor is stepping) or OnlyThis (if thisCursor is stepping).
530+
// This algorithm gives the critical guarantee that all locations (both nodes and key/value pairs) in both trees that
531+
// are identical by value (and possibly by reference) will be visited *at the same time* by the cursors.
532+
// This removes the possibility of emitting incorrect diffs, as well as allowing for skipping shared nodes.
533+
var _compare = this._compare;
534+
var thisCursor = BTree.makeDiffCursor(this);
535+
var otherCursor = BTree.makeDiffCursor(other);
536+
// It doesn't matter how thisSteppedLast is initialized.
537+
// Step order is only used when either cursor is at a leaf, and cursors always start at a node.
538+
var thisSuccess = true, otherSuccess = true, prevCursorOrder = BTree.compare(thisCursor, otherCursor, _compare);
539+
while (thisSuccess && otherSuccess) {
540+
var cursorOrder = BTree.compare(thisCursor, otherCursor, _compare);
541+
var thisLeaf = thisCursor.leaf, thisInternalSpine = thisCursor.internalSpine, thisLevelIndices = thisCursor.levelIndices;
542+
var otherLeaf = otherCursor.leaf, otherInternalSpine = otherCursor.internalSpine, otherLevelIndices = otherCursor.levelIndices;
543+
if (thisLeaf || otherLeaf) {
544+
// If the cursors were at the same location last step, then there is no work to be done.
545+
if (prevCursorOrder !== 0) {
546+
if (cursorOrder === 0) {
547+
if (thisLeaf && otherLeaf && different) {
548+
// Equal keys, check for modifications
549+
var valThis = thisLeaf.values[thisLevelIndices[thisLevelIndices.length - 1]];
550+
var valOther = otherLeaf.values[otherLevelIndices[otherLevelIndices.length - 1]];
551+
if (!Object.is(valThis, valOther)) {
552+
var result = different(thisCursor.currentKey, valThis, valOther);
553+
if (result && (result === null || result === void 0 ? void 0 : result.break))
554+
return result.break;
555+
}
556+
}
557+
}
558+
else if (cursorOrder > 0) {
559+
// If this is the case, we know that either:
560+
// 1. otherCursor stepped last from a starting position that trailed thisCursor, and is still behind, or
561+
// 2. thisCursor stepped last and leapfrogged otherCursor
562+
// Either of these cases is an "only other"
563+
if (otherLeaf && onlyOther) {
564+
var otherVal = otherLeaf.values[otherLevelIndices[otherLevelIndices.length - 1]];
565+
var result = onlyOther(otherCursor.currentKey, otherVal);
566+
if (result && result.break)
567+
return result.break;
568+
}
569+
}
570+
else if (onlyThis) {
571+
if (thisLeaf && prevCursorOrder !== 0) {
572+
var valThis = thisLeaf.values[thisLevelIndices[thisLevelIndices.length - 1]];
573+
var result = onlyThis(thisCursor.currentKey, valThis);
574+
if (result && result.break)
575+
return result.break;
576+
}
577+
}
578+
}
579+
}
580+
else if (!thisLeaf && !otherLeaf && cursorOrder === 0) {
581+
var lastThis = thisInternalSpine.length - 1;
582+
var lastOther = otherInternalSpine.length - 1;
583+
var nodeThis = thisInternalSpine[lastThis][thisLevelIndices[lastThis]];
584+
var nodeOther = otherInternalSpine[lastOther][otherLevelIndices[lastOther]];
585+
if (nodeOther === nodeThis) {
586+
prevCursorOrder = 0;
587+
thisSuccess = BTree.step(thisCursor, true);
588+
otherSuccess = BTree.step(otherCursor, true);
589+
continue;
590+
}
591+
}
592+
prevCursorOrder = cursorOrder;
593+
if (cursorOrder < 0) {
594+
thisSuccess = BTree.step(thisCursor);
595+
}
596+
else {
597+
otherSuccess = BTree.step(otherCursor);
598+
}
599+
}
600+
if (thisSuccess && onlyThis)
601+
return BTree.finishCursorWalk(thisCursor, otherCursor, _compare, onlyThis);
602+
if (otherSuccess && onlyOther)
603+
return BTree.finishCursorWalk(otherCursor, thisCursor, _compare, onlyOther);
604+
};
605+
BTree.finishCursorWalk = function (cursor, cursorFinished, compareKeys, callback) {
606+
var compared = BTree.compare(cursor, cursorFinished, compareKeys);
607+
if (compared === 0) {
608+
if (!BTree.step(cursor))
609+
return undefined;
610+
}
611+
else if (compared < 0) {
612+
check(false, "cursor walk terminated early");
613+
}
614+
return BTree.stepToEnd(cursor, callback);
615+
};
616+
BTree.stepToEnd = function (cursor, callback) {
617+
var canStep = true;
618+
while (canStep) {
619+
var leaf = cursor.leaf, levelIndices = cursor.levelIndices, currentKey = cursor.currentKey;
620+
if (leaf) {
621+
var value = leaf.values[levelIndices[levelIndices.length - 1]];
622+
var result = callback(currentKey, value);
623+
if (result && result.break)
624+
return result.break;
625+
}
626+
canStep = BTree.step(cursor);
627+
}
628+
return undefined;
629+
};
630+
BTree.makeDiffCursor = function (tree) {
631+
var _root = tree._root, height = tree.height;
632+
return { height: height, internalSpine: [[_root]], levelIndices: [0], leaf: undefined, currentKey: _root.maxKey() };
633+
};
634+
/**
635+
* Advances the cursor to the next step in the walk of its tree.
636+
* Cursors are walked backwards in sort order, as this allows them to leverage maxKey() in order to be compared in O(1).
637+
* @param cursor The cursor to step
638+
* @param stepToNode If true, the cursor will be advanced to the next node (skipping values)
639+
* @returns true if the step was completed and false if the step would have caused the cursor to move beyond the end of the tree.
640+
*/
641+
BTree.step = function (cursor, stepToNode) {
642+
if (stepToNode === void 0) { stepToNode = false; }
643+
var internalSpine = cursor.internalSpine, levelIndices = cursor.levelIndices, leaf = cursor.leaf;
644+
if (stepToNode || leaf) {
645+
var levelsLength = levelIndices.length;
646+
// Step to the next node only if:
647+
// - We are explicitly directed to via stepToNode, or
648+
// - There are no key/value pairs left to step to in this leaf
649+
if (stepToNode || levelIndices[levelsLength - 1] === 0) {
650+
var spineLength = internalSpine.length;
651+
// Root is leaf
652+
if (spineLength === 0)
653+
return false;
654+
// Walk back up the tree until we find a new subtree to descend into
655+
var nodeLevelIndex = spineLength - 1;
656+
var levelIndexWalkBack = nodeLevelIndex;
657+
while (levelIndexWalkBack >= 0) {
658+
var childIndex = levelIndices[levelIndexWalkBack];
659+
if (childIndex > 0) {
660+
if (levelIndexWalkBack < levelsLength - 1) {
661+
// Remove leaf state from cursor
662+
cursor.leaf = undefined;
663+
levelIndices.splice(levelIndexWalkBack + 1, levelsLength - levelIndexWalkBack);
664+
}
665+
// If we walked upwards past any internal node, splice them out
666+
if (levelIndexWalkBack < nodeLevelIndex)
667+
internalSpine.splice(levelIndexWalkBack + 1, spineLength - levelIndexWalkBack);
668+
// Move to new internal node
669+
var nodeIndex = --levelIndices[levelIndexWalkBack];
670+
cursor.currentKey = internalSpine[levelIndexWalkBack][nodeIndex].maxKey();
671+
return true;
672+
}
673+
levelIndexWalkBack--;
674+
}
675+
// Cursor is in the far left leaf of the tree, no more nodes to enumerate
676+
return false;
677+
}
678+
else {
679+
// Move to new leaf value
680+
var valueIndex = --levelIndices[levelsLength - 1];
681+
cursor.currentKey = leaf.keys[valueIndex];
682+
return true;
683+
}
684+
}
685+
else { // Cursor does not point to a value in a leaf, so move downwards
686+
var nextLevel = internalSpine.length;
687+
var currentLevel = nextLevel - 1;
688+
var node = internalSpine[currentLevel][levelIndices[currentLevel]];
689+
if (node.isLeaf) {
690+
// Entering into a leaf. Set the cursor to point at the last key/value pair.
691+
cursor.leaf = node;
692+
var valueIndex = levelIndices[nextLevel] = node.values.length - 1;
693+
cursor.currentKey = node.keys[valueIndex];
694+
}
695+
else {
696+
var children = node.children;
697+
internalSpine[nextLevel] = children;
698+
var childIndex = children.length - 1;
699+
levelIndices[nextLevel] = childIndex;
700+
cursor.currentKey = children[childIndex].maxKey();
701+
}
702+
return true;
703+
}
704+
};
705+
/**
706+
* Compares the two cursors. Returns a value indicating which cursor is ahead in a walk.
707+
* Note that cursors are advanced in reverse sorting order.
708+
*/
709+
BTree.compare = function (cursorA, cursorB, compareKeys) {
710+
var heightA = cursorA.height, currentKeyA = cursorA.currentKey, levelIndicesA = cursorA.levelIndices;
711+
var heightB = cursorB.height, currentKeyB = cursorB.currentKey, levelIndicesB = cursorB.levelIndices;
712+
// Reverse the comparison order, as cursors are advanced in reverse sorting order
713+
var keyComparison = compareKeys(currentKeyB, currentKeyA);
714+
if (keyComparison !== 0) {
715+
return keyComparison;
716+
}
717+
// Normalize depth values relative to the shortest tree.
718+
// This ensures that concurrent cursor walks of trees of differing heights can reliably land on shared nodes at the same time.
719+
// To accomplish this, a cursor that is on an internal node at depth D1 with maxKey X is considered "behind" a cursor on an
720+
// internal node at depth D2 with maxKey Y, when D1 < D2. Thus, always walking the cursor that is "behind" will allow the cursor
721+
// at shallower depth (but equal maxKey) to "catch up" and land on shared nodes.
722+
var heightMin = heightA < heightB ? heightA : heightB;
723+
var depthANormalized = levelIndicesA.length - (heightA - heightMin);
724+
var depthBNormalized = levelIndicesB.length - (heightB - heightMin);
725+
return depthANormalized - depthBNormalized;
726+
};
491727
/** Returns a new iterator for iterating the keys of each pair in ascending order.
492728
* @param firstKey: Minimum key to include in the output. */
493729
BTree.prototype.keys = function (firstKey) {
@@ -736,9 +972,13 @@ var BTree = /** @class */ (function () {
736972
/** Gets the height of the tree: the number of internal nodes between the
737973
* BTree object and its leaf nodes (zero if there are no internal nodes). */
738974
get: function () {
739-
for (var node = this._root, h = -1; node != null; h++)
740-
node = node.children;
741-
return h;
975+
var node = this._root;
976+
var height = -1;
977+
while (node) {
978+
height++;
979+
node = node.isLeaf ? undefined : node.children[0];
980+
}
981+
return height;
742982
},
743983
enumerable: false,
744984
configurable: true

0 commit comments

Comments
 (0)