summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Noordhuis <info@bnoordhuis.nl>2013-07-31 05:50:26 +0200
committerBen Noordhuis <info@bnoordhuis.nl>2013-07-31 13:23:06 +0200
commit3398cce1934004e4d60c7d7605fa7548582c362f (patch)
tree9bf39b8f57d858d86148118f049cecbc5a0ebf07
parent6359e017ac76e971089bcfcffe8dcb921f7f4db4 (diff)
downloadnode-3398cce1934004e4d60c7d7605fa7548582c362f.tar.gz
http: improve chunked res.write(buf) performance
Avoid a costly buffer-to-string operation. Instead, allocate a new buffer, copy the chunk header and data into it and send that. The speed difference is negligible on small payloads but it really shines with larger (10+ kB) chunks. benchmark/http/end-vs-write-end with 64 kB chunks gives 45-50% higher throughput. With 1 MB chunks, the difference is a staggering 590%. Of course, YMMV will vary with real workloads and networks but this commit should have a positive impact on CPU and memory consumption. Big kudos to Wyatt Preul (@wpreul) for reporting the issue and providing the initial patch. Fixes #5941 and #5944.
-rw-r--r--lib/http.js157
1 files changed, 93 insertions, 64 deletions
diff --git a/lib/http.js b/lib/http.js
index 7527fde1a..339a889be 100644
--- a/lib/http.js
+++ b/lib/http.js
@@ -447,6 +447,8 @@ function OutgoingMessage() {
this.useChunkedEncodingByDefault = true;
this.sendDate = false;
+ this._headerSent = false;
+ this._header = '';
this._hasBody = true;
this._trailer = '';
@@ -768,6 +770,92 @@ Object.defineProperty(OutgoingMessage.prototype, 'headersSent', {
});
+// Convert a number in the range 0-15 to a lowercase hexadecimal digit.
+function hex(val) {
+ // The comparison and bit hacks are deliberate. We could look up the
+ // value in a buffer with hexdigits[val & 15] but that adds a couple
+ // of bounds checks to each conversion.
+ return val <= 9 ? (val | 48) : ((val - 9) | 96);
+}
+
+
+function chunkify(chunk, headers, trailers, last) {
+ var chunklen = chunk.length;
+ var buflen = chunklen + 4; // '\r\n' + chunk + '\r\n'
+ var offset = 0;
+ var octets = 1;
+
+ // Skip expensive Buffer.byteLength() calls; only ISO-8859-1 characters
+ // are allowed in HTTP headers, therefore:
+ //
+ // headers.length == Buffer.byteLength(headers.length)
+ // trailers.length == Buffer.byteLength(trailers.length)
+ //
+ // Note: the actual encoding that is used is ASCII. That's de jure
+ // a violation of the spec but de facto correct because many HTTP
+ // clients get confused by non-ASCII headers.
+ if (last === true) buflen += 5; // '0\r\n\r\n'
+ if (headers !== '') buflen += headers.length;
+ if (trailers !== '') buflen += trailers.length;
+
+ if (chunklen & 0xf0000000) octets += 7;
+ else if (chunklen & 0xf000000) octets += 6;
+ else if (chunklen & 0xf00000) octets += 5;
+ else if (chunklen & 0xf0000) octets += 4;
+ else if (chunklen & 0xf000) octets += 3;
+ else if (chunklen & 0xf00) octets += 2;
+ else if (chunklen & 0xf0) octets += 1;
+ buflen += octets;
+
+ var buf = new Buffer(buflen);
+
+ if (headers !== '') {
+ buf.write(headers, 0, headers.length, 'ascii');
+ offset = headers.length;
+ }
+
+ // Write chunk length in hex to buffer. This effectively limits us
+ // to 4 GB chunks but that's okay because buffers are max 1 GB anyway.
+ switch (octets) {
+ case 8: buf[offset++] = hex((chunklen >>> 28) & 15);
+ case 7: buf[offset++] = hex((chunklen >>> 24) & 15);
+ case 6: buf[offset++] = hex((chunklen >>> 20) & 15);
+ case 5: buf[offset++] = hex((chunklen >>> 16) & 15);
+ case 4: buf[offset++] = hex((chunklen >>> 12) & 15);
+ case 3: buf[offset++] = hex((chunklen >>> 8) & 15);
+ case 2: buf[offset++] = hex((chunklen >>> 4) & 15);
+ }
+ buf[offset++] = hex(chunklen & 15);
+
+ // Add '\r\n'.
+ buf[offset++] = 13;
+ buf[offset++] = 10;
+
+ // Copy buffer.
+ chunk.copy(buf, offset);
+ offset += chunklen;
+
+ // Add trailing '\r\n'.
+ buf[offset++] = 13;
+ buf[offset++] = 10;
+
+ if (last === true) {
+ // Add trailing '0\r\n\r\n'.
+ buf[offset++] = 48;
+ buf[offset++] = 13;
+ buf[offset++] = 10;
+ buf[offset++] = 13;
+ buf[offset++] = 10;
+ }
+
+ if (trailers !== '') {
+ buf.write(trailers, offset, trailers.length, 'ascii');
+ }
+
+ return buf;
+}
+
+
OutgoingMessage.prototype.write = function(chunk, encoding) {
if (!this._header) {
this._implicitHeader();
@@ -787,23 +875,6 @@ OutgoingMessage.prototype.write = function(chunk, encoding) {
// signal the user to keep writing.
if (chunk.length === 0) return true;
- // TODO(bnoordhuis) Temporary optimization hack, remove in v0.11. We only
- // want to convert the buffer when we're sending:
- //
- // a) Transfer-Encoding chunks, because it lets us pack the chunk header
- // and the chunk into a single write(), or
- //
- // b) the first chunk of a fixed-length request, because it lets us pack
- // the request headers and the chunk into a single write().
- //
- // Converting to strings is expensive, CPU-wise, but reducing the number
- // of write() calls more than makes up for that because we're dramatically
- // reducing the number of TCP roundtrips.
- if (chunk instanceof Buffer && (this.chunkedEncoding || !this._headerSent)) {
- chunk = chunk.toString('binary');
- encoding = 'binary';
- }
-
var len, ret;
if (this.chunkedEncoding) {
if (typeof(chunk) === 'string' &&
@@ -812,8 +883,11 @@ OutgoingMessage.prototype.write = function(chunk, encoding) {
len = Buffer.byteLength(chunk, encoding);
chunk = len.toString(16) + CRLF + chunk + CRLF;
ret = this._send(chunk, encoding);
+ } else if (Buffer.isBuffer(chunk)) {
+ var buf = chunkify(chunk, '', '', false);
+ ret = this._send(buf, encoding);
} else {
- // buffer, or a non-toString-friendly encoding
+ // Non-toString-friendly encoding.
len = chunk.length;
this._send(len.toString(16) + CRLF);
this._send(chunk, encoding);
@@ -900,52 +974,7 @@ OutgoingMessage.prototype.end = function(data, encoding) {
}
} else if (Buffer.isBuffer(data)) {
if (this.chunkedEncoding) {
- var chunk_size = data.length.toString(16);
-
- // Skip expensive Buffer.byteLength() calls; only ISO-8859-1 characters
- // are allowed in HTTP headers. Therefore:
- //
- // this._header.length == Buffer.byteLength(this._header.length)
- // this._trailer.length == Buffer.byteLength(this._trailer.length)
- //
- var header_len = this._header.length;
- var chunk_size_len = chunk_size.length;
- var data_len = data.length;
- var trailer_len = this._trailer.length;
-
- var len = header_len +
- chunk_size_len +
- 2 + // '\r\n'.length
- data_len +
- 5 + // '\r\n0\r\n'.length
- trailer_len +
- 2; // '\r\n'.length
-
- var buf = new Buffer(len);
- var off = 0;
-
- buf.write(this._header, off, header_len, 'ascii');
- off += header_len;
-
- buf.write(chunk_size, off, chunk_size_len, 'ascii');
- off += chunk_size_len;
-
- crlf_buf.copy(buf, off);
- off += 2;
-
- data.copy(buf, off);
- off += data_len;
-
- zero_chunk_buf.copy(buf, off);
- off += 5;
-
- if (trailer_len > 0) {
- buf.write(this._trailer, off, trailer_len, 'ascii');
- off += trailer_len;
- }
-
- crlf_buf.copy(buf, off);
-
+ var buf = chunkify(data, this._header, this._trailer, true);
ret = this.connection.write(buf);
} else {
var header_len = this._header.length;