http2: improve perf of passing headers to C++

By passing a single string rather than many small ones and
a single block allocation for passing headers, save expensive
interactions with JS values and memory allocations.

PR-URL: https://github.com/nodejs/node/pull/14723
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Anna Henningsen 2017-08-10 00:52:35 +02:00
parent b646a3df29
commit 348dd66337
No known key found for this signature in database
GPG Key ID: D8B9F5AEAE84E4CF
5 changed files with 147 additions and 81 deletions

View File

@ -0,0 +1,47 @@
'use strict';
const common = require('../common.js');
const PORT = common.PORT;
var bench = common.createBenchmark(main, {
n: [1e3],
nheaders: [100, 1000],
}, { flags: ['--expose-http2', '--no-warnings'] });
function main(conf) {
const n = +conf.n;
const nheaders = +conf.nheaders;
const http2 = require('http2');
const server = http2.createServer();
const headersObject = { ':path': '/' };
for (var i = 0; i < nheaders; i++) {
headersObject[`foo${i}`] = `some header value ${i}`;
}
server.on('stream', (stream) => {
stream.respond();
stream.end('Hi!');
});
server.listen(PORT, () => {
const client = http2.connect(`http://localhost:${PORT}/`);
function doRequest(remaining) {
const req = client.request(headersObject);
req.end();
req.on('data', () => {});
req.on('end', () => {
if (remaining > 0) {
doRequest(remaining - 1);
} else {
bench.end(n);
server.close();
client.destroy();
}
});
}
bench.start();
doRequest(n);
});
}

View File

