Skip to content

Commit 348dd66

Browse files
committed
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: #14723 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent b646a3d commit 348dd66

File tree

5 files changed

+147
-81
lines changed

5 files changed

+147
-81
lines changed

benchmark/http2/headers.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const PORT = common.PORT;
5+
6+
var bench = common.createBenchmark(main, {
7+
n: [1e3],
8+
nheaders: [100, 1000],
9+
}, { flags: ['--expose-http2', '--no-warnings'] });
10+
11+
function main(conf) {
12+
const n = +conf.n;
13+
const nheaders = +conf.nheaders;
14+
const http2 = require('http2');
15+
const server = http2.createServer();
16+
17+
const headersObject = { ':path': '/' };
18+
for (var i = 0; i < nheaders; i++) {
19+
headersObject[`foo${i}`] = `some header value ${i}`;
20+
}
21+
22+
server.on('stream', (stream) => {
23+
stream.respond();
24+
stream.end('Hi!');
25+
});
26+
server.listen(PORT, () => {
27+
const client = http2.connect(`http://localhost:${PORT}/`);
28+
29+
function doRequest(remaining) {
30+
const req = client.request(headersObject);
31+
req.end();
32+
req.on('data', () => {});
33+
req.on('end', () => {
34+
if (remaining > 0) {
35+
doRequest(remaining - 1);
36+
} else {
37+
bench.end(n);
38+
server.close();
39+
client.destroy();
40+
}
41+
});
42+
}
43+
44+
bench.start();
45+
doRequest(n);
46+
});
47+
}

