Skip to content

Commit d12ac94

Browse files
committed
http: cache parser callbacks, intern header names
Two low-level reductions in the HTTP parser's per-request work: - Cache the kOnHeadersComplete, kOnBody and kOnMessageComplete callback lookups per parser instead of doing an object property Get on every message. These callbacks are assigned once when the parser object is created and never change; the cache is cleared in Init() so re-initialized parsers (direct binding users) stay correct. - Intern header field-name strings when building the header array. Field names repeat heavily across requests on a connection, so interning lets V8's string table return the same string object for each 'Host', 'User-Agent', etc. instead of allocating a new one per request. Header values are unique and stay non-interned. No observable behavior change: header names keep their original case in rawHeaders and all header/parser tests pass. On a keep-alive server this is a small but consistent CPU reduction (~2%), measured on both 1-header and 8-header requests. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 798bb16 commit d12ac94

1 file changed

Lines changed: 48 additions & 8 deletions

File tree

‎src/node_http_parser.cc‎

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,20 @@ struct StringPtr {
233233
return String::Empty(env->isolate());
234234
}
235235

236+
// Like ToString(), but interns the result in V8's string table. Used for
237+
// HTTP header field names, which repeat heavily across requests on a
238+
// connection: interning lets V8 hand back the same string object instead
239+
// of allocating a fresh one for every "Host", "User-Agent", etc.
240+
Local<String> ToInternalizedString(Environment* env) const {
241+
if (size_ != 0)
242+
return OneByteString(env->isolate(),
243+
str_,
244+
static_cast<int>(size_),
245+
v8::NewStringType::kInternalized);
246+
else
247+
return String::Empty(env->isolate());
248+
}
249+
236250
// Strip trailing OWS (SPC or HTAB) from string.
237251
Local<String> ToTrimmedString(Environment* env) {
238252
while (size_ > 0 && IsOWS(str_[size_ - 1])) {
@@ -441,9 +455,8 @@ class Parser : public AsyncWrap, public StreamListener {
441455
};
442456

443457
Local<Value> argv[A_MAX];
444-
Local<Object> obj = object();
445-
Local<Value> cb = obj->Get(env()->context(),
446-
kOnHeadersComplete).ToLocalChecked();
458+
Local<Value> cb =
459+
CachedCallback(kOnHeadersComplete, &on_headers_complete_cb_);
447460

448461
if (!cb->IsFunction())
449462
return 0;
@@ -521,7 +534,7 @@ class Parser : public AsyncWrap, public StreamListener {
521534
Environment* env = this->env();
522535
HandleScope handle_scope(env->isolate());
523536

524-
Local<Value> cb = object()->Get(env->context(), kOnBody).ToLocalChecked();
537+
Local<Value> cb = CachedCallback(kOnBody, &on_body_cb_);
525538

526539
if (!cb->IsFunction())
527540
return 0;
@@ -554,9 +567,8 @@ class Parser : public AsyncWrap, public StreamListener {
554567

555568
header_pairs_ = 0;
556569

557-
Local<Object> obj = object();
558-
Local<Value> cb = obj->Get(env()->context(),
559-
kOnMessageComplete).ToLocalChecked();
570+
Local<Value> cb =
571+
CachedCallback(kOnMessageComplete, &on_message_complete_cb_);
560572

561573
if (!cb->IsFunction())
562574
return 0;
@@ -940,7 +952,7 @@ class Parser : public AsyncWrap, public StreamListener {
940952
Local<Value> headers_v[kMaxHeaderFieldsCount * 2];
941953

942954
for (size_t i = 0; i < num_values_; ++i) {
943-
headers_v[i * 2] = fields_[i].ToString(env());
955+
headers_v[i * 2] = fields_[i].ToInternalizedString(env());
944956
headers_v[i * 2 + 1] = values_[i].ToTrimmedString(env());
945957
}
946958

@@ -975,10 +987,33 @@ class Parser : public AsyncWrap, public StreamListener {
975987
}
976988

977989

990+
// Fetches a JS callback property from the parser object, caching it so the
991+
// per-message object()->Get() lookup is only paid once. The message
992+
// callbacks (kOnHeadersComplete, kOnBody, kOnMessageComplete) are assigned
993+
// once when the parser object is created and never change afterwards. The
994+
// cache is cleared in Init() so it stays correct if the same parser object
995+
// is re-initialized with different callbacks (direct users of the binding).
996+
Local<Value> CachedCallback(uint32_t index, v8::Global<v8::Value>* cache) {
997+
Isolate* isolate = env()->isolate();
998+
if (!cache->IsEmpty()) {
999+
return cache->Get(isolate);
1000+
}
1001+
Local<Value> cb =
1002+
object()->Get(env()->context(), index).ToLocalChecked();
1003+
if (cb->IsFunction()) {
1004+
cache->Reset(isolate, cb);
1005+
}
1006+
return cb;
1007+
}
1008+
9781009
void Init(llhttp_type_t type, uint64_t max_http_header_size,
9791010
uint32_t lenient_flags) {
9801011
llhttp_init(&parser_, type, &settings);
9811012

1013+
on_headers_complete_cb_.Reset();
1014+
on_body_cb_.Reset();
1015+
on_message_complete_cb_.Reset();
1016+
9821017
if (lenient_flags & kLenientHeaders) {
9831018
llhttp_set_lenient_headers(&parser_, 1);
9841019
}
@@ -1102,6 +1137,11 @@ class Parser : public AsyncWrap, public StreamListener {
11021137
size_t header_pairs_ = 0;
11031138
double max_header_pairs_ = -1;
11041139
bool pending_pause_ = false;
1140+
1141+
// Lazily-cached JS message callbacks; see CachedCallback().
1142+
v8::Global<v8::Value> on_headers_complete_cb_;
1143+
v8::Global<v8::Value> on_body_cb_;
1144+
v8::Global<v8::Value> on_message_complete_cb_;
11051145
bool received_data_ = false;
11061146
uint64_t header_nread_ = 0;
11071147
uint64_t chunk_extensions_nread_ = 0;

0 commit comments

Comments
 (0)