d-ary-heap 2.1.2 → 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/src/index.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * @packageDocumentation
7
7
  * @module d-ary-heap
8
- * @version 2.1.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
+ }