-
-
Notifications
You must be signed in to change notification settings - Fork 34.4k
http: implement slab allocation for HTTP header parsing #61375
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
base: main
Are you sure you want to change the base?
Changes from all commits
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,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); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -122,72 +122,116 @@ 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<char, kSlabSize> 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. | ||
|
Member
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'm wondering if this was inteded to refer to something broader than what this PR does; I assume genuinely efficient allocation would involve allocating slab memory for a single HTTP message instead of for each
Member
Author
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. right! I tried both ideas, using MaybeStackBuffer and a single shared 8KB allocator for all strings: 56393d3 benchmark results show +30-46% improvement on fragmented input, with no regressions on normal cases. |
||
| 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<String> ToString(Environment* env) const { | ||
| if (size_ != 0) | ||
| return OneByteString(env->isolate(), str_, size_); | ||
| else | ||
| return String::Empty(env->isolate()); | ||
| } | ||
|
|
||
|
|
||
| // Strip trailing OWS (SPC or HTAB) from string. | ||
| Local<String> 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_; | ||
|
|
||
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.
The memory calculation is slightly inaccurate. 66 × 128 = 8,448 bytes, which is approximately 8.25KB, not 8KB. Consider updating to '~8.4KB' or '~8.5KB' for better accuracy.
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.
Thank you overly pedantic AI code review. The ~8KB is accurate enough here.
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.
Yes, I agree.