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

http: improve outgoing string write performance #13013

Merged
merged 1 commit into from May 26, 2017
Jump to file or symbol
Failed to load files and symbols.
+71 −81
Diff settings

Always

Just for now

Copy path View file
@@ -78,6 +78,9 @@ utcDate._onTimeout = function _onTimeout() {
};
function noopPendingOutput(amount) {}

This comment has been minimized.

@thefourtheye

thefourtheye May 16, 2017

Contributor

Mmmm, I thought our linter would complain about amount. It didn't.

function OutgoingMessage() {
Stream.call(this);
@@ -117,7 +120,7 @@ function OutgoingMessage() {
this._header = null;
this[outHeadersKey] = null;
this._onPendingData = null;
this._onPendingData = noopPendingOutput;

This comment has been minimized.

@refack

refack May 16, 2017

Member

Since you don't need it's name to unregister why not just () => {}?

This comment has been minimized.

@mscdex

mscdex May 16, 2017

Contributor

To avoid creating a new noop for each response.

}
util.inherits(OutgoingMessage, Stream);
@@ -234,12 +237,18 @@ OutgoingMessage.prototype._send = function _send(data, encoding, callback) {
(encoding === 'utf8' || encoding === 'latin1' || !encoding)) {
data = this._header + data;
} else {
this.output.unshift(this._header);
this.outputEncodings.unshift('latin1');
this.outputCallbacks.unshift(null);
this.outputSize += this._header.length;
if (typeof this._onPendingData === 'function')

This comment has been minimized.

@refack

refack May 16, 2017

Member

I like the "polymorphic" approach 👍
Question is the noop call faster than the if (typeof

This comment has been minimized.

@mscdex

mscdex May 16, 2017

Contributor

I didn't measure it by itself, but it should be to some degree since it no longer has to execute a conditional before calling the function.

this._onPendingData(this._header.length);
var header = this._header;
if (this.output.length === 0) {
this.output = [header];
this.outputEncodings = ['latin1'];

This comment has been minimized.

@trevnorris

trevnorris May 23, 2017

Contributor

General note on how we handle the response body. Browsers treat 'charset=latin1' as windows-1252, which is problematic. Example:

const headers =
  'HTTP/1.1 200 OK\r\n' +
  'Content-Type: text/html; charset=latin1\r\n' +
  'Content-Length: 2\r\n\r\n' +
  // Technically 0x83 is a control code, but browsers don't see it that way.
  '\u0083\n';
const data = Buffer.from(headers, 'latin1');

require('net').createServer(c => c.end(data)).listen(8778);

Hit that with the browser and you'll see ƒ, not a control character symbol.

this.outputCallbacks = [null];
} else {
this.output.unshift(header);
this.outputEncodings.unshift('latin1');
this.outputCallbacks.unshift(null);
}
this.outputSize += header.length;
this._onPendingData(header.length);
}
this._headerSent = true;
}
@@ -275,19 +284,13 @@ function _writeRaw(data, encoding, callback) {
return conn.write(data, encoding, callback);
}
// Buffer, as long as we're not destroyed.
return this._buffer(data, encoding, callback);
}
OutgoingMessage.prototype._buffer = function _buffer(data, encoding, callback) {
this.output.push(data);
this.outputEncodings.push(encoding);
this.outputCallbacks.push(callback);
this.outputSize += data.length;
if (typeof this._onPendingData === 'function')
this._onPendingData(data.length);
this._onPendingData(data.length);
return false;
};
}

This comment has been minimized.

@refack

refack May 16, 2017

Member

Isn't this the end of OutgoingMessage.prototype._send = function _send, so should have ;

This comment has been minimized.

@mscdex

mscdex May 16, 2017

Contributor

No, this is the end of _writeRaw() which uses a function declaration, so there is no semicolon needed.

