Skip to content

Commit 669b06e

Browse files
committed
timers: address PR review feedback
- Add hysteresis to prevent mode switching thrashing - Up threshold: n=10 (convert to heap) - Down threshold: n=6 (convert to linear) - Prevents oscillation around boundary - Add comprehensive test suite (test/parallel/test-priority-queue-adaptive.js) - Basic operations (insert, shift, peek) - Mode switching behavior - Edge cases (empty queue, single element) - Custom comparators and setPosition callbacks - DOS attack pattern (10K oscillations) - Stress test (1000 random operations) - Add statistical benchmark with variance reporting - Multiple trials for reproducibility - Reports stddev and coefficient of variation - Deterministic random pattern for consistency - Fix linearShift() bug when removing last element - Verify compatibility with timers.js call sites - All tests pass
1 parent 69d3974 commit 669b06e

File tree

3 files changed

+502
-8
lines changed

3 files changed

+502
-8
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use strict';
2+
3+
// Statistical benchmark with variance/stddev reporting
4+
// Runs multiple trials to ensure reproducibility
5+
6+
const common = require('../common.js');
7+
8+
const bench = common.createBenchmark(main, {
9+
queueSize: [5, 10, 20, 100, 1000, 10000],
10+
impl: ['original', 'adaptive'],
11+
trials: [10],
12+
});
13+
14+
function createHeap(impl, comparator, setPosition) {
15+
if (impl === 'original') {
16+
const PriorityQueue = require('../../lib/internal/priority_queue');
17+
return new PriorityQueue(comparator, setPosition);
18+
} else if (impl === 'adaptive') {
19+
const PriorityQueueAdaptive = require('../../lib/internal/priority_queue_adaptive');
20+
return new PriorityQueueAdaptive(comparator, setPosition);
21+
} else {
22+
throw new Error(`Unknown implementation: ${impl}`);
23+
}
24+
}
25+
26+
function runTrial(queueSize, impl) {
27+
const comparator = (a, b) => a.expiry - b.expiry;
28+
const setPosition = (node, pos) => { node.pos = pos; };
29+
const heap = createHeap(impl, comparator, setPosition);
30+
31+
// Initial queue state
32+
const timers = [];
33+
for (let i = 0; i < queueSize; i++) {
34+
timers.push({
35+
expiry: 1000 + i * 10000,
36+
id: i,
37+
pos: null,
38+
});
39+
heap.insert(timers[i]);
40+
}
41+
42+
const iterations = 100000;
43+
const startTime = process.hrtime.bigint();
44+
45+
for (let i = 0; i < iterations; i++) {
46+
const op = (i % 100) / 100; // Deterministic pattern
47+
48+
if (op < 0.80) {
49+
// 80% peek()
50+
heap.peek();
51+
} else if (op < 0.90) {
52+
// 10% shift + insert
53+
heap.shift();
54+
const newTimer = {
55+
expiry: 1000 + ((i * 12345) % 100000), // Deterministic random
56+
id: i,
57+
pos: null,
58+
};
59+
heap.insert(newTimer);
60+
} else {
61+
// 10% percolateDown
62+
const min = heap.peek();
63+
if (min) {
64+
min.expiry += 100;
65+
heap.percolateDown(1);
66+
}
67+
}
68+
}
69+
70+
const endTime = process.hrtime.bigint();
71+
const durationNs = Number(endTime - startTime);
72+
const opsPerSec = (iterations / durationNs) * 1e9;
73+
74+
return opsPerSec;
75+
}
76+
77+
function mean(values) {
78+
return values.reduce((a, b) => a + b, 0) / values.length;
79+
}
80+
81+
function stddev(values) {
82+
const avg = mean(values);
83+
const squareDiffs = values.map((value) => Math.pow(value - avg, 2));
84+
const avgSquareDiff = mean(squareDiffs);
85+
return Math.sqrt(avgSquareDiff);
86+
}
87+
88+
function main({ queueSize, impl, trials }) {
89+
const results = [];
90+
91+
// Run multiple trials
92+
for (let i = 0; i < trials; i++) {
93+
results.push(runTrial(queueSize, impl));
94+
}
95+
96+
const avg = mean(results);
97+
const std = stddev(results);
98+
const cv = (std / avg) * 100; // Coefficient of variation
99+
100+
bench.start();
101+
// Report mean as the primary metric
102+
bench.end(avg);
103+
104+
// Log statistical info (will appear in benchmark output)
105+
console.error(`n=${queueSize} impl=${impl}: ${avg.toFixed(0)} ops/sec ` +
106+
`(stddev=${std.toFixed(0)}, cv=${cv.toFixed(2)}%)`);
107+
}

