Skip to content

Commit 781531a

Browse files
committed
http: cache parser callbacks
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. No observable behavior change; all header/parser tests pass. On a keep-alive server this is a small but consistent CPU reduction. Signed-off-by: Matteo Collina <hello@matteocollina.com>
1 parent 798bb16 commit 781531a

1 file changed

Lines changed: 30 additions & 7 deletions

File tree

src/node_http_parser.cc

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -441,9 +441,8 @@ class Parser : public AsyncWrap, public StreamListener {
441441
};
442442

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

448447
if (!cb->IsFunction())
449448
return 0;
@@ -521,7 +520,7 @@ class Parser : public AsyncWrap, public StreamListener {
521520
Environment* env = this->env();
522521
HandleScope handle_scope(env->isolate());
523522

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

526525
if (!cb->IsFunction())
527526
return 0;
@@ -554,9 +553,8 @@ class Parser : public AsyncWrap, public StreamListener {
554553

555554
header_pairs_ = 0;
556555

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

561559
if (!cb->IsFunction())
562560
return 0;
@@ -940,6 +938,9 @@ class Parser : public AsyncWrap, public StreamListener {
940938
Local<Value> headers_v[kMaxHeaderFieldsCount * 2];
941939

942940
for (size_t i = 0; i < num_values_; ++i) {
941+
// Field names are not internalized: header names are attacker
942+
// controlled, so a flood of unique names would grow V8's string table
943+
// and pay the interning cost on every request with no dedup benefit.
943944
headers_v[i * 2] = fields_[i].ToString(env());
944945
headers_v[i * 2 + 1] = values_[i].ToTrimmedString(env());
945946
}
@@ -975,10 +976,28 @@ class Parser : public AsyncWrap, public StreamListener {
975976
}
976977

977978

979+
// Caches a set-once callback property; the cache is cleared in Init().
980+
Local<Value> CachedCallback(uint32_t index, v8::Global<v8::Value>* cache) {
981+
Isolate* isolate = env()->isolate();
982+
if (!cache->IsEmpty()) {
983+
return cache->Get(isolate);
984+
}
985+
Local<Value> cb =
986+
object()->Get(env()->context(), index).ToLocalChecked();
987+
if (cb->IsFunction()) {
988+
cache->Reset(isolate, cb);
989+
}
990+
return cb;
991+
}
992+
978993
void Init(llhttp_type_t type, uint64_t max_http_header_size,
979994
uint32_t lenient_flags) {
980995
llhttp_init(&parser_, type, &settings);
981996

997+
on_headers_complete_cb_.Reset();
998+
on_body_cb_.Reset();
999+
on_message_complete_cb_.Reset();
1000+
9821001
if (lenient_flags & kLenientHeaders) {
9831002
llhttp_set_lenient_headers(&parser_, 1);
9841003
}
@@ -1102,6 +1121,10 @@ class Parser : public AsyncWrap, public StreamListener {
11021121
size_t header_pairs_ = 0;
11031122
double max_header_pairs_ = -1;
11041123
bool pending_pause_ = false;
1124+
1125+
v8::Global<v8::Value> on_headers_complete_cb_;
1126+
v8::Global<v8::Value> on_body_cb_;
1127+
v8::Global<v8::Value> on_message_complete_cb_;
11051128
bool received_data_ = false;
11061129
uint64_t header_nread_ = 0;
11071130
uint64_t chunk_extensions_nread_ = 0;

0 commit comments

Comments
 (0)