npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

tiny-tree

v0.3.0

Published

Efficient, no-dependency b-tree and binary search tree for node or the browser

Downloads

18

Readme

TinyTree

TinyTree is a zero-dependency library that efficiently searches for uniquely-identified items in a range. It includes two implementations with different performance characteristics. ArrayTree provides extremely fast access for data that rarely changes. BTree provides slightly slower access but performs well with highly volatile data.

Both trees expose the same API, so you can use them interchangeably.

Motivation

Let's say you have a list of customers, and you want to find everyone with id between 320 and 400. If you had your customers in a database, you might write a query like SELECT * from customers where id >=320 and id <= 400;. If you wanted this query to run fast, you'd create an index on the id field.

TinyTree provides this kind of indexed lookup for JavaScript objects.

One way to do this is to create an array that is sorted by id. Then you can use binary search to find quickly find the items you want. This is roughly what ArrayTree does, but it includes a number of methods to make these queries more convenient to write and provides methods for modifying the list.

Sorted arrays perform extremely well for searches, but they are expensive to update, especially if you are inserting items in the middle. BTree provides a performant alternative for volatile data based on the B-Tree data structure.

Usage

Creating a new tree

// Node.js
const tinyTree = require("tiny-tree"); // or import {ArrayTree, BTree} from "tiny-tree" if you're using ES modules
const bTree = new tinyTree.BTree();
const arrayTree = new tinyTree.ArrayTree();
<!--Browser-->
<script type="text/javascript" src="tiny-tree.js"></script>
<script type="text/javascript">
  const bTree = new tinyTree.BTree();
  const arrayTree = new tinyTree.ArrayTree();
</script>
Options
const tree = new BTree({degree: 17});

BTree can create trees of any degree; by default, trees are of degree 15, which provides reasonable performance most of the time. You can change this by passing options to the constructor:

  • degree (integer; default 15; min 3)—determines how many items are stored at each node. See the performance section below for guidance on setting the degree.

ArrayTree does not take any options.

Adding/replacing entries

tree.set(key, value);

Keys must be strings or numbers. They are compared with javascript's default comparison operators (<, >, =, etc).

Values can be any data type.

Bulk loading items

let mySortedData = [
  ["key-a", "value-for-key-a"],
  ["key-b", "value-for-key-b"],
  ["key-c", "value-for-key-c"],
  ["key-d", "value-for-key-d"],
];

tree.bulkLoad(mySortedData);

You can get substantially better loading time by bulk loading a sorted list of entries. Bulk loading is only allowed on empty trees.

The data must be a sorted array of key/value arrays as shown in the example.

For BTrees, bulk loading also leads to better query performance (roughly 50%) and memory usage.

Removing entries

tree.delete(key);

Removing all entries

tree.clear();

Getting a single value by key or index

tree.get(key);
// returns value for the given key or `undefined` if no value has been set for this key
tree.getByIndex(4); // get the fifth entry from the sorted entries

Both functions return undefined if the index or key is not in the tree.

Getting all entries in order

// get all entries
tree.toArray();
// output [["key-a", "value-a"], ["key-b", "value-b"], ["key-c", "value-c"], ...]

// get all values (no keys)
tree.toArray(null, true);
// output ["value-a", "value-b", "value-c", ...]

Note that the first argument to toArray is an optional bounds (see below).

Getting values only is substantially faster than getting keys and values together. See the Performance section for more information.

Getting a range of entries based on key

// get all entries where key >= "key-a" and key < "key-c"
let bounds = {min: "key-a", max: "key-c"};
tree.toArray(bounds);
// output [["key-a", "value-a"], ["key-b", "value-b"]]

// get just values where key >= "key-a" and key < "key-c"
let bounds = {min: "key-a", max: "key-c"};
tree.toArray(bounds, true);
// output ["value-a", "value-b"]

You can limit your results to just the keys in a certain range. BTrees and ArrayTrees are especially well-suited to this kind of query.

tree.toArray(bounds optional object, valuesOnly optional boolean)—get key/value pairs or values based on bounds.

When setting bounds, min is inclusive and max is exclusive. If you need to change this behavior, set minExclusive and/or maxInclusive instead. All min/max properties are optional. It is equivalent to set min/minInclusive and max/maxExclusive.

Examples:

  • {min: 100, max: 200}: 100 ≤ key < 200
  • {minInclusive: 100, max: 200}: 100 ≤ key < 200
  • {minExclusive: 100, max: 200}: 100 < key < 200
  • {min: 100, maxInclusive: 200}: 100 ≤ key ≤ 200
  • {min: 100}: 100 ≤ key
  • {max: 200}: 200 < key

