Skip to content

Commit 42cfb80

Browse files
committed
stream: improve performance for sync write finishes
Improve performance and reduce memory usage when a writable stream is written to with the same callback (which is the most common case) and when the write operation finishes synchronously (which is also often the case). confidence improvement accuracy (*) (**) (***) streams/writable-manywrites.js sync='no' n=2000000 0.99 % ±3.20% ±4.28% ±5.61% streams/writable-manywrites.js sync='yes' n=2000000 *** 710.69 % ±19.65% ±26.47% ±35.09% Refs: nodejs#18013 Refs: nodejs#18367
1 parent 3e79c00 commit 42cfb80

File tree

3 files changed

+67
-8
lines changed

3 files changed

+67
-8
lines changed

benchmark/streams/writable-manywrites.js

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,19 @@ const common = require('../common');
44
const Writable = require('stream').Writable;
55

66
const bench = common.createBenchmark(main, {
7-
n: [2e6]
7+
n: [2e6],
8+
sync: ['yes', 'no']
89
});
910

10-
function main({ n }) {
11+
function main({ n, sync }) {
1112
const b = Buffer.allocUnsafe(1024);
1213
const s = new Writable();
14+
sync = sync === 'yes';
1315
s._write = function(chunk, encoding, cb) {
14-
cb();
16+
if (sync)
17+
cb();
18+
else
19+
process.nextTick(cb);
1520
};
1621

1722
bench.start();

lib/_stream_writable.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ function WritableState(options, stream, isDuplex) {
142142
// The amount that is being written when _write is called.
143143
this.writelen = 0;
144144

145+
// Storage for data passed to the afterWrite() callback in case of
146+
// synchronous _write() completion.
147+
this.afterWriteTickInfo = null;
148+
145149
this.bufferedRequest = null;
146150
this.lastBufferedRequest = null;
147151

@@ -498,22 +502,42 @@ function onwrite(stream, er) {
498502
}
499503

500504
if (sync) {
501-
process.nextTick(afterWrite, stream, state, cb);
505+
// It is a common case that the callback passed to .write() is always
506+
// the same. In that case, we do not schedule a new nextTick(), but rather
507+
// just increase a counter, to improve performance and avoid memory
508+
// allocations.
509+
if (state.afterWriteTickInfo !== null &&
510+
state.afterWriteTickInfo.cb === cb) {
511+
state.afterWriteTickInfo.count++;
512+
} else {
513+
state.afterWriteTickInfo = { count: 1, cb, stream, state };
514+
process.nextTick(afterWriteTick, state.afterWriteTickInfo);
515+
}
502516
} else {
503-
afterWrite(stream, state, cb);
517+
afterWrite(stream, state, 1, cb);
504518
}
505519
}
506520
}
507521

508-
function afterWrite(stream, state, cb) {
522+
function afterWriteTick({ stream, state, count, cb }) {
523+
return afterWrite(stream, state, count, cb);
524+
}
525+
526+
function afterWrite(stream, state, count, cb) {
527+
state.afterWriteTickInfo = null;
528+
509529
const needDrain = !state.ending && !stream.destroyed && state.length === 0 &&
510530
state.needDrain;
511531
if (needDrain) {
512532
state.needDrain = false;
513533
stream.emit('drain');
514534
}
515-
state.pendingcb--;
516-
cb();
535+
536+
while (count-- > 0) {
537+
state.pendingcb--;
538+
cb();
539+
}
540+
517541
finishMaybe(stream, state);
518542
}
519543

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use strict';
2+
const common = require('../common');
3+
const { Console } = require('console');
4+
const { Writable } = require('stream');
5+
const async_hooks = require('async_hooks');
6+
7+
// Make sure that repeated calls to console.log(), and by extension
8+
// stream.write() for the underlying stream, allocate exactly 1 tick object.
9+
// At the time of writing, that is enough to ensure a flat memory profile
10+
// from repeated console.log() calls, rather than having callbacks pile up
11+
// over time, assuming that data can be written synchronously.
12+
// Refs: https://github.com/nodejs/node/issues/18013
13+
// Refs: https://github.com/nodejs/node/issues/18367
14+
15+
const checkTickCreated = common.mustCall();
16+
17+
async_hooks.createHook({
18+
init(id, type, triggerId, resoure) {
19+
if (type === 'TickObject') checkTickCreated();
20+
}
21+
}).enable();
22+
23+
const console = new Console(new Writable({
24+
write: common.mustCall((chunk, encoding, cb) => {
25+
cb();
26+
}, 100)
27+
}));
28+
29+
for (let i = 0; i < 100; i++)
30+
console.log(i);

0 commit comments

Comments
 (0)