Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[x] stream: simplify writable buffering #29026

Closed
wants to merge 4 commits into from
Closed
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 56 additions & 114 deletions lib/_stream_writable.js
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ function WritableState(options, stream, isDuplex) {
// The amount that is being written when _write is called.
this.writelen = 0;

this.bufferedRequest = null;
this.lastBufferedRequest = null;
this.buffered = [];
this.buffered.allBuffers = true;

// Number of pending user-supplied write callbacks
// this must be 0 before 'finish' can be emitted
Expand All @@ -154,25 +154,10 @@ function WritableState(options, stream, isDuplex) {

// Should .destroy() be called after 'finish' (and potentially 'end')
this.autoDestroy = !!options.autoDestroy;

// Count buffered requests
this.bufferedRequestCount = 0;

// Allocate the first CorkedRequest, there is always
// one allocated and free to use, and we maintain at most two
const corkReq = { next: null, entry: null, finish: undefined };
corkReq.finish = onCorkedFinish.bind(undefined, corkReq, this);
this.corkedRequestsFree = corkReq;
}

WritableState.prototype.getBuffer = function getBuffer() {
var current = this.bufferedRequest;
const out = [];
while (current) {
out.push(current);
current = current.next;
}
return out;
return this.buffered;
};

Object.defineProperty(WritableState.prototype, 'buffer', {
Expand All @@ -182,6 +167,12 @@ Object.defineProperty(WritableState.prototype, 'buffer', {
'instead.', 'DEP0003')
});

Object.defineProperty(WritableState.prototype, 'bufferedRequestCount', {
get() {
return this.buffered.length;
}
});

// Test _writableState for inheritance to account for Duplex streams,
// whose prototype chain only points to Readable.
var realHasInstance;
Expand Down Expand Up @@ -315,12 +306,7 @@ Writable.prototype.uncork = function() {

if (state.corked) {
state.corked--;

if (!state.writing &&
!state.corked &&
!state.bufferProcessing &&
state.bufferedRequest)
clearBuffer(this, state);
clearBuffer(this, state);
}
};

Expand Down Expand Up @@ -376,7 +362,7 @@ Object.defineProperty(Writable.prototype, 'writableHighWaterMark', {
// If we're already writing something, then just put this
// in the queue, and wait our turn. Otherwise, call _write
// If we return false, then we need a drain event, so set that flag.
function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) {
function writeOrBuffer(stream, state, isBuf, chunk, encoding, callback) {
if (!isBuf) {
var newChunk = decodeChunk(state, chunk, encoding);
if (chunk !== newChunk) {
Expand All @@ -395,22 +381,11 @@ function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) {
state.needDrain = true;

if (state.writing || state.corked) {
var last = state.lastBufferedRequest;
state.lastBufferedRequest = {
chunk,
encoding,
isBuf,
callback: cb,
next: null
};
if (last) {
last.next = state.lastBufferedRequest;
} else {
state.bufferedRequest = state.lastBufferedRequest;
}
state.bufferedRequestCount += 1;
const buffered = state.buffered;
buffered.push({ chunk, encoding, callback });
buffered.allBuffers = isBuf && buffered.allBuffers;
} else {
doWrite(stream, state, false, len, chunk, encoding, cb);
doWrite(stream, state, false, len, chunk, encoding, callback);
}

return ret;
Expand Down Expand Up @@ -471,10 +446,7 @@ function onwrite(stream, er) {
// Check if we're actually ready to finish, but don't emit yet
var finished = needFinish(state) || stream.destroyed;

if (!finished &&
!state.corked &&
!state.bufferProcessing &&
state.bufferedRequest) {
if (!finished) {
clearBuffer(stream, state);
}

Expand All @@ -500,67 +472,51 @@ function afterWrite(stream, state, cb) {

// If there's something in the buffer waiting, then process it
function clearBuffer(stream, state) {
state.bufferProcessing = true;
var entry = state.bufferedRequest;

if (stream._writev && entry && entry.next) {
// Fast case, write everything using _writev()
var l = state.bufferedRequestCount;
var buffer = new Array(l);
var holder = state.corkedRequestsFree;
holder.entry = entry;

var count = 0;
var allBuffers = true;
while (entry) {
buffer[count] = entry;
if (!entry.isBuf)
allBuffers = false;
entry = entry.next;
count += 1;
}
buffer.allBuffers = allBuffers;
const buffered = state.buffered;
const bufferedLength = buffered.length;

doWrite(stream, state, true, state.length, buffer, '', holder.finish);
if (!bufferedLength || state.corked || state.bufferProcessing ||
state.writing) {
return;
}

// doWrite is almost always async, defer these to save a bit of time
// as the hot path ends with doWrite
state.bufferProcessing = true;
if (bufferedLength === 1) {
const chunk = buffered[0].chunk;
const encoding = buffered[0].encoding;
const callback = buffered[0].callback;

const len = state.objectMode ? 1 : chunk.length;
doWrite(stream, state, false, len, chunk, encoding, callback);
buffered.length = 0;
buffered.allBuffers = true;
} else if (stream._writev) {
state.pendingcb++;
state.lastBufferedRequest = null;
if (holder.next) {
state.corkedRequestsFree = holder.next;
holder.next = null;
} else {
var corkReq = { next: null, entry: null, finish: undefined };
corkReq.finish = onCorkedFinish.bind(undefined, corkReq, state);
state.corkedRequestsFree = corkReq;
// doWrite mutates buffered array. Keep a copy of callbacks.
const callbacks = [];
ronag marked this conversation as resolved.
Show resolved Hide resolved
for (let n = 0; n < buffered.length; ++n) {
callbacks.push(buffered[n].callback);
ronag marked this conversation as resolved.
Show resolved Hide resolved
}
state.bufferedRequestCount = 0;
} else {
// Slow case, write chunks one-by-one
while (entry) {
var chunk = entry.chunk;
var encoding = entry.encoding;
var cb = entry.callback;
var len = state.objectMode ? 1 : chunk.length;

doWrite(stream, state, false, len, chunk, encoding, cb);
entry = entry.next;
state.bufferedRequestCount--;
// If we didn't call the onwrite immediately, then
// it means that we need to wait until it does.
// also, that means that the chunk and cb are currently
// being processed, so move the buffer counter past them.
if (state.writing) {
break;
doWrite(stream, state, true, state.length, buffered, '', (err) => {
for (const callback of callbacks) {
state.pendingcb--;
callback(err);
}
});
state.buffered = [];
state.buffered.allBuffers = true;
} else {
let i;
for (i = 0; i < bufferedLength && !state.writing; i++) {
const chunk = buffered[i].chunk;
const encoding = buffered[i].encoding;
const callback = buffered[i].callback;

const len = state.objectMode ? 1 : chunk.length;
doWrite(stream, state, false, len, chunk, encoding, callback);
}

if (entry === null)
state.lastBufferedRequest = null;
buffered.splice(0, i);
}

state.bufferedRequest = entry;
state.bufferProcessing = false;
}

Expand Down Expand Up @@ -617,8 +573,8 @@ Object.defineProperty(Writable.prototype, 'writableLength', {

function needFinish(state) {
return (state.ending &&
state.length === 0 &&
state.bufferedRequest === null &&
!state.length &&
!state.buffered.length &&
!state.finished &&
!state.writing);
}
Expand Down Expand Up @@ -681,20 +637,6 @@ function endWritable(stream, state, cb) {
stream.writable = false;
}

function onCorkedFinish(corkReq, state, err) {
var entry = corkReq.entry;
corkReq.entry = null;
while (entry) {
var cb = entry.callback;
state.pendingcb--;
cb(err);
entry = entry.next;
}

// Reuse the free corkReq.
state.corkedRequestsFree.next = corkReq;
}

Object.defineProperty(Writable.prototype, 'destroyed', {
// Making it explicit this property is not enumerable
// because otherwise some prototype manipulation in
Expand Down