OutgoingMessage.prototype._storeHeader = _storeHeader;
@@ -624,27 +627,31 @@ Object.defineProperty(OutgoingMessage.prototype, 'headersSent', {
const crlf_buf = Buffer.from('\r\n');
OutgoingMessage.prototype.write = function write(chunk, encoding, callback) {
if (this.finished) {
return write_(this, chunk, encoding, callback, false);
};
function write_(msg, chunk, encoding, callback, fromEnd) {
if (msg.finished) {
var err = new Error('write after end');
nextTick(this.socket[async_id_symbol],
writeAfterEndNT.bind(this),
nextTick(msg.socket[async_id_symbol],
writeAfterEndNT.bind(msg),
err,
callback);
return true;
}
if (!this._header) {
this._implicitHeader();
if (!msg._header) {
msg._implicitHeader();
}
if (!this._hasBody) {
if (!msg._hasBody) {
debug('This type of response MUST NOT have a body. ' +
'Ignoring write() calls.');
return true;
}
if (typeof chunk !== 'string' && !(chunk instanceof Buffer)) {
if (!fromEnd && typeof chunk !== 'string' && !(chunk instanceof Buffer)) {
throw new TypeError('First argument must be a string or Buffer');
}
@@ -654,38 +661,28 @@ OutgoingMessage.prototype.write = function write(chunk, encoding, callback) {
if (chunk.length === 0) return true;
var len, ret;
if (this.chunkedEncoding) {
if (typeof chunk === 'string' &&
encoding !== 'hex' &&
encoding !== 'base64' &&
encoding !== 'latin1') {
if (msg.chunkedEncoding) {
if (typeof chunk === 'string')
len = Buffer.byteLength(chunk, encoding);
chunk = len.toString(16) + CRLF + chunk + CRLF;
ret = this._send(chunk, encoding, callback);
} else {
// buffer, or a non-toString-friendly encoding
if (typeof chunk === 'string')
len = Buffer.byteLength(chunk, encoding);
else
len = chunk.length;
if (this.connection && !this.connection.corked) {
this.connection.cork();
process.nextTick(connectionCorkNT, this.connection);
}
else
len = chunk.length;
this._send(len.toString(16), 'latin1', null);
this._send(crlf_buf, null, null);
this._send(chunk, encoding, null);
ret = this._send(crlf_buf, null, callback);
if (msg.connection && !msg.connection.corked) {
msg.connection.cork();
process.nextTick(connectionCorkNT, msg.connection);
}
msg._send(len.toString(16), 'latin1', null);
msg._send(crlf_buf, null, null);
msg._send(chunk, encoding, null);
ret = msg._send(crlf_buf, null, callback);
} else {
ret = this._send(chunk, encoding, callback);
ret = msg._send(chunk, encoding, callback);
}
debug('write ret = ' + ret);
return ret;
};
}
function writeAfterEndNT(err, callback) {
@@ -736,49 +733,40 @@ function onFinish(outmsg) {
outmsg.emit('finish');
}
OutgoingMessage.prototype.end = function end(data, encoding, callback) {
if (typeof data === 'function') {
callback = data;
data = null;
OutgoingMessage.prototype.end = function end(chunk, encoding, callback) {
if (typeof chunk === 'function') {
callback = chunk;
chunk = null;
} else if (typeof encoding === 'function') {
callback = encoding;
encoding = null;
}
if (data && typeof data !== 'string' && !(data instanceof Buffer)) {
throw new TypeError('First argument must be a string or Buffer');
}
if (this.finished) {
return false;
}
if (!this._header) {
if (data) {
if (typeof data === 'string')
this._contentLength = Buffer.byteLength(data, encoding);
var uncork;
if (chunk) {
if (typeof chunk !== 'string' && !(chunk instanceof Buffer)) {
throw new TypeError('First argument must be a string or Buffer');
}
if (!this._header) {
if (typeof chunk === 'string')
this._contentLength = Buffer.byteLength(chunk, encoding);
else
this._contentLength = data.length;
} else {
this._contentLength = 0;
this._contentLength = chunk.length;
}
if (this.connection) {
this.connection.cork();
uncork = true;
}
write_(this, chunk, encoding, null, true);
} else if (!this._header) {
this._contentLength = 0;
this._implicitHeader();
}
if (data && !this._hasBody) {
debug('This type of response MUST NOT have a body. ' +
'Ignoring data passed to end().');
data = null;
}
if (this.connection && data)
this.connection.cork();
if (data) {
// Normal body write.
this.write(data, encoding);
}
if (typeof callback === 'function')
this.once('finish', callback);
@@ -792,7 +780,7 @@ OutgoingMessage.prototype.end = function end(data, encoding, callback) {
ret = this._send('', 'latin1', finish);
}
if (this.connection && data)
if (uncork)
this.connection.uncork();
this.finished = true;
@@ -871,8 +859,7 @@ OutgoingMessage.prototype._flushOutput = function _flushOutput(socket) {
this.output = [];
this.outputEncodings = [];
this.outputCallbacks = [];
if (typeof this._onPendingData === 'function')
this._onPendingData(-this.outputSize);
this._onPendingData(-this.outputSize);
this.outputSize = 0;
return ret;
@@ -23,12 +23,12 @@
const common = require('../common');
const http = require('http');
let serverRes;
const server = http.Server(function(req, res) {
console.log('Server accepted request.');
serverRes = res;
res.writeHead(200);
res.write('Part of my res.');
res.destroy();
});
server.listen(0, common.mustCall(function() {
@@ -37,6 +37,7 @@ server.listen(0, common.mustCall(function() {
headers: { connection: 'keep-alive' }
}, common.mustCall(function(res) {
server.close();
serverRes.destroy();

This comment has been minimized.

@refack

refack May 16, 2017

Member

What is the benefit of moving it here?
Found the comment.

console.log(`Got res: ${res.statusCode}`);
console.dir(res.headers);
@@ -2,9 +2,10 @@
const common = require('../common');
const http = require('http');
let serverRes;
const server = http.Server(function(req, res) {
res.write('Part of my res.');
res.destroy();
serverRes = res;
});
server.listen(0, common.mustCall(function() {
@@ -13,6 +14,7 @@ server.listen(0, common.mustCall(function() {
headers: { connection: 'keep-alive' }
}, common.mustCall(function(res) {
server.close();
serverRes.destroy();
res.on('aborted', common.mustCall());
}));
}));
ProTip! Use n and p to navigate between commits in a pull request.