Skip to content

Commit bd49467

Browse files
Speed up index() using a cache
1 parent 5fb2c81 commit bd49467

File tree

3 files changed

+103
-4
lines changed

3 files changed

+103
-4
lines changed

lib/SymbolTree.js

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -463,15 +463,17 @@ class SymbolTree {
463463
/**
464464
* Find the index of the given object (the number of preceding siblings).
465465
*
466-
* `O(n)`
466+
* `O(n)`<br>
467+
* `O(1)` (cached)
467468
*
468469
* @method index
469470
* @memberOf module:symbol-tree#
470471
* @param {Object} child
471472
* @return {Number} The number of preceding siblings, or -1 if the object has no parent
472473
*/
473474
index(child) {
474-
const parentNode = this._node(this._node(child).parent);
475+
const childNode = this._node(child);
476+
const parentNode = this._node(childNode.parent);
475477

476478
if (!parentNode) {
477479
// In principal, you could also find out the number of preceding siblings
@@ -480,15 +482,25 @@ class SymbolTree {
480482
return -1;
481483
}
482484

483-
let index = 0;
485+
let index = childNode.getCachedIndex(parentNode);
486+
487+
if (index >= 0) {
488+
return index;
489+
}
490+
491+
index = 0;
484492
let object = parentNode.first;
485493

486494
while (object) {
495+
const node = this._node(object);
496+
node.setCachedIndex(parentNode, index);
497+
487498
if (object === child) {
488499
break;
489500
}
501+
490502
++index;
491-
object = this._node(object).next;
503+
object = node.next;
492504
}
493505

494506
return index;
@@ -533,6 +545,10 @@ class SymbolTree {
533545
removeNode.prev = null;
534546
removeNode.next = null;
535547

548+
if (parentNode) {
549+
parentNode.childrenChanged();
550+
}
551+
536552
return removeObject;
537553
}
538554

@@ -572,6 +588,10 @@ class SymbolTree {
572588
parentNode.first = newObject;
573589
}
574590

591+
if (parentNode) {
592+
parentNode.childrenChanged();
593+
}
594+
575595
return newObject;
576596
}
577597

@@ -611,6 +631,10 @@ class SymbolTree {
611631
parentNode.last = newObject;
612632
}
613633

634+
if (parentNode) {
635+
parentNode.childrenChanged();
636+
}
637+
614638
return newObject;
615639
}
616640

@@ -639,6 +663,7 @@ class SymbolTree {
639663
newNode.parent = referenceObject;
640664
referenceNode.first = newObject;
641665
referenceNode.last = newObject;
666+
referenceNode.childrenChanged();
642667
}
643668
else {
644669
this.insertBefore(newObject, referenceNode.first);
@@ -672,6 +697,7 @@ class SymbolTree {
672697
newNode.parent = referenceObject;
673698
referenceNode.first = newObject;
674699
referenceNode.last = newObject;
700+
referenceNode.childrenChanged();
675701
}
676702
else {
677703
this.insertAfter(newObject, referenceNode.last);

lib/SymbolTreeNode.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@ module.exports = class SymbolTreeNode {
88

99
this.first = null;
1010
this.last = null;
11+
12+
/** This value is incremented anytime a children is added or removed */
13+
this.childrenVersion = 0;
14+
15+
/** This value represents the cached node index, as long as
16+
* cachedIndexVersion matches with the childrenVersion of the parent */
17+
this.cachedIndex = -1;
18+
this.cachedIndexVersion = NaN; // NaN is never equal to anything
1119
}
1220

1321
get isAttached() {
@@ -17,4 +25,27 @@ module.exports = class SymbolTreeNode {
1725
get isEmpty() {
1826
return !this.first;
1927
}
28+
29+
childrenChanged() {
30+
/* jshint -W016 */
31+
// integer wrap around
32+
this.childrenVersion = (this.childrenVersion + 1) & 0xFFFFFFFF;
33+
}
34+
35+
getCachedIndex(parentNode) {
36+
// (assumes parentNode is actually the parent)
37+
if (this.cachedIndexVersion !== parentNode.childrenVersion) {
38+
this.cachedIndexVersion = NaN;
39+
// cachedIndex is no longer valid
40+
return -1;
41+
}
42+
43+
return this.cachedIndex; // -1 if not cached
44+
}
45+
46+
setCachedIndex(parentNode, index) {
47+
// (assumes parentNode is actually the parent)
48+
this.cachedIndexVersion = parentNode.childrenVersion;
49+
this.cachedIndex = index;
50+
}
2051
};

test/SymbolTree.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -918,3 +918,45 @@ test('look up the index of an object', function(t) {
918918

919919
t.end();
920920
});
921+
922+
test('cached index', function(t) {
923+
const tree = new SymbolTree();
924+
const a = {};
925+
const aa = {};
926+
const ab = {};
927+
const aba = {};
928+
const ac = {};
929+
const b = {};
930+
931+
tree.insertLast(aa, a);
932+
tree.insertLast(ab, a);
933+
tree.insertLast(aba, ab);
934+
tree.insertLast(ac, a);
935+
tree.insertAfter(b, a);
936+
937+
// looking up ac, will also set the cached index for aa and ab, so check that those are valid
938+
t.equal(2, tree.index(ac));
939+
t.equal(1, tree.index(ab));
940+
t.equal(0, tree.index(aa));
941+
942+
// removing something should invalidate the cache
943+
tree.remove(ab);
944+
t.equal(1, tree.index(ac));
945+
t.equal(-1, tree.index(ab));
946+
t.equal(0, tree.index(aa));
947+
948+
// insertAfter should invalidate
949+
tree.insertAfter(ab, aa);
950+
t.equal(0, tree.index(aa));
951+
t.equal(1, tree.index(ab));
952+
t.equal(2, tree.index(ac));
953+
954+
// insertBefore should invalidate
955+
const foo = {};
956+
tree.insertBefore(foo, ab);
957+
t.equal(0, tree.index(aa));
958+
t.equal(2, tree.index(ab));
959+
t.equal(3, tree.index(ac));
960+
961+
t.end();
962+
});

0 commit comments

Comments
 (0)