lib/internal/priority_queue_adaptive.js

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
// holds TimersList objects (one per unique timeout duration), not individual
1919
// timers, resulting in small queue sizes even in large applications.
2020

21-
const THRESHOLD = 10;
21+
// Hysteresis thresholds to prevent mode switching thrashing
22+
const HEAP_THRESHOLD = 10; // Switch to heap when size reaches this
23+
const LINEAR_THRESHOLD = 6; // Switch back to linear when size drops to this
2224
const D = 4; // D-ary branching factor
2325

2426
module.exports = class PriorityQueueAdaptive {
@@ -50,7 +52,7 @@ module.exports = class PriorityQueueAdaptive {
5052
const size = this.#size;
5153

5254
// Check if we need to convert to heap mode
53-
if (!this.#isHeap && size >= THRESHOLD) {
55+
if (!this.#isHeap && size >= HEAP_THRESHOLD) {
5456
this.#convertToHeap();
5557
}
5658

@@ -66,8 +68,8 @@ module.exports = class PriorityQueueAdaptive {
6668

6769
const result = this.#isHeap ? this.#heapShift() : this.#linearShift();
6870

69-
// Convert back to linear if size drops below threshold
70-
if (this.#isHeap && this.#size < THRESHOLD) {
71+
// Convert back to linear if size drops below lower threshold (hysteresis)
72+
if (this.#isHeap && this.#size < LINEAR_THRESHOLD) {
7173
this.#convertToLinear();
7274
}
7375

@@ -83,7 +85,7 @@ module.exports = class PriorityQueueAdaptive {
8385
this.#linearRemoveAt(pos);
8486
}
8587

86-
if (this.#isHeap && this.#size < THRESHOLD) {
88+
if (this.#isHeap && this.#size < LINEAR_THRESHOLD) {
8789
this.#convertToLinear();
8890
}
8991
}
@@ -97,7 +99,7 @@ module.exports = class PriorityQueueAdaptive {
9799
heap[i] = array[i];
98100
}
99101

100-
if (len >= THRESHOLD) {
102+
if (len >= HEAP_THRESHOLD) {
101103
this.#isHeap = true;
102104
this.#heapifyArray();
103105
} else {
@@ -159,13 +161,15 @@ module.exports = class PriorityQueueAdaptive {
159161
const minValue = heap[minPos];
160162
const lastPos = this.#size - 1;
161163

162-
if (minPos === lastPos) {
163-
heap[lastPos] = undefined;
164+
// If removing the last element, just clear it
165+
if (lastPos === 0) {
166+
heap[0] = undefined;
164167
this.#size = 0;
165168
this.#minPos = 0;
166169
return minValue;
167170
}
168171

172+
// Swap min with last element
169173
const lastValue = heap[lastPos];
170174
heap[minPos] = lastValue;
171175
heap[lastPos] = undefined;
@@ -174,6 +178,7 @@ module.exports = class PriorityQueueAdaptive {
174178
if (this.#setPosition !== undefined)
175179
this.#setPosition(lastValue, minPos);
176180

181+
// Find new minimum
177182
this.#updateMinPosition();
178183
return minValue;
179184
}

0 commit comments

Comments
 (0)