@ -375,7 +375,8 @@ function assertValidPseudoHeaderTrailer(key) {
function mapToHeaders(map, function mapToHeaders(map,
assertValuePseudoHeader = assertValidPseudoHeader) { assertValuePseudoHeader = assertValidPseudoHeader) {
const ret = []; let ret = '';
let count = 0;
const keys = Object.keys(map); const keys = Object.keys(map);
const singles = new Set(); const singles = new Set();
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
@ -402,7 +403,8 @@ function mapToHeaders(map,
const err = assertValuePseudoHeader(key); const err = assertValuePseudoHeader(key);
if (err !== undefined) if (err !== undefined)
return err; return err;
ret.unshift([key, String(value)]); ret = `${key}\0${String(value)}\0${ret}`;
count++;
} else { } else {
if (kSingleValueHeaders.has(key)) { if (kSingleValueHeaders.has(key)) {
if (singles.has(key)) if (singles.has(key))
@ -415,16 +417,18 @@ function mapToHeaders(map,
if (isArray) { if (isArray) {
for (var k = 0; k < value.length; k++) { for (var k = 0; k < value.length; k++) {
val = String(value[k]); val = String(value[k]);
ret.push([key, val]); ret += `${key}\0${val}\0`;
} }
count += value.length;
} else { } else {
val = String(value); val = String(value);
ret.push([key, val]); ret += `${key}\0${val}\0`;
count++;
} }
} }
} }
return ret; return [ret, count];
} }
class NghttpError extends Error { class NghttpError extends Error {

View File

@ -9,6 +9,8 @@ using v8::Boolean;
using v8::Context; using v8::Context;
using v8::Function; using v8::Function;
using v8::Integer; using v8::Integer;
using v8::String;
using v8::Uint32;
using v8::Undefined; using v8::Undefined;
namespace http2 { namespace http2 {
@ -1075,6 +1077,69 @@ void Http2Session::Unconsume() {
} }
Headers::Headers(Isolate* isolate,
Local<Context> context,
Local<Array> headers) {
CHECK_EQ(headers->Length(), 2);
Local<Value> header_string = headers->Get(context, 0).ToLocalChecked();
Local<Value> header_count = headers->Get(context, 1).ToLocalChecked();
CHECK(header_string->IsString());
CHECK(header_count->IsUint32());
count_ = header_count.As<Uint32>()->Value();
int header_string_len = header_string.As<String>()->Length();
if (count_ == 0) {
CHECK_EQ(header_string_len, 0);
return;
}
// Allocate a single buffer with count_ nghttp2_nv structs, followed
// by the raw header data as passed from JS. This looks like:
// | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents |
buf_.AllocateSufficientStorage((alignof(nghttp2_nv) - 1) +
count_ * sizeof(nghttp2_nv) +
header_string_len);
// Make sure the start address is aligned appropriately for an nghttp2_nv*.
char* start = reinterpret_cast<char*>(
ROUND_UP(reinterpret_cast<uintptr_t>(*buf_), alignof(nghttp2_nv)));
char* header_contents = start + (count_ * sizeof(nghttp2_nv));
nghttp2_nv* const nva = reinterpret_cast<nghttp2_nv*>(start);
CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length());
CHECK_EQ(header_string.As<String>()
->WriteOneByte(reinterpret_cast<uint8_t*>(header_contents),
0, header_string_len,
String::NO_NULL_TERMINATION),
header_string_len);
size_t n = 0;
char* p;
for (p = header_contents; p < header_contents + header_string_len; n++) {
if (n >= count_) {
// This can happen if a passed header contained a null byte. In that
// case, just provide nghttp2 with an invalid header to make it reject
// the headers list.
static uint8_t zero = '\0';
nva[0].name = nva[0].value = &zero;
nva[0].namelen = nva[0].valuelen = 1;
count_ = 1;
return;
}
nva[n].flags = NGHTTP2_NV_FLAG_NONE;
nva[n].name = reinterpret_cast<uint8_t*>(p);
nva[n].namelen = strlen(p);
p += nva[n].namelen + 1;
nva[n].value = reinterpret_cast<uint8_t*>(p);
nva[n].valuelen = strlen(p);
p += nva[n].valuelen + 1;
}
CHECK_EQ(p, header_contents + header_string_len);
CHECK_EQ(n, count_);
}
void Initialize(Local<Object> target, void Initialize(Local<Object> target,
Local<Value> unused, Local<Value> unused,
Local<Context> context, Local<Context> context,

View File

@ -515,54 +515,20 @@ class ExternalHeader :
class Headers { class Headers {
public: public:
Headers(Isolate* isolate, Local<Context> context, Local<Array> headers) { Headers(Isolate* isolate, Local<Context> context, Local<Array> headers);
headers_.AllocateSufficientStorage(headers->Length()); ~Headers() {}
Local<Value> item;
Local<Array> header;
for (size_t n = 0; n < headers->Length(); n++) {
item = headers->Get(context, n).ToLocalChecked();
CHECK(item->IsArray());
header = item.As<Array>();
Local<Value> key = header->Get(context, 0).ToLocalChecked();
Local<Value> value = header->Get(context, 1).ToLocalChecked();
CHECK(key->IsString());
CHECK(value->IsString());
size_t keylen = StringBytes::StorageSize(isolate, key, ASCII);
size_t valuelen = StringBytes::StorageSize(isolate, value, ASCII);
headers_[n].flags = NGHTTP2_NV_FLAG_NONE;
Local<Value> flag = header->Get(context, 2).ToLocalChecked();
if (flag->BooleanValue(context).ToChecked())
headers_[n].flags |= NGHTTP2_NV_FLAG_NO_INDEX;
uint8_t* buf = Malloc<uint8_t>(keylen + valuelen);
headers_[n].name = buf;
headers_[n].value = buf + keylen;
headers_[n].namelen =
StringBytes::Write(isolate,
reinterpret_cast<char*>(headers_[n].name),
keylen, key, ASCII);
headers_[n].valuelen =
StringBytes::Write(isolate,
reinterpret_cast<char*>(headers_[n].value),
valuelen, value, ASCII);
}
}
~Headers() {
for (size_t n = 0; n < headers_.length(); n++)
free(headers_[n].name);
}
nghttp2_nv* operator*() { nghttp2_nv* operator*() {
return *headers_; return reinterpret_cast<nghttp2_nv*>(*buf_);
} }
size_t length() const { size_t length() const {
return headers_.length(); return count_;
} }
private: private:
MaybeStackBuffer<nghttp2_nv> headers_; size_t count_;
MaybeStackBuffer<char, 3000> buf_;
}; };
} // namespace http2 } // namespace http2

View File

@ -82,16 +82,11 @@ const {
'BAR': [1] 'BAR': [1]
}; };
assert.deepStrictEqual(mapToHeaders(headers), [ assert.deepStrictEqual(
[ ':path', 'abc' ], mapToHeaders(headers),
[ ':status', '200' ], [ [ ':path', 'abc', ':status', '200', 'abc', '1', 'xyz', '1', 'xyz', '2',
[ 'abc', '1' ], 'xyz', '3', 'xyz', '4', 'bar', '1', '' ].join('\0'), 8 ]
[ 'xyz', '1' ], );
[ 'xyz', '2' ],
[ 'xyz', '3' ],
[ 'xyz', '4' ],
[ 'bar', '1' ]
]);
} }
{ {
@ -103,15 +98,11 @@ const {
'xyz': [1, 2, 3, 4] 'xyz': [1, 2, 3, 4]
}; };
assert.deepStrictEqual(mapToHeaders(headers), [ assert.deepStrictEqual(
[ ':status', '200' ], mapToHeaders(headers),
[ ':path', 'abc' ], [ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2',
[ 'abc', '1' ], 'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ]
[ 'xyz', '1' ], );
[ 'xyz', '2' ],
[ 'xyz', '3' ],
[ 'xyz', '4' ]
]);
} }
{ {
@ -124,15 +115,11 @@ const {
[Symbol('test')]: 1 // Symbol keys are ignored [Symbol('test')]: 1 // Symbol keys are ignored
}; };
assert.deepStrictEqual(mapToHeaders(headers), [ assert.deepStrictEqual(
[ ':status', '200' ], mapToHeaders(headers),
[ ':path', 'abc' ], [ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2',
[ 'abc', '1' ], 'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ]
[ 'xyz', '1' ], );
[ 'xyz', '2' ],
[ 'xyz', '3' ],
[ 'xyz', '4' ]
]);
} }
{ {
@ -144,14 +131,11 @@ const {
headers.foo = []; headers.foo = [];
headers[':status'] = 200; headers[':status'] = 200;
assert.deepStrictEqual(mapToHeaders(headers), [ assert.deepStrictEqual(
[ ':status', '200' ], mapToHeaders(headers),
[ ':path', 'abc' ], [ [ ':status', '200', ':path', 'abc', 'xyz', '1', 'xyz', '2', 'xyz', '3',
[ 'xyz', '1' ], 'xyz', '4', '' ].join('\0'), 6 ]
[ 'xyz', '2' ], );
[ 'xyz', '3' ],
[ 'xyz', '4' ]
]);
} }
// The following are not allowed to have multiple values // The following are not allowed to have multiple values