-
Notifications
You must be signed in to change notification settings - Fork 4.7k
http: couple fetch() receive backpressure to JS body consumption (h1/h2/h3) #29831
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 4 commits
8db502f
dc050be
794ee0e
4484653
a566b1b
4803eac
fbb3f10
c28c8c0
6aa5782
4113c53
670012d
7f808f2
08198eb
f35a1b5
66aab38
77a98f2
4b5dcf6
5f2d15d
1c38a8f
8709b87
e55438d
530c155
9b3b4b8
3b6d8f1
71af8d5
c88346f
783f56a
ceea99e
90bd4c9
ac57e89
425d2e2
e30543a
aa454aa
896feb4
3b45ea1
99915ab
3c33ac6
67640e5
338b758
c35a549
61e10b4
59f65dc
bf070b0
39bee27
574eedb
df93330
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 |
|---|---|---|
|
|
@@ -76,6 +76,7 @@ | |
| onStartStreaming: ?*const fn (ctx: *anyopaque) jsc.WebCore.DrainResult = null, | ||
| onReadableStreamAvailable: ?*const fn (ctx: *anyopaque, globalThis: *jsc.JSGlobalObject, readable: jsc.WebCore.ReadableStream) void = null, | ||
| onStreamCancelled: ?*const fn (ctx: ?*anyopaque) void = null, | ||
| onStreamConsumed: ?*const fn (ctx: ?*anyopaque, bytes: usize) void = null, | ||
| size_hint: Blob.SizeType = 0, | ||
|
|
||
| deinit: bool = false, | ||
|
|
@@ -519,6 +520,12 @@ | |
| reader.cancel_ctx = task; | ||
| } | ||
| } | ||
| if (locked.onStreamConsumed) |onConsumed| { | ||
| if (locked.task) |task| { | ||
| reader.drain_handler = onConsumed; | ||
| reader.drain_ctx = task; | ||
| } | ||
| } | ||
|
Check failure on line 528 in src/bun.js/webcore/Body.zig
|
||
|
|
||
| reader.context.setup(); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -348,6 +348,26 @@ | |
| } | ||
| } | ||
|
|
||
| /// HTTP-thread wake-up from `scheduleResponseBodyConsumed`: the JS reader | ||
| /// drained `bytes` from the ByteStream. Bump the stream's consumption | ||
| /// counter and release any per-stream window credit that has become | ||
| /// available. | ||
| pub fn consumeResponseBodyByHttpId(this: *ClientSession, async_http_id: u32, bytes: u32) void { | ||
| this.ref(); | ||
| defer this.deref(); | ||
| for (this.streams.values()) |stream| { | ||
| const client = stream.client orelse continue; | ||
| if (client.async_http_id != async_http_id) continue; | ||
| // `bytes` is decompressed; clamp the running total to wire bytes | ||
| // still outstanding so a compression surplus isn't banked to | ||
| // credit later DATA the reader hasn't touched. | ||
| stream.consumed_bytes = @min(stream.consumed_bytes +| bytes, stream.unacked_bytes); | ||
| this.replenishWindow(); | ||
| if (this.write_buffer.isNotEmpty()) _ = this.flush() catch |err| this.failAll(err); | ||
| return; | ||
| } | ||
| } | ||
|
|
||
| /// HTTP-thread wake-up from `scheduleRequestWrite`: new body bytes (or | ||
| /// end-of-body) are available in the ThreadSafeStreamBuffer. | ||
| pub fn streamBodyByHttpId(this: *ClientSession, async_http_id: u32, ended: bool) void { | ||
|
|
@@ -372,17 +392,29 @@ | |
|
|
||
| fn replenishWindow(this: *ClientSession) void { | ||
| const threshold = local_initial_window_size / 2; | ||
| // Connection-level credit stays receipt-based so one stream whose JS | ||
| // reader is stalled doesn't starve siblings of the shared window. | ||
| if (this.conn_unacked_bytes >= threshold) { | ||
| this.writeWindowUpdate(0, @intCast(this.conn_unacked_bytes)); | ||
| this.conn_unacked_bytes = 0; | ||
| } | ||
| var it = this.streams.iterator(); | ||
| while (it.next()) |e| { | ||
| const s = e.value_ptr.*; | ||
| if (s.unacked_bytes >= threshold and !s.remoteClosed()) { | ||
| this.writeWindowUpdate(s.id, @intCast(s.unacked_bytes)); | ||
| s.unacked_bytes = 0; | ||
| if (s.remoteClosed()) continue; | ||
| // Streaming consumer (`res.body.getReader()`): credit only what JS | ||
| // has actually drained, clamped to wire bytes received so a | ||
| // decompressed body can't inflate the window past what was sent. | ||
| // Buffering consumers (`await res.text()` etc.) keep receipt-based | ||
| // crediting — the whole body is going into memory regardless, so | ||
| // withholding the window just slows the transfer. | ||
| const streaming = if (s.client) |c| c.signals.get(.response_body_streaming) else false; | ||
| const avail: u32 = if (streaming) @min(s.consumed_bytes, s.unacked_bytes) else s.unacked_bytes; | ||
| if (avail >= threshold) { | ||
| this.writeWindowUpdate(s.id, @intCast(avail)); | ||
| s.unacked_bytes -= avail; | ||
| s.consumed_bytes -|= avail; | ||
| } | ||
|
Check failure on line 417 in src/http/h2_client/ClientSession.zig
|
||
|
claude[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
| } | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.