-
Notifications
You must be signed in to change notification settings - Fork 30.1k
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
http2: improve perf of passing headers to C++ #14723
Changes from all commits
f6d3a00
327da38
d02a8a7
a9d9f6d
eea3825
b7080cf
95964a1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -375,7 +375,8 @@ function assertValidPseudoHeaderTrailer(key) { | |
|
||
function mapToHeaders(map, | ||
assertValuePseudoHeader = assertValidPseudoHeader) { | ||
const ret = []; | ||
let ret = ''; | ||
let count = 0; | ||
const keys = Object.keys(map); | ||
const singles = new Set(); | ||
for (var i = 0; i < keys.length; i++) { | ||
|
@@ -402,7 +403,8 @@ function mapToHeaders(map, | |
const err = assertValuePseudoHeader(key); | ||
if (err !== undefined) | ||
return err; | ||
ret.unshift([key, String(value)]); | ||
ret = `${key}\0${String(value)}\0${ret}`; | ||
count++; | ||
} else { | ||
if (kSingleValueHeaders.has(key)) { | ||
if (singles.has(key)) | ||
|
@@ -415,16 +417,18 @@ function mapToHeaders(map, | |
if (isArray) { | ||
for (var k = 0; k < value.length; k++) { | ||
val = String(value[k]); | ||
ret.push([key, val]); | ||
ret += `${key}\0${val}\0`; | ||
} | ||
count += value.length; | ||
} else { | ||
val = String(value); | ||
ret.push([key, val]); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A big part of the reason this was an array is because there will be a third value that needs to be passed in at some point... specifically, a flag that indicates whether or not the header pair should be stored in the header compression table There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I suppose we can add another NUL-delimited column if we were to implement that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We could even just add a single character/byte per header to the string. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. single byte (bit-flag) per header should work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact, why don't we go ahead and add that extra byte now as a reserved field in the structure. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One idea for the API... right now headers are set as an object.. e.g. {
':status': 200,
'content-type': 'text/plain',
'foo': 'bar'
} We could (not saying should) allow the value to be wrapped somehow... e.g. {
':status': 200,
'content-type': http2.flaggedHeader('text/plain', { store: false }),
'foo': 'bar'
} This feels a bit awkward but it optimizes for the common case and API familiarity and only adds the weirdness when someone really does want to use the more advanced feature. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or, alternatively... (just brainstorming here ;-) ...) {
':status': 200,
'foo': 'bar',
[http2.noStore]: {
'content-type': 'text/plain'
}
} Where The other option is to change the headers API entirely and use a const headers = new http2.Headers();
headers.set(':status', 200);
headers.set('content-type', 'text/plain', { store: false });
headers.set('foo', 'bar'); This has it's whole own set of issues, including being slower than using an object literal. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jasnell I do like the second approach, for the reason you mentioned. :) Do you think we might ever want to add more options than There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jasnell Prototyped this out at addaleax/node@bb2fb6c, but I’d say it’s different enough to leave it for another PR. |
||
ret += `${key}\0${val}\0`; | ||
count++; | ||
} | ||
} | ||
} | ||
|
||
return ret; | ||
return [ret, count]; | ||
} | ||
|
||
class NghttpError extends Error { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,6 +9,8 @@ using v8::Boolean; | |
using v8::Context; | ||
using v8::Function; | ||
using v8::Integer; | ||
using v8::String; | ||
using v8::Uint32; | ||
using v8::Undefined; | ||
|
||
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)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parens aren't necessary here. (They don't hurt either, of course.) |
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to have a test case against this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There already is :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather inserting a fake header, can we simply abort and set an There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @jasnell Yes, we could do that, but is that really a case that we want to optimize for? Plus, it would create inconsistency with how we handle other invalid headers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nah, you're right, this works. |
||
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, | ||
Local<Value> unused, | ||
Local<Context> context, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This presupposes that neither the key nor the val contain null characters. That may not be a safe assumption to make
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jasnell #14723 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, saw that as I read further :-)