Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions benchmark/http/bench-parser-fragmented.js
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);
}
125 changes: 88 additions & 37 deletions src/node_http_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Copy link

Copilot AI Jan 13, 2026

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.

Suggested change
// Memory impact: ~8KB per parser (66 StringPtr × 128 bytes).
// Memory impact: ~8.4KB per parser (66 StringPtr × 128 bytes).

Copilot uses AI. Check for mistakes.
Copy link
Member

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.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree.

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.
Copy link
Member

Choose a reason for hiding this comment

The 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 StringPtr

Copy link
Member Author

Choose a reason for hiding this comment

The 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])) {
Expand All @@ -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;
};
Expand Down Expand Up @@ -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()
Expand All @@ -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();

Expand Down Expand Up @@ -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_;
Expand Down
Loading