Skip to content

Commit c163dc1

Browse files
Thiago SantosFarenheith
Thiago Santos
authored andcommitted
lib: performance improvement on readline async iterator
Using a direct approach to create the readline async iterator allowed an iteration over 20 to 58% faster. **BREAKING CHANGE**: With that change, the async iteterator obtained from the readline interface doesn't have the property "stream" any longer. This happened because it's no longer created through a Readable, instead, the async iterator is created directly from the events of the readline interface instance, so, if anyone is using that property, this change will break their code. Also, the Readable added a backpressure control that is fairly compensated by the use of FixedQueue + monitoring its size. This control wasn't really precise with readline before, though, because it only pauses the reading of the original stream, but the lines generated from the last message received from it was still emitted. For example: if the readable was paused at 1000 messages but the last one received generated 10k lines, but no further messages were emitted again until the queue was lower than the readable highWaterMark. A similar behavior still happens with the new implementation, but the highWaterMark used is fixed: 1024, and the original stream is resumed again only after the queue is cleared. Before making that change, I created a package implementing the same concept used here to validate it. You can find it [here](https://github.com/Farenheith/faster-readline-iterator) if this helps anyhow.
1 parent cf69964 commit c163dc1

6 files changed

+387
-135
lines changed

lib/events.js

Lines changed: 32 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
ArrayPrototypeShift,
2828
ArrayPrototypeSlice,
2929
ArrayPrototypeSplice,
30+
ArrayFrom,
3031
Boolean,
3132
Error,
3233
ErrorCaptureStackTrace,
@@ -63,6 +64,7 @@ const {
6364
ERR_UNHANDLED_ERROR
6465
},
6566
} = require('internal/errors');
67+
const getLinkedMap = require('internal/linkedMap');
6668

