Skip to content

Commit c99a6c4

Browse files
committed
lib: optimize FixedQueue by reusing emptied segment
1 parent 947ea53 commit c99a6c4

File tree

2 files changed

+45
-11
lines changed

2 files changed

+45
-11
lines changed

benchmark/util/fixed-queue.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
'use strict';
2+
3+
const common = require('../common');
4+
5+
const bench = common.createBenchmark(main, {
6+
n: [1e5],
7+
}, { flags: ['--expose-internals'] });
8+
9+
function main({ n }) {
10+
const FixedQueue = require('internal/fixed_queue');
11+
const queue = new FixedQueue();
12+
bench.start();
13+
for (let i = 0; i < n; i++)
14+
queue.push(i);
15+
for (let i = 0; i < n; i++)
16+
queue.shift();
17+
bench.end(n);
18+
}

lib/internal/fixed_queue.js

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ const kMask = kSize - 1;
4949
// | undefined | | item |
5050
// +-----------+ +-----------+
5151
//
52+
// Implementation detail: To reduce allocations, a single spare
53+
// FixedCircularBuffer may be kept for reuse. When a segment empties, it
54+
// is stored as the spare, and when the head fills, the spare is linked
55+
// in. This does not change the active list structure or queue semantics.
56+
//
5257
// Adding a value means moving `top` forward by one, removing means
5358
// moving `bottom` forward by one. After reaching the end, the queue
5459
// wraps around.
@@ -74,36 +79,46 @@ class FixedCircularBuffer {
7479
}
7580

7681
push(data) {
77-
this.list[this.top] = data;
78-
this.top = (this.top + 1) & kMask;
82+
const top = this.top;
83+
this.list[top] = data;
84+
this.top = (top + 1) & kMask;
7985
}
8086

8187
shift() {
82-
const nextItem = this.list[this.bottom];
88+
const bottom = this.bottom;
89+
const nextItem = this.list[bottom];
8390
if (nextItem === undefined)
8491
return null;
85-
this.list[this.bottom] = undefined;
86-
this.bottom = (this.bottom + 1) & kMask;
92+
this.list[bottom] = undefined;
93+
this.bottom = (bottom + 1) & kMask;
8794
return nextItem;
8895
}
8996
}
9097

9198
module.exports = class FixedQueue {
9299
constructor() {
93100
this.head = this.tail = new FixedCircularBuffer();
101+
this._spare = null;
94102
}
95103

96104
isEmpty() {
97105
return this.head.isEmpty();
98106
}
99107

100108
push(data) {
101-
if (this.head.isFull()) {
102-
// Head is full: Creates a new queue, sets the old queue's `.next` to it,
103-
// and sets it as the new main queue.
104-
this.head = this.head.next = new FixedCircularBuffer();
109+
let head = this.head;
110+
if (head.isFull()) {
111+
// Head is full: reuse the spare buffer if available.
112+
// Otherwise create a new one. Link it and advance head.
113+
let nextBuf = this._spare;
114+
if (nextBuf !== null) {
115+
this._spare = null;
116+
} else {
117+
nextBuf = new FixedCircularBuffer();
118+
}
119+
this.head = head = head.next = nextBuf;
105120
}
106-
this.head.push(data);
121+
head.push(data);
107122
}
108123

109124
shift() {
@@ -112,7 +127,8 @@ module.exports = class FixedQueue {
112127
if (tail.isEmpty() && tail.next !== null) {
113128
// If there is another queue, it forms the new tail.
114129
this.tail = tail.next;
115-
tail.next = null;
130+
// Overwrite any existing spare
131+
this._spare = tail;
116132
}
117133
return next;
118134
}

0 commit comments

Comments
 (0)