diff --git a/benchmark/http/bench-parser-fragmented.js b/benchmark/http/bench-parser-fragmented.js new file mode 100644 index 00000000000000..6dd8f332a7ffad --- /dev/null +++ b/benchmark/http/bench-parser-fragmented.js @@ -0,0 +1,67 @@ +'use strict'; + +const common = require('../common'); + +const bench = common.createBenchmark(main, { + len: [8, 16], + frags: [2, 4, 8], + n: [1e5], +}, { + flags: ['--expose-internals', '--no-warnings'], +}); + +function main({ len, frags, n }) { + const { HTTPParser } = common.binding('http_parser'); + const REQUEST = HTTPParser.REQUEST; + const kOnHeaders = HTTPParser.kOnHeaders | 0; + const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; + const kOnBody = HTTPParser.kOnBody | 0; + const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0; + + function processHeaderFragmented(fragments, n) { + const parser = newParser(REQUEST); + + bench.start(); + for (let i = 0; i < n; i++) { + // Send header in fragments + for (const frag of fragments) { + parser.execute(frag, 0, frag.length); + } + parser.initialize(REQUEST, {}); + } + bench.end(n); + } + + function newParser(type) { + const parser = new HTTPParser(); + parser.initialize(type, {}); + + parser.headers = []; + + parser[kOnHeaders] = function() { }; + parser[kOnHeadersComplete] = function() { }; + parser[kOnBody] = function() { }; + parser[kOnMessageComplete] = function() { }; + + return parser; + } + + // Build the header + let header = `GET /hello HTTP/1.1\r\nContent-Type: text/plain\r\n`; + + for (let i = 0; i < len; i++) { + header += `X-Filler${i}: ${Math.random().toString(36).substring(2)}\r\n`; + } + header += '\r\n'; + + // Split header into fragments + const headerBuf = Buffer.from(header); + const fragSize = Math.ceil(headerBuf.length / frags); + const fragments = []; + + for (let i = 0; i < headerBuf.length; i += fragSize) { + fragments.push(headerBuf.slice(i, Math.min(i + fragSize, headerBuf.length))); + } + + processHeaderFragmented(fragments, n); +} diff --git a/src/node_http_parser.cc b/src/node_http_parser.cc index 26ddbf57854672..c1682907e4a3fe 100644 --- a/src/node_http_parser.cc +++ b/src/node_http_parser.cc @@ -122,64 +122,109 @@ class BindingData : public BaseObject { SET_MEMORY_INFO_NAME(BindingData) }; -// helper class for the Parser -struct StringPtr { - StringPtr() { - on_heap_ = false; - Reset(); - } +class Parser; + +class StringPtrAllocator { + public: + // Memory impact: ~8KB per parser (66 StringPtr × 128 bytes). + static constexpr size_t kSlabSize = 8192; + + StringPtrAllocator() { buffer_.SetLength(0); } + // Allocate memory from the slab. Returns nullptr if full. + char* Allocate(size_t size) { + const size_t current = buffer_.length(); + if (current + size > kSlabSize) { + return nullptr; + } + buffer_.SetLength(current + size); + return buffer_.out() + current; + } - ~StringPtr() { - Reset(); + // Check if pointer is within this allocator's buffer. + bool Contains(const char* ptr) const { + return ptr >= buffer_.out() && ptr < buffer_.out() + buffer_.capacity(); } + // Reset allocator for new message. + void Reset() { buffer_.SetLength(0); } + private: + MaybeStackBuffer buffer_; +}; + +struct StringPtr { + StringPtr() = default; + ~StringPtr() { Reset(); } - // If str_ does not point to a heap string yet, this function makes it do + StringPtr(const StringPtr&) = delete; + StringPtr& operator=(const StringPtr&) = delete; + + void SetAllocator(StringPtrAllocator* allocator) { allocator_ = allocator; } + + // If str_ does not point to owned storage yet, this function makes it do // so. This is called at the end of each http_parser_execute() so as not // to leak references. See issue #2438 and test-http-parser-bad-ref.js. void Save() { - if (!on_heap_ && size_ > 0) { - char* s = new char[size_]; - memcpy(s, str_, size_); - str_ = s; - on_heap_ = true; + if (str_ == nullptr || on_heap_ || + (allocator_ != nullptr && allocator_->Contains(str_))) { + return; + } + // Try allocator first, fall back to heap + if (allocator_ != nullptr) { + char* ptr = allocator_->Allocate(size_); + if (ptr != nullptr) { + memcpy(ptr, str_, size_); + str_ = ptr; + return; + } } + char* s = new char[size_]; + memcpy(s, str_, size_); + str_ = s; + on_heap_ = true; } - void Reset() { if (on_heap_) { delete[] str_; on_heap_ = false; } - str_ = nullptr; size_ = 0; } - void Update(const char* str, size_t size) { if (str_ == nullptr) { str_ = str; - } else if (on_heap_ || str_ + size_ != str) { - // Non-consecutive input, make a copy on the heap. - // TODO(bnoordhuis) Use slab allocation, O(n) allocs is bad. - char* s = new char[size_ + size]; - memcpy(s, str_, size_); - memcpy(s + size_, str, size); - - if (on_heap_) - delete[] str_; - else - on_heap_ = true; + } else if (on_heap_ || + (allocator_ != nullptr && allocator_->Contains(str_)) || + str_ + size_ != str) { + // Non-consecutive input, make a copy + const size_t new_size = size_ + size; + char* new_str = nullptr; + + // Try allocator first (if not already on heap) + if (!on_heap_ && allocator_ != nullptr) { + new_str = allocator_->Allocate(new_size); + } - str_ = s; + if (new_str != nullptr) { + memcpy(new_str, str_, size_); + memcpy(new_str + size_, str, size); + str_ = new_str; + } else { + // Fall back to heap + char* s = new char[new_size]; + memcpy(s, str_, size_); + memcpy(s + size_, str, size); + if (on_heap_) delete[] str_; + str_ = s; + on_heap_ = true; + } } size_ += size; } - Local ToString(Environment* env) const { if (size_ != 0) return OneByteString(env->isolate(), str_, size_); @@ -187,7 +232,6 @@ struct StringPtr { return String::Empty(env->isolate()); } - // Strip trailing OWS (SPC or HTAB) from string. Local ToTrimmedString(Environment* env) { while (size_ > 0 && IsOWS(str_[size_ - 1])) { @@ -196,14 +240,12 @@ struct StringPtr { return ToString(env); } - - const char* str_; - bool on_heap_; - size_t size_; + const char* str_ = nullptr; + bool on_heap_ = false; + size_t size_ = 0; + StringPtrAllocator* allocator_ = nullptr; }; -class Parser; - struct ParserComparator { bool operator()(const Parser* lhs, const Parser* rhs) const; }; @@ -260,6 +302,13 @@ class Parser : public AsyncWrap, public StreamListener { current_buffer_len_(0), current_buffer_data_(nullptr), binding_data_(binding_data) { + // Wire up all StringPtrs to use the shared allocator + for (size_t i = 0; i < kMaxHeaderFieldsCount; i++) { + fields_[i].SetAllocator(&allocator_); + values_[i].SetAllocator(&allocator_); + } + url_.SetAllocator(&allocator_); + status_message_.SetAllocator(&allocator_); } SET_NO_MEMORY_INFO() @@ -278,6 +327,7 @@ class Parser : public AsyncWrap, public StreamListener { headers_completed_ = false; chunk_extensions_nread_ = 0; last_message_start_ = uv_hrtime(); + allocator_.Reset(); url_.Reset(); status_message_.Reset(); @@ -1006,6 +1056,7 @@ class Parser : public AsyncWrap, public StreamListener { llhttp_t parser_; + StringPtrAllocator allocator_; // shared slab for all StringPtrs StringPtr fields_[kMaxHeaderFieldsCount]; // header fields StringPtr values_[kMaxHeaderFieldsCount]; // header values StringPtr url_;