d-ary-heap 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -1
- package/dist/index.cjs +161 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +265 -3
- package/dist/index.d.ts +265 -3
- package/dist/index.js +157 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/PriorityQueue.ts +63 -1
- package/src/comparators.ts +1 -1
- package/src/index.ts +16 -1
- package/src/instrumentation.ts +302 -0
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @packageDocumentation
|
|
7
7
|
* @module d-ary-heap
|
|
8
|
-
* @version 2.
|
|
8
|
+
* @version 2.4.0
|
|
9
9
|
* @license Apache-2.0
|
|
10
10
|
* @copyright 2023-2025 Eric Jacopin
|
|
11
11
|
*/
|
|
@@ -28,3 +28,18 @@ export {
|
|
|
28
28
|
reverse,
|
|
29
29
|
chain,
|
|
30
30
|
} from './comparators';
|
|
31
|
+
|
|
32
|
+
// Instrumentation utilities for performance analysis (opt-in, zero-cost when disabled)
|
|
33
|
+
export {
|
|
34
|
+
createComparisonStats,
|
|
35
|
+
instrumentComparator,
|
|
36
|
+
theoreticalInsertComparisons,
|
|
37
|
+
theoreticalPopComparisons,
|
|
38
|
+
theoreticalDecreasePriorityComparisons,
|
|
39
|
+
} from './instrumentation';
|
|
40
|
+
|
|
41
|
+
export type {
|
|
42
|
+
OperationType,
|
|
43
|
+
ComparisonStats,
|
|
44
|
+
InstrumentedComparator,
|
|
45
|
+
} from './instrumentation';
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Instrumentation utilities for d-ary heap performance analysis.
|
|
3
|
+
*
|
|
4
|
+
* This module provides opt-in instrumentation to count comparisons performed
|
|
5
|
+
* during heap operations. It is designed for:
|
|
6
|
+
*
|
|
7
|
+
* - **Educational purposes**: Understanding the theoretical vs actual cost of heap operations
|
|
8
|
+
* - **Benchmarking**: Measuring real comparison counts across different arities
|
|
9
|
+
* - **Visualization**: Powering interactive demos that show heap behavior
|
|
10
|
+
*
|
|
11
|
+
* ## Design Philosophy: Zero-Cost When Disabled
|
|
12
|
+
*
|
|
13
|
+
* Instrumentation follows these principles:
|
|
14
|
+
*
|
|
15
|
+
* 1. **Opt-in only**: No overhead when not using instrumentation
|
|
16
|
+
* 2. **Non-breaking**: Existing code continues to work unchanged
|
|
17
|
+
* 3. **Per-operation tracking**: Distinguish insert/pop/decreasePriority comparisons
|
|
18
|
+
*
|
|
19
|
+
* ## Cross-Language Consistency
|
|
20
|
+
*
|
|
21
|
+
* Currently, instrumentation is implemented in TypeScript only. The table below
|
|
22
|
+
* shows the idiomatic zero-cost approach for each language, planned for v2.5.0:
|
|
23
|
+
*
|
|
24
|
+
* | Language | Mechanism | Overhead When Disabled | Status |
|
|
25
|
+
* |------------|----------------------------------|------------------------|--------|
|
|
26
|
+
* | TypeScript | Optional hooks + instrumented comparator | Zero (JIT optimization) | ✅ Implemented |
|
|
27
|
+
* | Go | Nil stats pointer | ~1 cycle (nil check) | Planned v2.5.0 |
|
|
28
|
+
* | Rust | Generic over StatsCollector trait | Zero (monomorphization) | Planned v2.5.0 |
|
|
29
|
+
* | C++ | Template policy class | Zero (inlining) | Planned v2.5.0 |
|
|
30
|
+
* | Zig | Comptime bool parameter | Zero (branch elimination) | Planned v2.5.0 |
|
|
31
|
+
*
|
|
32
|
+
* ## Usage Example
|
|
33
|
+
*
|
|
34
|
+
* ```typescript
|
|
35
|
+
* import { PriorityQueue, minBy, instrumentComparator } from 'd-ary-heap';
|
|
36
|
+
*
|
|
37
|
+
* // 1. Wrap your comparator with instrumentation
|
|
38
|
+
* const comparator = instrumentComparator(minBy((v: Vertex) => v.distance));
|
|
39
|
+
*
|
|
40
|
+
* // 2. Create priority queue with operation hooks
|
|
41
|
+
* const pq = new PriorityQueue({
|
|
42
|
+
* d: 4,
|
|
43
|
+
* comparator,
|
|
44
|
+
* keyExtractor: (v) => v.id,
|
|
45
|
+
* onBeforeOperation: (op) => comparator.startOperation(op),
|
|
46
|
+
* onAfterOperation: () => comparator.endOperation(),
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* // 3. Use normally - comparisons are tracked automatically
|
|
50
|
+
* pq.insert({ id: 'A', distance: 0 });
|
|
51
|
+
* pq.insert({ id: 'B', distance: 5 });
|
|
52
|
+
* pq.pop();
|
|
53
|
+
*
|
|
54
|
+
* // 4. Access statistics
|
|
55
|
+
* console.log(comparator.stats);
|
|
56
|
+
* // { insert: 1, pop: 2, decreasePriority: 0, total: 3 }
|
|
57
|
+
*
|
|
58
|
+
* // 5. Reset for next measurement
|
|
59
|
+
* comparator.stats.reset();
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* ## Theoretical Complexity Reference
|
|
63
|
+
*
|
|
64
|
+
* For a d-ary heap with n elements:
|
|
65
|
+
*
|
|
66
|
+
* | Operation | Comparisons (worst case) |
|
|
67
|
+
* |------------------|----------------------------|
|
|
68
|
+
* | insert | ⌊log_d(n)⌋ |
|
|
69
|
+
* | pop | d × ⌊log_d(n)⌋ |
|
|
70
|
+
* | decreasePriority | ⌊log_d(n)⌋ (upward only) |
|
|
71
|
+
* | increasePriority | d × ⌊log_d(n)⌋ (downward) |
|
|
72
|
+
*
|
|
73
|
+
* The demo visualization compares actual counts against these theoretical bounds.
|
|
74
|
+
*
|
|
75
|
+
* @module instrumentation
|
|
76
|
+
* @version 2.4.0
|
|
77
|
+
* @license Apache-2.0
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
import type { Comparator } from './PriorityQueue';
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Operation types that can be tracked.
|
|
84
|
+
*
|
|
85
|
+
* Note: `increasePriority` is tracked separately because in Dijkstra's algorithm
|
|
86
|
+
* it manifests as decreasePriority (lowering distance = higher priority in min-heap).
|
|
87
|
+
*/
|
|
88
|
+
export type OperationType = 'insert' | 'pop' | 'decreasePriority' | 'increasePriority';
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Statistics tracking comparison counts per operation type.
|
|
92
|
+
*
|
|
93
|
+
* All counts start at zero and accumulate until `reset()` is called.
|
|
94
|
+
*/
|
|
95
|
+
export interface ComparisonStats {
|
|
96
|
+
/** Comparisons during insert operations (moveUp) */
|
|
97
|
+
insert: number;
|
|
98
|
+
|
|
99
|
+
/** Comparisons during pop operations (moveDown + bestChildPosition) */
|
|
100
|
+
pop: number;
|
|
101
|
+
|
|
102
|
+
/** Comparisons during decreasePriority operations (moveUp + moveDown) */
|
|
103
|
+
decreasePriority: number;
|
|
104
|
+
|
|
105
|
+
/** Comparisons during increasePriority operations (moveUp) */
|
|
106
|
+
increasePriority: number;
|
|
107
|
+
|
|
108
|
+
/** Total comparisons across all operation types */
|
|
109
|
+
readonly total: number;
|
|
110
|
+
|
|
111
|
+
/** Reset all counters to zero */
|
|
112
|
+
reset(): void;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* An instrumented comparator that tracks comparison counts.
|
|
117
|
+
*
|
|
118
|
+
* This extends a regular comparator with:
|
|
119
|
+
* - `stats`: Current comparison counts
|
|
120
|
+
* - `startOperation(type)`: Begin tracking for an operation
|
|
121
|
+
* - `endOperation()`: Stop tracking current operation
|
|
122
|
+
*
|
|
123
|
+
* The comparator itself remains a valid `Comparator<T>` and can be used
|
|
124
|
+
* anywhere a regular comparator is expected.
|
|
125
|
+
*/
|
|
126
|
+
export interface InstrumentedComparator<T> extends Comparator<T> {
|
|
127
|
+
/** Current comparison statistics */
|
|
128
|
+
readonly stats: ComparisonStats;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Signal the start of a heap operation.
|
|
132
|
+
* Comparisons will be attributed to this operation type until `endOperation()`.
|
|
133
|
+
*
|
|
134
|
+
* @param type - The operation type being started
|
|
135
|
+
*/
|
|
136
|
+
startOperation(type: OperationType): void;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Signal the end of the current heap operation.
|
|
140
|
+
* Subsequent comparisons will not be counted until the next `startOperation()`.
|
|
141
|
+
*/
|
|
142
|
+
endOperation(): void;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Create comparison statistics tracker.
|
|
147
|
+
*
|
|
148
|
+
* @returns Fresh stats object with all counts at zero
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* const stats = createComparisonStats();
|
|
153
|
+
* stats.insert = 5;
|
|
154
|
+
* stats.pop = 10;
|
|
155
|
+
* console.log(stats.total); // 15
|
|
156
|
+
* stats.reset();
|
|
157
|
+
* console.log(stats.total); // 0
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
export function createComparisonStats(): ComparisonStats {
|
|
161
|
+
const stats: ComparisonStats = {
|
|
162
|
+
insert: 0,
|
|
163
|
+
pop: 0,
|
|
164
|
+
decreasePriority: 0,
|
|
165
|
+
increasePriority: 0,
|
|
166
|
+
|
|
167
|
+
get total(): number {
|
|
168
|
+
return this.insert + this.pop + this.decreasePriority + this.increasePriority;
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
reset(): void {
|
|
172
|
+
this.insert = 0;
|
|
173
|
+
this.pop = 0;
|
|
174
|
+
this.decreasePriority = 0;
|
|
175
|
+
this.increasePriority = 0;
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
return stats;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Wrap a comparator with instrumentation to track comparison counts.
|
|
184
|
+
*
|
|
185
|
+
* The returned comparator:
|
|
186
|
+
* - Behaves identically to the original for comparison purposes
|
|
187
|
+
* - Tracks how many times it's called, attributed to operation types
|
|
188
|
+
* - Has zero overhead when `startOperation()` hasn't been called
|
|
189
|
+
*
|
|
190
|
+
* ## How It Works
|
|
191
|
+
*
|
|
192
|
+
* 1. Call `startOperation('insert')` before `pq.insert()`
|
|
193
|
+
* 2. The comparator increments `stats.insert` for each comparison
|
|
194
|
+
* 3. Call `endOperation()` after the operation completes
|
|
195
|
+
* 4. Repeat for other operations
|
|
196
|
+
*
|
|
197
|
+
* The `PriorityQueue` class supports `onBeforeOperation` and `onAfterOperation`
|
|
198
|
+
* hooks to automate this.
|
|
199
|
+
*
|
|
200
|
+
* ## Performance Note
|
|
201
|
+
*
|
|
202
|
+
* When `currentOperation` is null (between operations), the instrumented
|
|
203
|
+
* comparator performs only a single null check before calling the original.
|
|
204
|
+
* Modern JavaScript engines optimize this extremely well.
|
|
205
|
+
*
|
|
206
|
+
* @param comparator - The original comparator to instrument
|
|
207
|
+
* @returns An instrumented comparator with stats tracking
|
|
208
|
+
*
|
|
209
|
+
* @example
|
|
210
|
+
* ```typescript
|
|
211
|
+
* import { minBy, instrumentComparator } from 'd-ary-heap';
|
|
212
|
+
*
|
|
213
|
+
* const cmp = instrumentComparator(minBy<number, number>(x => x));
|
|
214
|
+
*
|
|
215
|
+
* // Manual usage (without hooks)
|
|
216
|
+
* cmp.startOperation('insert');
|
|
217
|
+
* console.log(cmp(5, 3)); // false, and stats.insert++
|
|
218
|
+
* console.log(cmp(3, 5)); // true, and stats.insert++
|
|
219
|
+
* cmp.endOperation();
|
|
220
|
+
*
|
|
221
|
+
* console.log(cmp.stats.insert); // 2
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
export function instrumentComparator<T>(comparator: Comparator<T>): InstrumentedComparator<T> {
|
|
225
|
+
const stats = createComparisonStats();
|
|
226
|
+
let currentOperation: OperationType | null = null;
|
|
227
|
+
|
|
228
|
+
// Create the instrumented function
|
|
229
|
+
const instrumented = ((a: T, b: T): boolean => {
|
|
230
|
+
// Only count when actively tracking an operation
|
|
231
|
+
if (currentOperation !== null) {
|
|
232
|
+
stats[currentOperation]++;
|
|
233
|
+
}
|
|
234
|
+
return comparator(a, b);
|
|
235
|
+
}) as InstrumentedComparator<T>;
|
|
236
|
+
|
|
237
|
+
// Attach stats (read-only from outside)
|
|
238
|
+
Object.defineProperty(instrumented, 'stats', {
|
|
239
|
+
value: stats,
|
|
240
|
+
writable: false,
|
|
241
|
+
enumerable: true,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Attach operation control methods
|
|
245
|
+
instrumented.startOperation = (type: OperationType): void => {
|
|
246
|
+
currentOperation = type;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
instrumented.endOperation = (): void => {
|
|
250
|
+
currentOperation = null;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
return instrumented;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Calculate theoretical comparison count for an insert operation.
|
|
258
|
+
*
|
|
259
|
+
* Insert performs at most ⌊log_d(n)⌋ comparisons (one per level during moveUp).
|
|
260
|
+
*
|
|
261
|
+
* @param n - Number of elements in heap AFTER insert
|
|
262
|
+
* @param d - Heap arity
|
|
263
|
+
* @returns Theoretical worst-case comparison count
|
|
264
|
+
*/
|
|
265
|
+
export function theoreticalInsertComparisons(n: number, d: number): number {
|
|
266
|
+
if (n <= 1) return 0;
|
|
267
|
+
return Math.floor(Math.log(n) / Math.log(d));
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Calculate theoretical comparison count for a pop operation.
|
|
272
|
+
*
|
|
273
|
+
* Pop performs at most d × ⌊log_d(n)⌋ comparisons:
|
|
274
|
+
* - At each level, find best among d children (d-1 comparisons)
|
|
275
|
+
* - Compare best child with current (1 comparison)
|
|
276
|
+
* - Total: d comparisons per level × ⌊log_d(n)⌋ levels
|
|
277
|
+
*
|
|
278
|
+
* @param n - Number of elements in heap BEFORE pop
|
|
279
|
+
* @param d - Heap arity
|
|
280
|
+
* @returns Theoretical worst-case comparison count
|
|
281
|
+
*/
|
|
282
|
+
export function theoreticalPopComparisons(n: number, d: number): number {
|
|
283
|
+
if (n <= 1) return 0;
|
|
284
|
+
const height = Math.floor(Math.log(n) / Math.log(d));
|
|
285
|
+
return d * height;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Calculate theoretical comparison count for a decreasePriority operation.
|
|
290
|
+
*
|
|
291
|
+
* DecreasePriority in our implementation calls both moveUp and moveDown
|
|
292
|
+
* for safety, but typically only moveUp executes (upward movement).
|
|
293
|
+
* Worst case: ⌊log_d(n)⌋ comparisons.
|
|
294
|
+
*
|
|
295
|
+
* @param n - Number of elements in heap
|
|
296
|
+
* @param d - Heap arity
|
|
297
|
+
* @returns Theoretical worst-case comparison count (moveUp path)
|
|
298
|
+
*/
|
|
299
|
+
export function theoreticalDecreasePriorityComparisons(n: number, d: number): number {
|
|
300
|
+
if (n <= 1) return 0;
|
|
301
|
+
return Math.floor(Math.log(n) / Math.log(d));
|
|
302
|
+
}
|