Skip to content

Commit 24e7bd5

Browse files
committed
fix: performance and doc improvements
Improving traversal performance overall and creating a benchmark documents comparing traversal with this libs against other ones that also implements tree, and traversal strategies
1 parent b0cab7a commit 24e7bd5

File tree

12 files changed

+935
-388
lines changed

12 files changed

+935
-388
lines changed

libs/js-tuple/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,8 +213,10 @@ function createKey<T extends readonly unknown[]>(elements: T): Readonly<T> {
213213

214214
For complex scenarios—such as custom traversal orders, subtree iteration, or post-order cleanup—js-tuple provides highly flexible traversal APIs for both `NestedMap` and `NestedSet`. You can choose between depth-first and breadth-first traversal, and between pre-order and post-order yielding, to match your algorithm's needs.
215215

216+
216217
- **NestedMap:** See [Advanced NestedMap.entries](https://github.com/codibre/js-utils/blob/main/libs/js-tuple/docs/nestedmap-entries-advanced.md) for details on traversal modes, yield order, edge cases, and performance considerations.
217218
- **NestedSet:** See [Advanced NestedSet](https://github.com/codibre/js-utils/blob/main/libs/js-tuple/docs/nestedset-advanced.md) for set-specific traversal, subtree operations, and advanced patterns.
219+
- **Traversal Performance:** See [Traversal Performance](https://github.com/codibre/js-utils/blob/main/libs/js-tuple/docs/traversal-performance.md) for detailed benchmarks, memory insights, and practical guidance on choosing the best traversal mode for your use case.
218220

219221
These guides cover:
220222
- How to use `basePath` for partial/subtree traversal

libs/js-tuple/benchmark/run-all.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
const { execSync } = require('child_process');
4+
5+
const benchmarkDir = __dirname;
6+
const files = fs
7+
.readdirSync(benchmarkDir)
8+
.filter((f) => f.endsWith('.js') && f !== 'run-all.js');
9+
10+
for (const file of files) {
11+
console.log(`\nRunning: ${file}`);
12+
execSync(`node ${path.join(benchmarkDir, file)}`, { stdio: 'inherit' });
13+
}
Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
// Benchmark: DFS and BFS Pre-Order Traversal Comparison
2+
// Compares js-tuple NestedMap, tree-model, bintrees, js-tree, binary-search-tree
3+
4+
const Benchmark = require('benchmark');
5+
const TreeModel = require('tree-model');
6+
const { RBTree } = require('bintrees');
7+
const JSTree = require('js-tree');
8+
const { NestedMap, TraverseMode, YieldMode } = require('../dist/min');
9+
10+
const SIZES = [100, 500, 1000, 2000, 5000];
11+
const MODES = [
12+
{ name: 'DFS Pre-Order', traverseMode: 'dfs', order: 'pre' },
13+
{ name: 'DFS Post-Order', traverseMode: 'dfs', order: 'post' },
14+
{ name: 'BFS Pre-Order', traverseMode: 'bfs', order: 'pre' },
15+
{ name: 'BFS Post-Order', traverseMode: 'bfs', order: 'post' },
16+
];
17+
18+
function buildJsTupleTree(NODE_COUNT) {
19+
const map = new NestedMap();
20+
for (let i = 0; i < NODE_COUNT; ++i) {
21+
map.set([i], i);
22+
}
23+
return map;
24+
}
25+
26+
function buildTreeModelTree(NODE_COUNT) {
27+
const tree = new TreeModel();
28+
let root = tree.parse({ id: 0, children: [] });
29+
let current = root;
30+
for (let i = 1; i < NODE_COUNT; ++i) {
31+
const child = tree.parse({ id: i, children: [] });
32+
current.addChild(child);
33+
current = child;
34+
}
35+
return root;
36+
}
37+
38+
function buildBintree(NODE_COUNT) {
39+
const tree = new RBTree((a, b) => a - b);
40+
for (let i = 0; i < NODE_COUNT; ++i) {
41+
tree.insert(i);
42+
}
43+
return tree;
44+
}
45+
46+
function buildJsTree(NODE_COUNT) {
47+
let rootObj = { id: 0, children: [] };
48+
let current = rootObj;
49+
for (let i = 1; i < NODE_COUNT; ++i) {
50+
const child = { id: i, children: [] };
51+
current.children.push(child);
52+
current = child;
53+
}
54+
return new JSTree(rootObj);
55+
}
56+
57+
function dfsPreTreeModel(root, NODE_COUNT) {
58+
let count = 0;
59+
root.walk(() => {
60+
count++;
61+
});
62+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
63+
}
64+
65+
function dfsPostTreeModel(root, NODE_COUNT) {
66+
let count = 0;
67+
function walk(node) {
68+
if (node.children) {
69+
for (const child of node.children) {
70+
walk(child);
71+
}
72+
}
73+
count++;
74+
}
75+
walk(root);
76+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
77+
}
78+
79+
function bfsPreTreeModel(root, NODE_COUNT) {
80+
let count = 0;
81+
const queue = [root];
82+
while (queue.length) {
83+
const node = queue.shift();
84+
count++;
85+
if (node.children) {
86+
for (const child of node.children) {
87+
queue.push(child);
88+
}
89+
}
90+
}
91+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
92+
}
93+
94+
function bfsPostTreeModel(root, NODE_COUNT) {
95+
let count = 0;
96+
function bfsPost(nodes) {
97+
const nextLevel = [];
98+
for (const node of nodes) {
99+
if (node.children) {
100+
for (const child of node.children) {
101+
nextLevel.push(child);
102+
}
103+
}
104+
}
105+
if (nextLevel.length) bfsPost(nextLevel);
106+
for (const node of nodes) {
107+
count++;
108+
}
109+
}
110+
bfsPost([root]);
111+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
112+
}
113+
114+
function dfsPreBintree(tree, NODE_COUNT) {
115+
let count = 0;
116+
tree.each(() => {
117+
count++;
118+
});
119+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
120+
}
121+
122+
function dfsPostBintree(tree, NODE_COUNT) {
123+
let count = 0;
124+
function walk(node) {
125+
if (!node) return;
126+
walk(node.left);
127+
walk(node.right);
128+
if (node.hasOwnProperty('key')) count++;
129+
}
130+
walk(tree._root);
131+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
132+
}
133+
134+
function bfsPreBintree(tree, NODE_COUNT) {
135+
throw new Error('BFS not supported for RBTree');
136+
}
137+
138+
function bfsPostBintree(tree, NODE_COUNT) {
139+
throw new Error('BFS not supported for RBTree');
140+
}
141+
142+
function dfsPreJsTree(tree, NODE_COUNT) {
143+
let count = 0;
144+
function walk(node) {
145+
count++;
146+
if (node.children) {
147+
for (const child of node.children) {
148+
walk(child);
149+
}
150+
}
151+
}
152+
walk(tree.obj);
153+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
154+
}
155+
156+
function dfsPostJsTree(tree, NODE_COUNT) {
157+
let count = 0;
158+
function walk(node) {
159+
if (node.children) {
160+
for (const child of node.children) {
161+
walk(child);
162+
}
163+
}
164+
count++;
165+
}
166+
walk(tree.obj);
167+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
168+
}
169+
170+
function bfsPreJsTree(tree, NODE_COUNT) {
171+
let count = 0;
172+
const queue = [tree.obj];
173+
while (queue.length) {
174+
const node = queue.shift();
175+
count++;
176+
if (node.children) {
177+
for (const child of node.children) {
178+
queue.push(child);
179+
}
180+
}
181+
}
182+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
183+
}
184+
185+
function bfsPostJsTree(tree, NODE_COUNT) {
186+
let count = 0;
187+
function bfsPost(nodes) {
188+
const nextLevel = [];
189+
for (const node of nodes) {
190+
if (node.children) {
191+
for (const child of node.children) {
192+
nextLevel.push(child);
193+
}
194+
}
195+
}
196+
if (nextLevel.length) bfsPost(nextLevel);
197+
for (const node of nodes) {
198+
count++;
199+
}
200+
}
201+
bfsPost([tree.obj]);
202+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
203+
}
204+
205+
function runSuite(NODE_COUNT) {
206+
for (const mode of MODES) {
207+
console.log(`\n${mode.name} Benchmark (${NODE_COUNT} nodes)\n`);
208+
const suite = new Benchmark.Suite();
209+
const failedLibs = [];
210+
211+
suite
212+
.add('js-tuple NestedMap', function () {
213+
const map = buildJsTupleTree(NODE_COUNT);
214+
let count = 0;
215+
let traverseMode, yieldMode;
216+
if (mode.traverseMode === 'dfs') {
217+
traverseMode = TraverseMode.DepthFirst;
218+
} else {
219+
traverseMode = TraverseMode.BreadthFirst;
220+
}
221+
if (mode.order === 'pre') {
222+
yieldMode = YieldMode.PreOrder;
223+
} else {
224+
yieldMode = YieldMode.PostOrder;
225+
}
226+
for (const value of map.values({ traverseMode, yieldMode })) {
227+
count++;
228+
}
229+
if (count !== NODE_COUNT) throw new Error('Incorrect node count');
230+
})
231+
.add('tree-model', function () {
232+
const root = buildTreeModelTree(NODE_COUNT);
233+
if (mode.traverseMode === 'dfs' && mode.order === 'pre')
234+
dfsPreTreeModel(root, NODE_COUNT);
235+
else if (mode.traverseMode === 'dfs' && mode.order === 'post')
236+
dfsPostTreeModel(root, NODE_COUNT);
237+
else if (mode.traverseMode === 'bfs' && mode.order === 'pre')
238+
bfsPreTreeModel(root, NODE_COUNT);
239+
else if (mode.traverseMode === 'bfs' && mode.order === 'post')
240+
bfsPostTreeModel(root, NODE_COUNT);
241+
})
242+
.add('bintrees RBTree', function () {
243+
const tree = buildBintree(NODE_COUNT);
244+
if (mode.traverseMode === 'dfs' && mode.order === 'pre')
245+
dfsPreBintree(tree, NODE_COUNT);
246+
else if (mode.traverseMode === 'dfs' && mode.order === 'post')
247+
dfsPostBintree(tree, NODE_COUNT);
248+
else if (mode.traverseMode === 'bfs' && mode.order === 'pre')
249+
bfsPreBintree(tree, NODE_COUNT);
250+
else if (mode.traverseMode === 'bfs' && mode.order === 'post')
251+
bfsPostBintree(tree, NODE_COUNT);
252+
})
253+
.add('js-tree', function () {
254+
const tree = buildJsTree(NODE_COUNT);
255+
if (mode.traverseMode === 'dfs' && mode.order === 'pre')
256+
dfsPreJsTree(tree, NODE_COUNT);
257+
else if (mode.traverseMode === 'dfs' && mode.order === 'post')
258+
dfsPostJsTree(tree, NODE_COUNT);
259+
else if (mode.traverseMode === 'bfs' && mode.order === 'pre')
260+
bfsPreJsTree(tree, NODE_COUNT);
261+
else if (mode.traverseMode === 'bfs' && mode.order === 'post')
262+
bfsPostJsTree(tree, NODE_COUNT);
263+
})
264+
.on('cycle', function (event) {
265+
console.log(String(event.target));
266+
})
267+
.on('error', function (event) {
268+
console.log(
269+
`FAILED: ${event.target.name} (${event.target.error && event.target.error.message})`,
270+
);
271+
failedLibs.push(event.target.name);
272+
})
273+
.on('complete', function () {
274+
if (failedLibs.length) {
275+
console.log(
276+
`\nLibraries that failed for ${mode.name} ${NODE_COUNT} nodes: ${failedLibs.join(', ')}`,
277+
);
278+
}
279+
console.log(
280+
`\nFastest for ${mode.name}: ` + this.filter('fastest').map('name'),
281+
);
282+
console.log('\n' + '='.repeat(80) + '\n');
283+
console.log(`${mode.name} Benchmark complete!`);
284+
})
285+
.run({ async: false });
286+
}
287+
}
288+
289+
for (const size of SIZES) {
290+
runSuite(size);
291+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Traversal Performance in js-tuple and Other Tree Libraries
2+
3+
## Choosing the Most Efficient Traversal
4+
5+
When working with tree-like data structures, traversal performance can vary significantly depending on the method and library used. In `js-tuple`'s `NestedMap`, the most performant traversal option is the `values` method. This method only yields values and does **not** keep track of the key path, making it the fastest and most memory-efficient choice. **Always prefer `values` when you do not need the key path.**
6+
7+
If you require the key path for each value, use the `entries` method. While `entries` is still efficient, it incurs additional overhead to construct and yield the key path for each entry. Use it only when strictly necessary.
8+
9+
## Traversal Insights
10+
11+
Choosing the right traversal mode depends on your use case and the shape of your data:
12+
13+
- **DFS Pre-Order**: Best for visiting nodes as soon as they are discovered. Use for tasks like copying, serialization, or searching for a specific value. Memory usage is low and scales with tree depth.
14+
15+
- **DFS Post-Order**: Ideal for operations that require processing children before parents, such as deleting or freeing resources. Also memory-efficient, scaling with tree depth.
16+
17+
- **BFS Pre-Order**: Useful for level-order processing, such as finding the shortest path or working with breadth-based algorithms. Memory usage scales with the width of the tree (number of nodes at the widest level), but is generally manageable for most trees.
18+
19+
- **BFS Post-Order**: Rarely needed in practice. It requires storing all levels in memory before yielding results, leading to high memory consumption for large or wide trees. Avoid this mode unless you have a specific need for bottom-up, level-wise processing.
20+
21+
**General Guidance:**
22+
- Prefer DFS traversals for deep trees and when memory is a concern.
23+
- Use BFS Pre-Order for algorithms that require level-wise access, but be cautious with very wide trees.
24+
- Avoid BFS Post-Order for large or wide trees, as it can consume significant memory and impact performance.
25+
26+
## Benchmark Results
27+
28+
Below are the results of benchmarks comparing `js-tuple NestedMap` with other popular tree libraries (`tree-model`, `bintrees RBTree`, `js-tree`) across different traversal modes and tree sizes. Each table shows the operations per second (ops/sec) and error margin (±%) for each library and tree size. Failures are indicated with the reason.
29+
30+
### DFS Pre-Order
31+
32+
| Library | 100 nodes | 500 nodes | 1000 nodes | 2000 nodes | 5000 nodes |
33+
|---------------------|-------------------------|-------------------------|-------------------------|-------------------------|-------------------------|
34+
| js-tuple NestedMap | **196,732 ±0.70%** | **28,600 ±5.63%** | **11,487 ±4.88%** | **5,158 ±5.09%** | **1,430 ±6.44%** |
35+
| tree-model | 149,317 ±8.77% | 23,521 ±5.26% | 9,768 ±6.34% | 4,371 ±6.52% | **1,550 ±6.45%** |
36+
| bintrees RBTree | 130,453 ±8.06% | 14,811 ±5.08% | 6,648 ±5.86% | 2,749 ±5.61% | 1,128 ±5.14% |
37+
| js-tree | 79,663 ±6.43% | 10,940 ±5.80% | 4,929 ±6.32% | 2,546 ±5.18% | Failed (Stack Overflow) |
38+
39+
### DFS Post-Order
40+
41+
| Library | 100 nodes | 500 nodes | 1000 nodes | 2000 nodes | 5000 nodes |
42+
|---------------------|-------------------------|-------------------------|-------------------------|-------------------------|-------------------------|
43+
| js-tuple NestedMap | **153,212 ±5.72%** | **22,350 ±4.67%** | **10,129 ±6.63%** | **5,053 ±3.65%** | 1,274 ±5.30% |
44+
| tree-model | 145,119 ±4.92% | 18,578 ±5.14% | **9,696 ±6.67%** | 3,755 ±6.67% | **1,461 ±6.61%** |
45+
| bintrees RBTree | Failed (Node Count) | Failed (Node Count) | Failed (Node Count) | Failed (Node Count) | Failed (Node Count) |
46+
| js-tree | 87,221 ±4.26% | 10,395 ±7.43% | 4,790 ±6.89% | 2,525 ±6.43% | Failed (Stack Overflow) |
47+
48+
### BFS Pre-Order
49+
50+
| Library | 100 nodes | 500 nodes | 1000 nodes | 2000 nodes | 5000 nodes |
51+
|---------------------|-------------------------|-------------------------|-------------------------|-------------------------|-------------------------|
52+
| js-tuple NestedMap | **135,164 ±4.75%** | **22,833 ±5.33%** | **11,169 ±5.16%** | **4,577 ±6.32%** | **1,334 ±5.86%** |
53+
| tree-model | 120,425 ±5.86% | 19,104 ±6.21% | 9,559 ±5.60% | **4,397 ±5.46%** | **1,319 ±5.87%** |
54+
| bintrees RBTree | Failed (Not Supported) | Failed (Not Supported) | Failed (Not Supported) | Failed (Not Supported) | Failed (Not Supported) |
55+
| js-tree | 77,796 ±5.06% | 10,252 ±7.11% | 5,066 ±6.56% | 2,547 ±5.80% | Failed (Stack Overflow) |
56+
57+
### BFS Post-Order
58+
59+
| Library | 100 nodes | 500 nodes | 1000 nodes | 2000 nodes | 5000 nodes |
60+
|---------------------|-------------------------|-------------------------|-------------------------|-------------------------|-------------------------|
61+
| js-tuple NestedMap | **128,635 ±4.57%** | **19,808 ±5.38%** | **9,316 ±5.26%** | **4,397 ±4.98%** | **1,327 ±5.78%** |
62+
| tree-model | 121,117 ±4.79% | 12,302 ±3.83% | 7,056 ±7.32% | 3,264 ±5.93% | Failed (Stack Overflow) |
63+
| bintrees RBTree | Failed (Not Supported) | Failed (Not Supported) | Failed (Not Supported) | Failed (Not Supported) | Failed (Not Supported) |
64+
| js-tree | 72,851 ±4.32% | 8,728 ±6.31% | 4,737 ±4.83% | 1,673 ±5.13% | Failed (Stack Overflow) |
65+
66+
## Summary
67+
68+
- **Use `values` for best performance**: It avoids key path construction and is the fastest option.
69+
- If you need key paths, `entries` is still efficient but slower than `values`.
70+
- For large trees, some libraries may fail due to stack overflow or unsupported traversal modes.
71+
- `js-tuple NestedMap` and `tree-model` are generally the fastest and most robust options across traversal modes and tree sizes.
72+
73+
For more details on usage and API, see the main documentation.

0 commit comments

Comments
 (0)