Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
8 changes: 4 additions & 4 deletions packages/bun-uws/src/AsyncSocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,8 @@ struct AsyncSocket {

/* Returns the user space backpressure. */
size_t getBufferedAmount() {
/* We return the actual amount of bytes in backbuffer, including pendingRemoval */
return getAsyncSocketData()->buffer.totalLength();
/* Unsent bytes only; already-written bytes waiting for compaction are not backpressure. */
return getAsyncSocketData()->buffer.length();
}
Comment thread
robobun marked this conversation as resolved.

/* Returns the text representation of an IPv4 or IPv6 address */
Expand Down Expand Up @@ -253,7 +253,7 @@ struct AsyncSocket {
/* Check if we couldn't write the entire buffer */
if ((unsigned int) written < buffer_len) {
/* Remove the successfully written data from the buffer */
asyncSocketData->buffer.erase((unsigned int) written);
asyncSocketData->buffer.erase((size_t) written);

/* If we wrote less than we attempted, the socket buffer is likely full
* likely is used as an optimization hint to the compiler
Expand Down Expand Up @@ -301,7 +301,7 @@ struct AsyncSocket {
/* On failure return, otherwise continue down the function */
if ((unsigned int) written < buffer_len) {
/* Update buffering (todo: we can do better here if we keep track of what happens to this guy later on) */
asyncSocketData->buffer.erase((unsigned int) written);
asyncSocketData->buffer.erase((size_t) written);

if (optionally) {
/* Thankfully we can exit early here */
Expand Down
10 changes: 5 additions & 5 deletions packages/bun-uws/src/AsyncSocketData.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ namespace uWS {

struct BackPressure {
std::string buffer;
unsigned int pendingRemoval = 0;
size_t pendingRemoval = 0;
BackPressure(BackPressure &&other) {
buffer = std::move(other.buffer);
pendingRemoval = other.pendingRemoval;
Expand All @@ -34,12 +34,12 @@ struct BackPressure {
void append(const char *data, size_t length) {
buffer.append(data, length);
}
void erase(unsigned int length) {
void erase(size_t length) {
pendingRemoval += length;
/* Always erase a minimum of 1/32th the current backpressure */
if (pendingRemoval > (buffer.length() >> 5)) {
/* Compact once half the buffer is dead space so draining n bytes moves
* O(n) total (geometric series). clear() releases capacity on full drain. */
if (pendingRemoval > (buffer.length() >> 1)) {
buffer.erase(0, pendingRemoval);
buffer.shrink_to_fit();
pendingRemoval = 0;
}
}
Expand Down
8 changes: 8 additions & 0 deletions packages/bun-uws/src/HttpResponse.h
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,14 @@

size_t length = data.length();

/* A write this large is split into several AsyncSocket::write calls below
* (chunk framing, INT_MAX slicing). Reserve once so those appends never
* realloc + copy a multi-GB backpressure buffer. */
if (length > 1024 * 1024) {
auto &bp = Super::getAsyncSocketData()->buffer;
bp.reserve(bp.length() + length + 32);
}

Check warning on line 605 in packages/bun-uws/src/HttpResponse.h

View check run for this annotation

Claude / Claude Code Review

Repeated >1MB writes still O(n²) on libc++ via exact-fit reserve

The remaining `length > 1024 * 1024` gate still fires on *every* user-level write >1MB, so on libc++ (macOS) N successive 2MB writes to a stalled socket each `reserve()` exact-fit and realloc+copy the accumulated buffer → O(N²), whereas pre-PR `append()` grew geometrically. Gating on `length > INT_MAX` (the only case a single write is actually split into multiple appends), or reserving `max(2*capacity, needed)`, keeps the 4 GiB fix without this. Narrow: libc++ only, requires repeated >1MB writes
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.

// Special handling for extremely large data (greater than UINT_MAX bytes)
// most clients expect a max of UINT_MAX, so we need to split the write into multiple writes
if (length > UINT_MAX) {
Expand Down
3 changes: 2 additions & 1 deletion packages/bun-uws/src/WebSocket.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ struct WebSocket : AsyncSocket<SSL> {
}

size_t memoryCost() {
return getBufferedAmount() + sizeof(WebSocket);
/* Allocation footprint for reportExtraMemoryAllocated, not unsent bytes. */
return Super::getAsyncSocketData()->buffer.totalLength() + sizeof(WebSocket);
}

/* Sending fragmented messages puts a bit of effort on the user; you must not interleave regular sends
Expand Down
7 changes: 2 additions & 5 deletions test/js/node/http/node-http-backpressure-max.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import http from "node:http";
import type { AddressInfo } from "node:net";

describe("backpressure", () => {
// Linux CI only have 8GB with is not enought because we will clone all or most of this 4GB into memory
// Linux CI only has 8GB which is not enough because we clone all or most of this 4GB into memory.
it.skipIf(isCI && isLinux)(
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"should handle backpressure with the maximum allowed bytes",
async () => {
Expand Down Expand Up @@ -45,9 +45,6 @@ describe("backpressure", () => {

expect(totalBytes).toBe(payloadSize);
},
// Moving 4 GiB through the server and the fetch reader takes ~60s on the
// slowest CI runners (darwin x64), which sat exactly at the old 60s
// limit and made the test flaky there.
120_000,
30_000,
);
});
Loading