lib/internal/http2/util.js

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,8 @@ function assertValidPseudoHeaderTrailer(key) {
375375

376376
function mapToHeaders(map,
377377
assertValuePseudoHeader = assertValidPseudoHeader) {
378-
const ret = [];
378+
let ret = '';
379+
let count = 0;
379380
const keys = Object.keys(map);
380381
const singles = new Set();
381382
for (var i = 0; i < keys.length; i++) {
@@ -402,7 +403,8 @@ function mapToHeaders(map,
402403
const err = assertValuePseudoHeader(key);
403404
if (err !== undefined)
404405
return err;
405-
ret.unshift([key, String(value)]);
406+
ret = `${key}\0${String(value)}\0${ret}`;
407+
count++;
406408
} else {
407409
if (kSingleValueHeaders.has(key)) {
408410
if (singles.has(key))
@@ -415,16 +417,18 @@ function mapToHeaders(map,
415417
if (isArray) {
416418
for (var k = 0; k < value.length; k++) {
417419
val = String(value[k]);
418-
ret.push([key, val]);
420+
ret += `${key}\0${val}\0`;
419421
}
422+
count += value.length;
420423
} else {
421424
val = String(value);
422-
ret.push([key, val]);
425+
ret += `${key}\0${val}\0`;
426+
count++;
423427
}
424428
}
425429
}
426430

427-
return ret;
431+
return [ret, count];
428432
}
429433

430434
class NghttpError extends Error {

src/node_http2.cc

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ using v8::Boolean;
99
using v8::Context;
1010
using v8::Function;
1111
using v8::Integer;
12+
using v8::String;
13+
using v8::Uint32;
1214
using v8::Undefined;
1315

1416
namespace http2 {
@@ -1075,6 +1077,69 @@ void Http2Session::Unconsume() {
10751077
}
10761078

10771079

1080+
Headers::Headers(Isolate* isolate,
1081+
Local<Context> context,
1082+
Local<Array> headers) {
1083+
CHECK_EQ(headers->Length(), 2);
1084+
Local<Value> header_string = headers->Get(context, 0).ToLocalChecked();
1085+
Local<Value> header_count = headers->Get(context, 1).ToLocalChecked();
1086+
CHECK(header_string->IsString());
1087+
CHECK(header_count->IsUint32());
1088+
count_ = header_count.As<Uint32>()->Value();
1089+
int header_string_len = header_string.As<String>()->Length();
1090+
1091+
if (count_ == 0) {
1092+
CHECK_EQ(header_string_len, 0);
1093+
return;
1094+
}
1095+
1096+
// Allocate a single buffer with count_ nghttp2_nv structs, followed
1097+
// by the raw header data as passed from JS. This looks like:
1098+
// | possible padding | nghttp2_nv | nghttp2_nv | ... | header contents |
1099+
buf_.AllocateSufficientStorage((alignof(nghttp2_nv) - 1) +
1100+
count_ * sizeof(nghttp2_nv) +
1101+
header_string_len);
1102+
// Make sure the start address is aligned appropriately for an nghttp2_nv*.
1103+
char* start = reinterpret_cast<char*>(
1104+
ROUND_UP(reinterpret_cast<uintptr_t>(*buf_), alignof(nghttp2_nv)));
1105+
char* header_contents = start + (count_ * sizeof(nghttp2_nv));
1106+
nghttp2_nv* const nva = reinterpret_cast<nghttp2_nv*>(start);
1107+
1108+
CHECK_LE(header_contents + header_string_len, *buf_ + buf_.length());
1109+
CHECK_EQ(header_string.As<String>()
1110+
->WriteOneByte(reinterpret_cast<uint8_t*>(header_contents),
1111+
0, header_string_len,
1112+
String::NO_NULL_TERMINATION),
1113+
header_string_len);
1114+
1115+
size_t n = 0;
1116+
char* p;
1117+
for (p = header_contents; p < header_contents + header_string_len; n++) {
1118+
if (n >= count_) {
1119+
// This can happen if a passed header contained a null byte. In that
1120+
// case, just provide nghttp2 with an invalid header to make it reject
1121+
// the headers list.
1122+
static uint8_t zero = '\0';
1123+
nva[0].name = nva[0].value = &zero;
1124+
nva[0].namelen = nva[0].valuelen = 1;
1125+
count_ = 1;
1126+
return;
1127+
}
1128+
1129+
nva[n].flags = NGHTTP2_NV_FLAG_NONE;
1130+
nva[n].name = reinterpret_cast<uint8_t*>(p);
1131+
nva[n].namelen = strlen(p);
1132+
p += nva[n].namelen + 1;
1133+
nva[n].value = reinterpret_cast<uint8_t*>(p);
1134+
nva[n].valuelen = strlen(p);
1135+
p += nva[n].valuelen + 1;
1136+
}
1137+
1138+
CHECK_EQ(p, header_contents + header_string_len);
1139+
CHECK_EQ(n, count_);
1140+
}
1141+
1142+
10781143
void Initialize(Local<Object> target,
10791144
Local<Value> unused,
10801145
Local<Context> context,

src/node_http2.h

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -515,54 +515,20 @@ class ExternalHeader :
515515

516516
class Headers {
517517
public:
518-
Headers(Isolate* isolate, Local<Context> context, Local<Array> headers) {
519-
headers_.AllocateSufficientStorage(headers->Length());
520-
Local<Value> item;
521-
Local<Array> header;
522-
523-
for (size_t n = 0; n < headers->Length(); n++) {
524-
item = headers->Get(context, n).ToLocalChecked();
525-
CHECK(item->IsArray());
526-
header = item.As<Array>();
527-
Local<Value> key = header->Get(context, 0).ToLocalChecked();
528-
Local<Value> value = header->Get(context, 1).ToLocalChecked();
529-
CHECK(key->IsString());
530-
CHECK(value->IsString());
531-
size_t keylen = StringBytes::StorageSize(isolate, key, ASCII);
532-
size_t valuelen = StringBytes::StorageSize(isolate, value, ASCII);
533-
headers_[n].flags = NGHTTP2_NV_FLAG_NONE;
534-
Local<Value> flag = header->Get(context, 2).ToLocalChecked();
535-
if (flag->BooleanValue(context).ToChecked())
536-
headers_[n].flags |= NGHTTP2_NV_FLAG_NO_INDEX;
537-
uint8_t* buf = Malloc<uint8_t>(keylen + valuelen);
538-
headers_[n].name = buf;
539-
headers_[n].value = buf + keylen;
540-
headers_[n].namelen =
541-
StringBytes::Write(isolate,
542-
reinterpret_cast<char*>(headers_[n].name),
543-
keylen, key, ASCII);
544-
headers_[n].valuelen =
545-
StringBytes::Write(isolate,
546-
reinterpret_cast<char*>(headers_[n].value),
547-
valuelen, value, ASCII);
548-
}
549-
}
550-
551-
~Headers() {
552-
for (size_t n = 0; n < headers_.length(); n++)
553-
free(headers_[n].name);
554-
}
518+
Headers(Isolate* isolate, Local<Context> context, Local<Array> headers);
519+
~Headers() {}
555520

556521
nghttp2_nv* operator*() {
557-
return *headers_;
522+
return reinterpret_cast<nghttp2_nv*>(*buf_);
558523
}
559524

560525
size_t length() const {
561-
return headers_.length();
526+
return count_;
562527
}
563528

564529
private:
565-
MaybeStackBuffer<nghttp2_nv> headers_;
530+
size_t count_;
531+
MaybeStackBuffer<char, 3000> buf_;
566532
};
567533

568534
} // namespace http2

test/parallel/test-http2-util-headers-list.js

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,11 @@ const {
8282
'BAR': [1]
8383
};
8484

85-
assert.deepStrictEqual(mapToHeaders(headers), [
86-
[ ':path', 'abc' ],
87-
[ ':status', '200' ],
88-
[ 'abc', '1' ],
89-
[ 'xyz', '1' ],
90-
[ 'xyz', '2' ],
91-
[ 'xyz', '3' ],
92-
[ 'xyz', '4' ],
93-
[ 'bar', '1' ]
94-
]);
85+
assert.deepStrictEqual(
86+
mapToHeaders(headers),
87+
[ [ ':path', 'abc', ':status', '200', 'abc', '1', 'xyz', '1', 'xyz', '2',
88+
'xyz', '3', 'xyz', '4', 'bar', '1', '' ].join('\0'), 8 ]
89+
);
9590
}
9691

9792
{
@@ -103,15 +98,11 @@ const {
10398
'xyz': [1, 2, 3, 4]
10499
};
105100

106-
assert.deepStrictEqual(mapToHeaders(headers), [
107-
[ ':status', '200' ],
108-
[ ':path', 'abc' ],
109-
[ 'abc', '1' ],
110-
[ 'xyz', '1' ],
111-
[ 'xyz', '2' ],
112-
[ 'xyz', '3' ],
113-
[ 'xyz', '4' ]
114-
]);
101+
assert.deepStrictEqual(
102+
mapToHeaders(headers),
103+
[ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2',
104+
'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ]
105+
);
115106
}
116107

117108
{
@@ -124,15 +115,11 @@ const {
124115
[Symbol('test')]: 1 // Symbol keys are ignored
125116
};
126117

127-
assert.deepStrictEqual(mapToHeaders(headers), [
128-
[ ':status', '200' ],
129-
[ ':path', 'abc' ],
130-
[ 'abc', '1' ],
131-
[ 'xyz', '1' ],
132-
[ 'xyz', '2' ],
133-
[ 'xyz', '3' ],
134-
[ 'xyz', '4' ]
135-
]);
118+
assert.deepStrictEqual(
119+
mapToHeaders(headers),
120+
[ [ ':status', '200', ':path', 'abc', 'abc', '1', 'xyz', '1', 'xyz', '2',
121+
'xyz', '3', 'xyz', '4', '' ].join('\0'), 7 ]
122+
);
136123
}
137124

138125
{
@@ -144,14 +131,11 @@ const {
144131
headers.foo = [];
145132
headers[':status'] = 200;
146133

147-
assert.deepStrictEqual(mapToHeaders(headers), [
148-
[ ':status', '200' ],
149-
[ ':path', 'abc' ],
150-
[ 'xyz', '1' ],
151-
[ 'xyz', '2' ],
152-
[ 'xyz', '3' ],
153-
[ 'xyz', '4' ]
154-
]);
134+
assert.deepStrictEqual(
135+
mapToHeaders(headers),
136+
[ [ ':status', '200', ':path', 'abc', 'xyz', '1', 'xyz', '2', 'xyz', '3',
137+
'xyz', '4', '' ].join('\0'), 6 ]
138+
);
155139
}
156140

157141
// The following are not allowed to have multiple values

0 commit comments

Comments
 (0)