From eda31da3ea26a7fa26538d1376ea8cd4ad9d649b Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sat, 2 Apr 2016 21:23:54 -0700 Subject: [PATCH] add a bst for mempool. --- lib/bcoin/bst.js | 754 +++++++++++++++++++++++++++++++++++++++++++++++ lib/bcoin/ldb.js | 3 + 2 files changed, 757 insertions(+) create mode 100644 lib/bcoin/bst.js diff --git a/lib/bcoin/bst.js b/lib/bcoin/bst.js new file mode 100644 index 00000000..e8d5b7ec --- /dev/null +++ b/lib/bcoin/bst.js @@ -0,0 +1,754 @@ +/** + * bst.js - iterative binary search tree for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var bcoin = require('../bcoin'); +var utils = require('./utils'); +var assert = utils.assert; +var DUMMY = new Buffer([0]); + +/** + * BST + */ + +function BST(location, options) { + if (!(this instanceof BST)) + return new BST(location, options); + + if (!options) + options = {}; + + this.options = options; + this.root = null; + this.compare = options.compare || utils.cmp; +} + +BST.prototype.search = function search(key, options) { + var current = this.root; + var cmp; + + if (typeof key === 'string') + key = new Buffer(key, 'ascii'); + + if (!options) + options = {}; + + while (current) { + cmp = this.compare(key, current.key); + if (cmp === 0) { + return options.asBuffer === false + ? current.value.toString('utf8') + : current.value; + } + if (cmp < 0) + current = current.left; + else + current = current.right; + } +}; + +BST.prototype.insert = function insert(key, value, options) { + var current = this.root; + var left = false; + var parent, cmp; + + if (typeof key === 'string') + key = new Buffer(key, 'ascii'); + + if (typeof value === 'string') + value = new Buffer(value, 'utf8'); + + if (!options) + options = {}; + + while (current) { + cmp = this.compare(key, current.key); + + if (cmp === 0) { + current.value = value; + return; + } + + if (cmp < 0) { + parent = current; + left = true; + current = current.left; + } else { + parent = current; + left = false; + current = current.right; + } + } + + if (!parent) { + this.root = { key: key, value: value }; + return; + } + + if (left) + parent.left = { key: key, value: value }; + else + parent.right = { key: key, value: value }; +}; + +BST.prototype.remove = function remove(key, options) { + var current = this.root; + var left = false; + var parent, use; + + if (typeof key === 'string') + key = new Buffer(key, 'ascii'); + + if (!options) + options = {}; + + while (current) { + cmp = this.compare(key, current.key); + + if (cmp === 0) + break; + + if (cmp < 0) { + parent = current; + left = true; + current = current.left; + } else { + parent = current; + left = false; + current = current.right; + } + } + + if (!current) + return false; + + if (!current.left && !current.right) { + if (!parent) { + this.root = null; + } else { + if (left) + parent.left = null; + else + parent.right = null; + } + + return true; + } + + if (!current.left || !current.right) { + if (current.left) + child = current.left; + else + child = current.right; + + if (!parent) { + this.root = child; + } else { + if (left) + parent.left = child; + else + parent.right = child; + } + + return true; + } + + parent = current; + use = current.left; + left = true; + while (use.right) { + parent = use; + use = use.right; + left = false; + } + + current.key = use.key; + current.value = use.value; + + if (left) + current.left = use.left; + else + parent.right = use.left; + + return true; +}; + +BST.prototype.snapshot = function snapshot() { + var current = this.root; + var stack = []; + var left = true; + var parent, copy, snapshot; + + for (;;) { + if (current) { + if (left) { + copy = clone(current); + if (parent) + parent.left = copy; + else + snapshot = copy; + } else { + copy = clone(current); + if (parent) + parent.right = copy; + else + snapshot = copy; + } + stack.push(copy); + parent = copy; + left = true; + current = current.left; + continue; + } + + if (stack.length === 0) + break; + + current = stack.pop(); + parent = current; + left = false; + current = current.right; + } + + return snapshot; +}; + +BST.prototype.traverse = function traverse(options, test) { + var current = this.root; + var stack = []; + var items = []; + + if (!options) + options = {}; + + for (;;) { + if (current) { + if (test(current)) { + items.push({ + key: options.keyAsBuffer === false + ? current.key.toString('ascii') + : current.key, + value: options.valueAsBuffer === false + ? current.value.toString('utf8') + : current.value + }); + } + stack.push(current); + current = current.left; + continue; + } + + if (stack.length === 0) + break; + + current = stack.pop(); + current = current.right; + } + + return items; +}; + +BST.prototype.dump = function dump(options) { + return this.traverse(options, function() { return true; }); +}; + +BST.prototype.range = function range(gte, lte, options) { + var current = this.root; + var stack = []; + var items = []; + var cmp; + + if (typeof gte === 'string') + gte = new Buffer(gte, 'ascii'); + + if (typeof lte === 'string') + lte = new Buffer(lte, 'ascii'); + + if (!options) + options = {}; + + for (;;) { + if (current) { + cmp = this.rangeCompare(current.key, gte, lte); + if (cmp === 0) { + items.push({ + key: options.keyAsBuffer === false + ? current.key.toString('ascii') + : current.key, + value: options.valueAsBuffer === false + ? current.value.toString('utf8') + : current.value + }); + stack.push(current); + } + if (cmp <= 0) + current = current.left; + else + current = current.right; + continue; + } + + if (stack.length === 0) + break; + + current = stack.pop(); + current = current.right; + } + + return items; +}; + +BST.prototype.rangeCompare = function rangeCompare(key, gteKey, lteKey) { + var gte, lte; + + if (gteKey) + gte = this.compare(key, gteKey); + else + gte = 0; + + if (lteKey) + lte = this.compare(key, lteKey); + else + lte = 0; + + if (gte >= 0 && lte <= 0) + return 0; + + if (lte > 0) + return -1; + + if (gte < 0) + return 1; + + assert(false); +}; + +/** + * Leveldown Methods + */ + +BST.prototype.open = function open(options, callback) { + this.options = options; + return utils.nextTick(callback); +}; + +BST.prototype.close = function close(callback) { + return utils.nextTick(callback); +}; + +BST.prototype.get = function get(key, options, callback) { + var item, err; + + if (!callback) { + callback = options; + options = null; + } + + if (!options) + options = {}; + + item = this.search(key, options); + + if (!item) { + err = new Error('BST_NOTFOUND: Key not found.'); + err.notFound = true; + err.type = 'NotFoundError'; + return utils.asyncify(callback)(err); + } + + return utils.asyncify(callback)(null, item); +}; + +BST.prototype.put = function put(key, value, options, callback) { + var item; + + if (!callback) { + callback = options; + options = null; + } + + if (!options) + options = {}; + + this.insert(key, value, options); + + return utils.nextTick(callback); +}; + +BST.prototype.del = function del(key, options, callback) { + var item; + + if (!callback) { + callback = options; + options = null; + } + + if (!options) + options = {}; + + this.remove(key, options); + + return utils.nextTick(callback); +}; + +BST.prototype.batch = function batch(ops, options, callback) { + var batch; + + if (!callback) { + callback = options; + options = null; + } + + if (!options) + options = {}; + + batch = new Batch(this, options); + + if (ops) { + batch.ops = ops; + return batch.write(callback); + } + + return batch; +}; + +BST.prototype.iterator = function iterator(options) { + return new Iterator(this, options); +}; + +BST.prototype.getProperty = function getProperty(name) { + return null; +}; + +BST.prototype.approximateSize = function approximateSize(start, end, callback) { + var size = this.range(start, end).reduce(function(total, item) { + return total + item.key.length + item.value.length; + }, 0); + return utils.asyncify(callback)(null, size); +}; + +BST.destroy = function destroy(location, callback) { + return utils.nextTick(callback); +}; + +BST.repair = function repair(location, callback) { + return utils.asyncify(callback)(new Error('Cannot repair.')); +}; + +/** + * Batch + */ + +function Batch(tree, options) { + this.options = options || {}; + this.ops = []; + this.tree = tree; +} + +Batch.prototype.put = function(key, value) { + assert(this.tree, 'Already written.'); + this.ops.push({ type: 'put', key: key, value: value }); + return this; +}; + +Batch.prototype.del = function del(key) { + assert(this.tree, 'Already written.'); + this.ops.push({ type: 'del', key: key }); + return this; +}; + +Batch.prototype.write = function write(callback) { + var self = this; + + if (!this.tree) + return utils.asyncify(callback)(new Error('Already written.')); + + this.ops.forEach(function(op) { + if (op.type === 'put') + self.tree.insert(op.key, op.value); + else if (op.type === 'del') + self.tree.remove(op.key); + else + assert(false); + }); + + this.ops.length = 0; + delete this.ops; + delete this.options; + delete this.tree; + + utils.nextTick(callback); + + return this; +}; + +/** + * Iterator + */ + +function Iterator(tree, options) { + if (!options) + options = {}; + + assert(!options.lt, 'LT is not implemented.'); + assert(!options.gt, 'GT is not implemented.'); + + this.options = { + keys: options.keys, + values: options.values, + gte: options.gte || options.start, + lte: options.lte || options.end, + keyAsBuffer: options.keyAsBuffer, + valueAsBuffer: options.valueAsBuffer, + reverse: options.reverse, + limit: options.limit + }; + + this.tree = tree; + this.ended = false; + this.items = this.tree.range(this.options.gte, this.options.lte); + this.index = this.options.reverse ? this.items.length - 1 : 0; + this.total = 0; +} + +Iterator.prototype.next = function(callback) { + var item, key, value; + + if (this.ended) + return utils.asyncify(callback)(new Error('Cannot call next after end.')); + + if (this.options.reverse) + item = this.items[this.index--]; + else + item = this.items[this.index++]; + + if (this.options.limit != null) { + if (this.total++ >= this.options.limit) { + this._end(); + return utils.nextTick(callback); + } + } + + if (!item) { + this._end(); + return utils.nextTick(callback); + } + + key = item.key; + value = item.value; + + if (this.options.keys === false) + key = DUMMY; + + if (this.options.values === false) + value = DUMMY; + + if (this.options.keyAsBuffer === false) + key = key.toString('ascii'); + + if (this.options.valueAsBuffer === false) + value = value.toString('utf8'); + + return utils.asyncify(callback)(null, key, value); +}; + +Iterator.prototype.seek = function seek(key) { + assert(!this.ended, 'Already ended.'); + + if (typeof key === 'string') + key = new Buffer(key, 'ascii'); + + this.index = binarySearch(this.items, key, true, function(a, b) { + return self.compare(a.key, b); + }); +}; + +Iterator.prototype._end = function end(callback) { + if (!this.tree) + return; + + delete this.tree; + this.items.length = 0; + delete this.items; +}; + +Iterator.prototype.end = function end(callback) { + if (this.ended) + return utils.asyncify(callback)(new Error('Already ended.')); + + this.ended = true; + this._end(); + + return utils.nextTick(callback); +}; + +/** + * Helpers + */ + +function binarySearch(items, key, insert, compare) { + var start = 0; + var end = items.length - 1; + var pos, cmp; + + if (!compare) + compare = utils.cmp; + + while (start <= end) { + pos = (start + end) >>> 1; + cmp = compare(items[pos], key); + + if (cmp === 0) + return pos; + + if (cmp < 0) + start = pos + 1; + else + end = pos - 1; + } + + if (!insert) + return -1; + + return start - 1; +} + +function clone(node) { + return { + key: node.key, + value: node.value, + left: node.left, + right: node.right + }; +} + +/** + * Expose + */ + +module.exports = BST; + +if (module.parent) + return; + +var tree = new BST(); + +function bench(tree, name) { + var start = Date.now(); + var istart = Date.now(); + for (var i = 0; i < 100000; i++) { + tree.insert('foo ' + i, 'bar ' + i); + } + console.log('%s insert: %d', name, Date.now() - istart); + var sstart = Date.now(); + for (var i = 0; i < 100000; i++) { + tree.search('foo ' + i); + } + console.log('%s search: %d', name, Date.now() - sstart); + var rstart = Date.now(); + for (var i = 0; i < 100000; i++) { + if (i > 50000) + tree.remove('foo ' + i); + } + console.log('%s remove: %d', name, Date.now() - rstart); + var itstart = Date.now(); + tree.range('foo 700', 'foo 80000', { keyAsBuffer: false, valueAsBuffer: false }); + console.log('%s iter: %d', name, Date.now() - itstart); + console.log('%s total: %d', name, Date.now() - start); +} + +bench(tree, 'tree'); + +var tree = new BST(); +for (var i = 0; i < 1000; i++) { + tree.insert('foo ' + i, 'bar ' + i); +} +for (var i = 0; i < 1000; i++) { + assert(tree.search('foo ' + i).toString('utf8') === 'bar ' + i); + if (i > 900) + tree.remove('foo ' + i); +} +for (var i = 0; i < 1000; i++) { + if (i > 900) + assert(tree.search('foo ' + i) == null); + else + assert(tree.search('foo ' + i).toString('utf8') === 'bar ' + i); +} + +var items = tree.range('foo 700', 'foo 800', { keyAsBuffer: false, valueAsBuffer: false }); + +//utils.print(items); + +tree.open(function() { + var batch = tree.batch(); + for (var i = 0; i < 1000; i++) { + var key = 'foo ' + i; + var val = new Buffer('bar ' + i, 'ascii'); + batch.put(key, val); + } + batch.write(function(err) { + if (err) + throw err; + var batch = tree.batch(); + for (var i = 0; i < 1000; i++) { + var key = 'foo ' + i; + var val = new Buffer('bar ' + i, 'ascii'); + if (i > 950) + batch.del(key); + else + batch.put(key, val); + } + batch.write(function(err) { + if (err) + throw err; + utils.forRangeSerial(0, 1000, function(i, next) { + var key = 'foo ' + i; + var val = new Buffer('bar ' + i, 'ascii'); + tree.get(key, function(err, value) { + if (i > 950) { + assert(err); + assert(err.type === 'NotFoundError'); + return next(); + } + if (err) + return next(err); + assert(utils.isEqual(value, val)); + next(); + }); + }, function(err) { + if (err) + throw err; + var iter = tree.iterator({ + gte: 'foo 900', + lte: 'foo 999', + keyAsBuffer: false, + fillCache: false + }); + (function next() { + iter.next(function(err, key, value) { + if (err) { + return iter.end(function(e) { + throw err; + }); + } + if (key === undefined) { + return iter.end(function(e) { + if (e) + throw e; + utils.print(tree.dump()); + tree.approximateSize(null, null, function(err, size) { + utils.print(size); + return console.log('done'); + }); + }); + } + console.log(key + ' : ' + value); + next(); + }); + })(); + }); + }); + }); +}); diff --git a/lib/bcoin/ldb.js b/lib/bcoin/ldb.js index b182730f..324b8246 100644 --- a/lib/bcoin/ldb.js +++ b/lib/bcoin/ldb.js @@ -54,6 +54,9 @@ function getLocation(name) { } function getBackend(backend) { + if (backend === 'bst') + return require('./bst'); + if (bcoin.isBrowser) return require('level-js');