Getting a range of entries based on index (sort position)

// get the top two entries
tree.toArrayByIndex(0, 2, true);
// output ["value-a", "value-b"]

// get the bottom two entries
tree.toArrayByIndex(tree.size - 3, 2, true);
// output ["value-y", "value-z"]

tree.toArrayByIndex(start integer, count integer, valuesOnly optional boolean)

This works very much like toArray, but instead of querying by key, you are querying by sort position. This is useful for determining the top N or bottom N entries.

Size and other statistics

tree.size; // get total number of values stored
tree.getStats(); // get detailed information about the BTree

If you are trying to tune your tree for performance, detailed statistics might be helpful.

For BTree, getStats returns an object with the following fields:

  • degree—the BTree's degree, as set in the constructor.
  • size—the total number of values stored. This is the same value as tree.size.
  • depth—the depth of the tree structure. Deeper trees can take longer to traverse. In general, trees with higher degrees will be shallower.
  • fillFactor—the proportion of keys that are non-empty. A fillFactor of 1 means that all keys are filled and there is no empty space. For a large tree with random data, values of 0.6 to 0.7 are typical.
  • nodes—the total number of nodes in the tree. In general, trees with higher degrees will need fewer nodes.
  • saturationFactor—the proportion of nodes that are completely filled. Adding keys to a saturated node will cause the tree to rebalance. Conversely, trees with a low saturation factor can accommodate new entries with higher performance. In general, trees of higher degree have a lower saturation factor.
  • keySlots—the total number of keys that the tree can store with its current node configuration.
  • filledKeySlots—the number of these slots that are filled. Note that fillFactor = filledKeySlots / keySlots.
  • saturatedNodes—the number of saturated nodes. Note that saturationFactor = saturatedNodes / nodes.

For ArrayTree, getStats is included mostly for compatibility and returns an object with the following field:

  • size—the total number of values stored. This is the same value as tree.size.

Performance

Performance is highly dependent on your data and access patterns, but there are some general rules that can help.

  • For gets and queries, ArrayTree is always faster than BTree, in some cases by more than 50x. If your data doesn't change much, you should use ArrayTree.
  • If your data changes a lot, you should use BTree. If you have more than 50,000 items or so with lots of adds and removes, ArrayTree may be unusably slow.
  • For queries, you should use valuesOnly = true if possible. For ArrayTree, this provides an enormous performance benefit (often 10x). For BTree, the performance boost is modest (typically around 5%).
  • For both ArrayTree and BTree, bulk loads are always faster and provide equal or better query performance. If you can bulk load your data, you should.

BTree Benchmarks

For BTrees, degree affects performance in a complicated way that depends on how you access data (especially the number of items you query at a time). Up to a point, higher-degree trees have slower inserts/deletes but faster lookups. The following table also illustrates the substantial benefits of bulk loading.

On a 2017 15" MacBook Pro, here are operations per second for typical operations.