6769
const {
6870
validateAbortSignal,
@@ -386,30 +388,19 @@ EventEmitter.prototype.emit = function emit(type, ...args) {
386388
if (handler === undefined)
387389
return false;
388390

389-
if (typeof handler === 'function') {
390-
const result = handler.apply(this, args);
391+
const listeners = ArrayFrom(handler);
392+
const len = handler.length;
393+
for (let i = 0; i < len; ++i) {
394+
const result = listeners[i].apply(this, args);
391395

392396
// We check if result is undefined first because that
393397
// is the most common case so we do not pay any perf
394-
// penalty
398+
// penalty.
399+
// This code is duplicated because extracting it away
400+
// would make it non-inlineable.
395401
if (result !== undefined && result !== null) {
396402
addCatch(this, result, type, args);
397403
}
398-
} else {
399-
const len = handler.length;
400-
const listeners = arrayClone(handler);
401-
for (let i = 0; i < len; ++i) {
402-
const result = listeners[i].apply(this, args);
403-
404-
// We check if result is undefined first because that
405-
// is the most common case so we do not pay any perf
406-
// penalty.
407-
// This code is duplicated because extracting it away
408-
// would make it non-inlineable.
409-
if (result !== undefined && result !== null) {
410-
addCatch(this, result, type, args);
411-
}
412-
}
413404
}
414405

415406
return true;
@@ -442,36 +433,29 @@ function _addListener(target, type, listener, prepend) {
442433

443434
if (existing === undefined) {
444435
// Optimize the case of one listener. Don't need the extra array object.
445-
events[type] = listener;
436+
existing = events[type] = getLinkedMap().push(listener);
446437
++target._eventsCount;
438+
} else if (prepend) {
439+
existing.unshift(listener);
447440
} else {
448-
if (typeof existing === 'function') {
449-
// Adding the second element, need to change to array.
450-
existing = events[type] =
451-
prepend ? [listener, existing] : [existing, listener];
452-
// If we've already got an array, just append.
453-
} else if (prepend) {
454-
existing.unshift(listener);
455-
} else {
456-
existing.push(listener);
457-
}
441+
existing.push(listener);
442+
}
458443

459-
// Check for listener leak
460-
m = _getMaxListeners(target);
461-
if (m > 0 && existing.length > m && !existing.warned) {
462-
existing.warned = true;
463-
// No error code for this since it is a Warning
464-
// eslint-disable-next-line no-restricted-syntax
465-
const w = new Error('Possible EventEmitter memory leak detected. ' +
466-
`${existing.length} ${String(type)} listeners ` +
467-
`added to ${inspect(target, { depth: -1 })}. Use ` +
468-
'emitter.setMaxListeners() to increase limit');
469-
w.name = 'MaxListenersExceededWarning';
470-
w.emitter = target;
471-
w.type = type;
472-
w.count = existing.length;
473-
process.emitWarning(w);
474-
}
444+
// Check for listener leak
445+
m = _getMaxListeners(target);
446+
if (m > 0 && existing.length > m && !existing.warned) {
447+
existing.warned = true;
448+
// No error code for this since it is a Warning
449+
// eslint-disable-next-line no-restricted-syntax
450+
const w = new Error('Possible EventEmitter memory leak detected. ' +
451+
`${existing.length} ${String(type)} listeners ` +
452+
`added to ${inspect(target, { depth: -1 })}. Use ` +
453+
'emitter.setMaxListeners() to increase limit');
454+
w.name = 'MaxListenersExceededWarning';
455+
w.emitter = target;
456+
w.type = type;
457+
w.count = existing.length;
458+
process.emitWarning(w);
475459
}
476460

477461
return target;
@@ -564,39 +548,10 @@ EventEmitter.prototype.removeListener =
564548
const list = events[type];
565549
if (list === undefined)
566550
return this;
567-
568-
if (list === listener || list.listener === listener) {
569-
if (--this._eventsCount === 0)
570-
this._events = ObjectCreate(null);
571-
else {
551+
if (list?.remove(listener)) {
552+
if (list.length === 0) {
572553
delete events[type];
573-
if (events.removeListener)
574-
this.emit('removeListener', type, list.listener || listener);
575-
}
576-
} else if (typeof list !== 'function') {
577-
let position = -1;
578-
579-
for (let i = list.length - 1; i >= 0; i--) {
580-
if (list[i] === listener || list[i].listener === listener) {
581-
position = i;
582-
break;
583-
}
584-
}
585-
586-
if (position < 0)
587-
return this;
588-
589-
if (position === 0)
590-
list.shift();
591-
else {
592-
if (spliceOne === undefined)
593-
spliceOne = require('internal/util').spliceOne;
594-
spliceOne(list, position);
595554
}
596-
597-
if (list.length === 1)
598-
events[type] = list[0];
599-
600555
if (events.removeListener !== undefined)
601556
this.emit('removeListener', type, listener);
602557
}
@@ -720,19 +675,7 @@ EventEmitter.prototype.listenerCount = listenerCount;
720675
* @returns {number}
721676
*/
722677
function listenerCount(type) {
723-
const events = this._events;
724-
725-
if (events !== undefined) {
726-
const evlistener = events[type];
727-
728-
if (typeof evlistener === 'function') {
729-
return 1;
730-
} else if (evlistener !== undefined) {
731-
return evlistener.length;
732-
}
733-
}
734-
735-
return 0;
678+
return this._events?.[type]?.length || 0;
736679
}
737680

738681
/**

lib/internal/linkedMap.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
const {
2+
Map,
3+
SymbolIterator,
4+
} = primordials;
5+
6+
function push(root, value) {
7+
const node = { value };
8+
if (root.last) {
9+
node.previous = root.last;
10+
root.last.next = root.last = node;
11+
} else {
12+
root.last = root.first = node;
13+
}
14+
root.length++;
15+
16+
return node;
17+
}
18+
19+
function unshift(value) {
20+
const node = { value };
21+
if (root.first) {
22+
node.next = root.first;
23+
root.first.previous = root.first = node;
24+
} else {
25+
root.last = root.first = node;
26+
}
27+
root.length++;
28+
29+
return node;
30+
}
31+
32+
function shift(root) {
33+
if (root.first) {
34+
const { value } = root.first;
35+
root.first = root.first.next;
36+
root.length--;
37+
return value;
38+
}
39+
}
40+
41+
42+
function getLinkedMap() {
43+
const map = new Map();
44+
function addToMap(key, node, operation) {
45+
let refs = map.get(key);
46+
if (!refs) {
47+
map.set(key, refs = { length: 0 });
48+
}
49+
operation(refs, node);
50+
}
51+
const root = { length: 0 };
52+
53+
return {
54+
get length() {
55+
return root.length;
56+
},
57+
push(value) {
58+
const node = push(root, value);
59+
addToMap(value, node, push);
60+
61+
return this;
62+
},
63+
unshift(value) {
64+
const node = unshift(root, value);
65+
addToMap(key, node, unshift);
66+
67+
return this;
68+
},
69+
remove(value) {
70+
const refs = map.get(value);
71+
if (refs) {
72+
const result = shift(refs);
73+
if (result.previous)
74+
result.previous.next = result.next;
75+
if (result.next)
76+
result.next.previous = result.previous;
77+
if (refs.length === 0) {
78+
map.delete(value);
79+
}
80+
root.length--;
81+
return 1;
82+
}
83+
return 0;
84+
},
85+
[SymbolIterator]() {
86+
let node = root.first;
87+
88+
const iterator = {
89+
next() {
90+
if (!node) {
91+
return { done: true };
92+
}
93+
const result = {
94+
done: false,
95+
value: node.value,
96+
};
97+
node = node.next;
98+
return result;
99+
},
100+
[SymbolIterator]() {
101+
return iterator;
102+
}
103+
};
104+
105+
return iterator;
106+
}
107+
}
108+
}
109+
110+
module.exports = getLinkedMap;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
'use strict';
2+
const {
3+
Promise,
4+
SymbolAsyncIterator,
5+
ArrayPrototypeConcat,
6+
} = primordials;
7+
const FixedQueue = require('internal/fixed_queue');
8+
9+
const PAUSE_THRESHOLD = 1024;
10+
const RESUME_THRESHOLD = 1;
11+
const ITEM_EVENTS = ['data'];
12+
const CLOSE_EVENTS = ['close', 'end'];
13+
const ERROR_EVENTS = ['error'];
14+
15+
16+
function waitNext(emitter, next, events) {
17+
return new Promise((resolve, reject) => {
18+
const resolveNext = () => {
19+
for (let i = 0; i < events.length; i++)
20+
emitter.off(events[i], resolveNext);
21+
try {
22+
resolve(next());
23+
} catch (promiseError) {
24+
reject(promiseError);
25+
}
26+
};
27+
for (let i = 0; i < events.length; i++)
28+
emitter.once(events[i], resolveNext);
29+
});
30+
}
31+
32+
module.exports = function eventsToAsyncIteratorFactory(readable, {
33+
pauseThreshold = PAUSE_THRESHOLD,
34+
resumeThreshold = RESUME_THRESHOLD,
35+
closeEvents = CLOSE_EVENTS,
36+
itemEvents = ITEM_EVENTS,
37+
errorEvents = ERROR_EVENTS,
38+
}) {
39+
const events = ArrayPrototypeConcat(itemEvents, errorEvents, closeEvents);
40+
const highWaterMark = RESUME_THRESHOLD;
41+
42+
const queue = new FixedQueue();
43+
let done = false;
44+
let error;
45+
let queueSize = 0;
46+
let paused = false;
47+
const onError = (value) => {
48+
turn('off');
49+
error = value;
50+
};
51+
const onClose = () => {
52+
turn('off');
53+
done = true;
54+
};
55+
const onItem = (value) => {
56+
queue.push(value);
57+
queueSize++;
58+
if (queueSize >= pauseThreshold) {
59+
paused = true;
60+
readable.pause();
61+
}
62+
};
63+
function turn(onOff) {
64+
for (let i = 0; i < closeEvents.length; i++)
65+
readable[onOff](closeEvents[i], onClose);
66+
for (let i = 0; i < itemEvents.length; i++)
67+
readable[onOff](itemEvents[i], onItem);
68+
for (let i = 0; i < itemEvents.length; i++)
69+
readable[onOff](errorEvents[i], onError);
70+
}
71+
72+
turn('on');
73+
74+
function next() {
75+
if (!queue.isEmpty()) {
76+
const value = queue.shift();
77+
queueSize--;
78+
if (queueSize < resumeThreshold) {
79+
paused = false;
80+
readable.resume();
81+
}
82+
return {
83+
done: false,
84+
value,
85+
};
86+
}
87+
if (error) {
88+
throw error;
89+
}
90+
if (done) {
91+
return { done };
92+
}
93+
return waitNext(readable, next, events);
94+
}
95+
96+
return {
97+
next,
98+
highWaterMark,
99+
get isPaused() {
100+
return paused;
101+
},
102+
get queueSize() {
103+
return queueSize;
104+
},
105+
[SymbolAsyncIterator]() { return result; },
106+
};
107+
};

0 commit comments

Comments
 (0)