From a10bdb51b18dfaad874f3702a1daea51ec2d4514 Mon Sep 17 00:00:00 2001 From: Brian White Date: Sat, 13 May 2017 10:07:44 -0400 Subject: [PATCH] http: improve outgoing string write performance PR-URL: https://github.com/nodejs/node/pull/13013 Reviewed-By: James M Snell --- lib/_http_outgoing.js | 143 ++++++++---------- test/parallel/test-http-abort-client.js | 5 +- .../test-http-client-aborted-event.js | 4 +- 3 files changed, 71 insertions(+), 81 deletions(-) diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 9b492df85a..84aa57151f 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -78,6 +78,9 @@ utcDate._onTimeout = function _onTimeout() { }; +function noopPendingOutput(amount) {} + + function OutgoingMessage() { Stream.call(this); @@ -117,7 +120,7 @@ function OutgoingMessage() { this._header = null; this[outHeadersKey] = null; - this._onPendingData = null; + this._onPendingData = noopPendingOutput; } 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._onPendingData(this._header.length); + var header = this._header; + if (this.output.length === 0) { + this.output = [header]; + this.outputEncodings = ['latin1']; + 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; -}; +} 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; diff --git a/test/parallel/test-http-abort-client.js b/test/parallel/test-http-abort-client.js index ada72dbcd0..644b73f2c9 100644 --- a/test/parallel/test-http-abort-client.js +++ b/test/parallel/test-http-abort-client.js @@ -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(); console.log(`Got res: ${res.statusCode}`); console.dir(res.headers); diff --git a/test/parallel/test-http-client-aborted-event.js b/test/parallel/test-http-client-aborted-event.js index a9036a927d..1e7feb7d58 100644 --- a/test/parallel/test-http-client-aborted-event.js +++ b/test/parallel/test-http-client-aborted-event.js @@ -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()); })); }));