| Test | Degree 3 | Degree 7 | Degree 15 | Degree 31 | Degree 63 | |:-------------------------------------------|:------------|:------------|:------------|:------------|:------------| | Bulk load 10,000 items | 142 ops/s | 232 ops/s | 216 ops/s | 159 ops/s | 91 ops/s | | Fill factor | 99.9% | 99.8% | 99.7% | 99.5% | 99.0% | | Depth | 9 | 5 | 4 | 3 | 3 | | Get 1000 items by key | 2,329 ops/s | 2,855 ops/s | 2,469 ops/s | 1,887 ops/s | 985 ops/s | | Get 1000 items by index | 5,317 | 8,659 | 11,038 | 12,504 | 9,649 | | Query 1000 ranges of size 100 by key | 212 | 336 | 352 | 327 | 237 | | Query 1000 ranges of size 100 by index | 259 | 446 | 521 | 465 | 427 | | Query 1000 ranges of size 1000 by key | 28 | 50 | 61 | 60 | 64 | | Query 1000 ranges of size 1000 by index | 29 | 51 | 64 | 72 | 75 | | Load 10,000 items in random order | 82 ops/s | 150 ops/s | 162 ops/s | 133 ops/s | | | Fill factor | 67.0% | 68.3% | 69.4% | 69.2% | 69.5% | | Depth | 11 | 6 | 4 | 3 | 3 | | Get 1000 items by key | 2,289 ops/s | 2,646 ops/s | 2,392 ops/s | 1,749 ops/s | 1,131 ops/s | | Get 1000 items by index | 3,932 | 7,443 | 9,708 | 10,981 | 8,889 | | Query 1000 ranges of size 100 by key | 132 ops/s | 233 ops/s | 274 ops/s | 273 ops/s | 218 ops/s | | Query 1000 ranges of size 100 by index | 148 | 284 | 363 | 410 | 357 | | Query 1000 ranges of size 1000 by key | 17 | 33 | 43 | 50 | 51 | | Query 1000 ranges of size 1000 by index | 17 | 34 | 45 | 53 | 58 | | Bulk load 1,000,000 items | 0.57 ops/s | 0.74 ops/s | 0.72 ops/s | 0.57 ops/s | 0.42 ops/s | | Fill factor | 100% | 100% | 100% | 100% | 100% | | Depth | 13 | 8 | 6 | 5 | 4 | | Get 1000 items by key | 329 ops/s | 420 ops/s | 305 ops/s | 186 ops/s | 114 ops/s | | Get 1000 items by index | 1,694 | 2,518 | 2,875 | 2,620 | 2,092 | | Query 1000 ranges of size 100 by key | 49 | 84 | 86 | 69 | 46 | | Query 1000 ranges of size 100 by index | 61 | 146 | 229 | 342 | 294 | | Query 1000 ranges of size 1000 by key | 7.1 | 16 | 23 | 27 | 22 | | Query 1000 ranges of size 1000 by index | 7.5 | 18 | 29 | 37 | 41 | | Load 1,000,000 items in random order | 0.17 ops/s | 0.25 ops/s | 0.25 ops/s | 0.21 ops/s | 0.15 ops/s | | Fill factor | 67.0% | 68.1% | 68.6% | 69.2% | 69.4% | | Depth | 16 | 9 | 6 | 5 | 4 | | Get 1000 items by key | 310 ops/s | 386 ops/s | 354 ops/s | 226 ops/s | 147 ops/s | | Get 1000 items by index | 614 | 1,542 | 1,998 | 2,271 | 2,364 | | Query 1000 ranges of size 100 by key | 22 | 48 | 68 | 66 | 51 | | Query 1000 ranges of size 100 by index | 24 | 60 | 116 | 171 | 221 | | Query 1000 ranges of size 1000 by key | 2.7 | 6.7 | 13 | 16 | 20 | | Query 1000 ranges of size 1000 by index | 2.8 | 6.9 | 14 | 22 | 29 |

BTree vs ArrayTree benchmarks

ArrayTree provides very close to the theoretical best-case performance for queries. If your data changes rarely, you can get an enormous boost in performance by using it. Also, if your data is fairly small (less than 10,000 items), you should consider using it even if your data is volatile, provided that queries are significantly more common than updates.

ArrayTree is especially efficient at getting contiguous arrays of values, where it achieves nearly the same performance as raw array access.

| Test | BTree (degree 15) | ArrayTree | Speedup (slowdown) | |:---------------------------------------------------|:-------------------|:-------------------|:-------------------| | Bulk load 10k items, add/remove 5k items | 71 ops/s | 37 ops/s | (1.9x) | | Get 1000 items by key | 2,100 | 3,700 | 1.8x | | Get 1000 items by index | 9,800 | 540,000 | 55x | | Query 1000 ranges of size 100 by key | 300 | 1,500 | 5x | | Query 1000 ranges of size 100 by index | 450 | 12,000 | 34x | | Query 1000 ranges of size 1000 by key | 55 | 730 | 13x | | Query 1000 ranges of size 1000 by index | 59 | 1,400 | 24x | | Bulk load 100k items, add/remove 50k items | 3.0 ops/s | 0.07 ops/s | (42x) | | Get 1000 items by key | 880 | 1,600 | 1.8x | | Get 1000 items by index | 4,700 | 300,000 | 64x | | Query 1000 ranges of size 100 by key | 130 | 430 | 3.3x | | Query 1000 ranges of size 100 by index | 340 | 1,900 | 5.6x | | Query 1000 ranges of size 1000 by key | 31 | 160 | 5.2x | | Query 1000 ranges of size 1000 by index | 41 | 580 | 14x | | Bulk load 1M items, add/remove 500k items | 0.16 ops/s | Did not finish | - | | Get 1000 items by key | 305 | n/a | - | | Get 1000 items by index | 2,069 | n/a | - | | Query 1000 ranges of size 100 by key | 71 | n/a | - | | Query 1000 ranges of size 100 by index | 150 | n/a | - | | Query 1000 ranges of size 1000 by key | 17 | n/a | - | | Query 1000 ranges of size 1000 by index | 20 | n/a | - |