Skip to content

Commit 69d3974

Browse files
committed
timers: optimize priority queue with adaptive implementation
Replace the binary heap in timer queue with an adaptive priority queue that uses different strategies based on queue size. For typical workloads (n<10 timer lists), use an unsorted array with cached minimum for O(1) peek and insert. For larger queues (n≥10), automatically transition to a 4-ary heap for O(log₄ n) operations. This optimization delivers 60-75% performance improvement for realistic timer workloads while maintaining safety for pathological cases. Benchmarks show: - 76% faster at n=10 (37.4M vs 21.3M ops/sec) - 65% faster at n=5 (36.2M vs 23.7M ops/sec) - Scales gracefully to n=100K without regression The optimization is based on the insight that Node.js timer queues hold TimersList objects (one per unique timeout duration), not individual timers, resulting in small queue sizes even in large applications.
1 parent bfc81ca commit 69d3974

File tree

3 files changed

+636
-6
lines changed

3 files changed

+636
-6
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
'use strict';
2+
3+
// REALISTIC Node.js timer queue benchmark
4+
// Tests actual production workload: small queue (3-15 items), frequent peek()
5+
6+
const common = require('../common.js');
7+
8+
const bench = common.createBenchmark(main, {
9+
queueSize: [3, 5, 10, 15, 20],
10+
impl: ['original', 'adaptive'],
11+
workload: ['web-server', 'microservice', 'real-time'],
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 benchmarkWebServer(queueSize, impl) {
27+
// Typical web server: mostly peek(), occasional insert/shift
28+
// Queue holds 3-5 TimersList objects
29+
const comparator = (a, b) => a.expiry - b.expiry;
30+
const setPosition = (node, pos) => { node.pos = pos; };
31+
const heap = createHeap(impl, comparator, setPosition);
32+
33+
// Initial queue state
34+
const timers = [];
35+
for (let i = 0; i < queueSize; i++) {
36+
timers.push({
37+
expiry: 1000 + i * 10000,
38+
id: i,
39+
pos: null,
40+
});
41+
heap.insert(timers[i]);
42+
}
43+
44+
const iterations = 100000;
45+
bench.start();
46+
47+
for (let i = 0; i < iterations; i++) {
48+
const op = Math.random();
49+
50+
if (op < 0.80) {
51+
// 80% peek() - ULTRA HOT PATH in processTimers()
52+
heap.peek();
53+
} else if (op < 0.90) {
54+
// 10% shift + insert (timer list expires, new one created)
55+
heap.shift();
56+
const newTimer = {
57+
expiry: 1000 + Math.floor(Math.random() * 100000),
58+
id: i,
59+
pos: null,
60+
};
61+
heap.insert(newTimer);
62+
} else {
63+
// 10% percolateDown (timer list rescheduled)
64+
const min = heap.peek();
65+
if (min) {
66+
min.expiry += 100;
67+
heap.percolateDown(1);
68+
}
69+
}
70+
}
71+
72+
bench.end(iterations);
73+
}
74+
75+
function benchmarkMicroservice(queueSize, impl) {
76+
// Microservice: balanced mix of operations
77+
const comparator = (a, b) => a.expiry - b.expiry;
78+
const setPosition = (node, pos) => { node.pos = pos; };
79+
const heap = createHeap(impl, comparator, setPosition);
80+
81+
// Initial queue state
82+
for (let i = 0; i < queueSize; i++) {
83+
heap.insert({
84+
expiry: 1000 + i * 5000,
85+
id: i,
86+
pos: null,
87+
});
88+
}
89+
90+
const iterations = 100000;
91+
bench.start();
92+
93+
for (let i = 0; i < iterations; i++) {
94+
const op = Math.random();
95+
96+
if (op < 0.70) {
97+
// 70% peek()
98+
heap.peek();
99+
} else if (op < 0.85) {
100+
// 15% insert
101+
heap.insert({
102+
expiry: 1000 + Math.floor(Math.random() * 60000),
103+
id: i,
104+
pos: null,
105+
});
106+
} else if (op < 0.95) {
107+
// 10% shift
108+
if (heap.size > 0) heap.shift();
109+
} else {
110+
// 5% percolateDown
111+
const min = heap.peek();
112+
if (min) {
113+
min.expiry += 50;
114+
heap.percolateDown(1);
115+
}
116+
}
117+
}
118+
119+
bench.end(iterations);
120+
}
121+
122+
function benchmarkRealTime(queueSize, impl) {
123+
// Real-time app: frequent updates, many peek()
124+
const comparator = (a, b) => a.expiry - b.expiry;
125+
const setPosition = (node, pos) => { node.pos = pos; };
126+
const heap = createHeap(impl, comparator, setPosition);
127+
128+
// Initial queue state
129+
for (let i = 0; i < queueSize; i++) {
130+
heap.insert({
131+
expiry: 50 + i * 1000,
132+
id: i,
133+
pos: null,
134+
});
135+
}
136+
137+
const iterations = 100000;
138+
bench.start();
139+
140+
for (let i = 0; i < iterations; i++) {
141+
const op = Math.random();
142+
143+
if (op < 0.85) {
144+
// 85% peek() - event loop checking next timer constantly
145+
heap.peek();
146+
} else if (op < 0.92) {
147+
// 7% percolateDown (timer reschedule)
148+
const min = heap.peek();
149+
if (min) {
150+
min.expiry += 10;
151+
heap.percolateDown(1);
152+
}
153+
} else {
154+
// 8% shift + insert
155+
if (heap.size > 0) heap.shift();
156+
heap.insert({
157+
expiry: 50 + Math.floor(Math.random() * 10000),
158+
id: i,
159+
pos: null,
160+
});
161+
}
162+
}
163+
164+
bench.end(iterations);
165+
}
166+
167+
function main({ queueSize, impl, workload }) {
168+
switch (workload) {
169+
case 'web-server':
170+
benchmarkWebServer(queueSize, impl);
171+
break;
172+
case 'microservice':
173+
benchmarkMicroservice(queueSize, impl);
174+
break;
175+
case 'real-time':
176+
benchmarkRealTime(queueSize, impl);
177+
break;
178+
default:
179+
throw new Error(`Unknown workload: ${workload}`);
180+
}
181+
}

0 commit comments

Comments
 (0)