Skip to content

Commit 1576cb8

Browse files
authored
http2: increase default window sizes
Increase the default HTTP/2 stream window from 64KB (65535) to 4MB (4194304) and the default local connection window to 32MB (33554432). The default 64KB window limits throughput on high-latency connections to window_size / RTT. With a 250ms RTT, throughput is limited to 256KB/s. The new defaults improve throughput to 16MB/s (128Mbps) for the stream window and 128MB/s (1Gbps) for the connection window. Fixes: #38426 Signed-off-by: Matteo Collina <hello@matteocollina.com> PR-URL: #64623 Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com> Reviewed-By: Tim Perry <pimterry@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day> Reviewed-By: Ethan Arrowood <ethan@arrowood.dev> Reviewed-By: Aviv Keller <me@aviv.sh> Reviewed-By: Robert Nagy <ronagy@icloud.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 31cde9f commit 1576cb8

19 files changed

Lines changed: 96 additions & 66 deletions

doc/api/http2.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3656,7 +3656,7 @@ properties.
36563656
permitted on the `Http2Session` instances. **Default:** `true`.
36573657
* `initialWindowSize` {number} Specifies the _sender's_ initial window size in
36583658
bytes for stream-level flow control. The minimum allowed value is 0. The
3659-
maximum allowed value is 2<sup>32</sup>-1. **Default:** `65535`.
3659+
maximum allowed value is 2<sup>32</sup>-1. **Default:** `4194304`.
36603660
* `maxFrameSize` {number} Specifies the size in bytes of the largest frame
36613661
payload. The minimum allowed value is 16,384. The maximum allowed value is
36623662
2<sup>24</sup>-1. **Default:** `16384`.

lib/internal/http2/core.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1205,7 +1205,16 @@ function setupHandle(socket, type, options) {
12051205
}
12061206

12071207
const settings = typeof options.settings === 'object' ?
1208-
options.settings : {};
1208+
{ ...options.settings } : {};
1209+
1210+
// Increase the default initial window size to improve throughput
1211+
// on high-latency connections. The HTTP/2 default of 65535 (64KB)
1212+
// limits throughput to window_size / RTT. By increasing to 4MB,
1213+
// throughput is significantly improved.
1214+
// See https://github.com/nodejs/node/issues/38426
1215+
if (settings.initialWindowSize === undefined) {
1216+
settings.initialWindowSize = constants.DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE;
1217+
}
12091218

12101219
this.settings(settings);
12111220

src/node_http2.cc

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,17 @@ Http2Session::Http2Session(Http2State* http2_state,
612612
&alloc_info), 0);
613613
session_.reset(session);
614614

615+
// Increase the default local connection window to improve throughput
616+
// on high-latency connections. The default 64KB window limits throughput
617+
// to window_size / RTT. With a 32MB connection window, throughput is
618+
// significantly improved. See https://github.com/nodejs/node/issues/38426
619+
CHECK_EQ(nghttp2_session_set_local_window_size(
620+
session,
621+
NGHTTP2_FLAG_NONE,
622+
0,
623+
DEFAULT_SETTINGS_LOCAL_CONNECTION_WINDOW_SIZE),
624+
0);
625+
615626
outgoing_storage_.reserve(1024);
616627
outgoing_buffers_.reserve(32);
617628

src/node_http2.h

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,18 @@ constexpr uint64_t kDefaultMaxSessionMemory = 10000000;
4343
constexpr uint32_t DEFAULT_SETTINGS_HEADER_TABLE_SIZE = 4096;
4444
constexpr uint32_t DEFAULT_SETTINGS_ENABLE_PUSH = 1;
4545
constexpr uint32_t DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS = 0xffffffffu;
46-
constexpr uint32_t DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE = 65535;
46+
constexpr uint32_t DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE = 4194304;
4747
constexpr uint32_t DEFAULT_SETTINGS_MAX_FRAME_SIZE = 16384;
4848
constexpr uint32_t DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE = 65535;
4949
constexpr uint32_t DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL = 0;
5050
constexpr uint32_t MAX_MAX_FRAME_SIZE = 16777215;
5151
constexpr uint32_t MIN_MAX_FRAME_SIZE = DEFAULT_SETTINGS_MAX_FRAME_SIZE;
5252
constexpr uint32_t MAX_INITIAL_WINDOW_SIZE = 2147483647;
5353

54+
// Default local connection window size (32MB) to improve throughput
55+
// on high-latency connections. See https://github.com/nodejs/node/issues/38426
56+
constexpr uint32_t DEFAULT_SETTINGS_LOCAL_CONNECTION_WINDOW_SIZE = 33554432;
57+
5458
// Stream is not going to have any DATA frames
5559
constexpr int STREAM_OPTION_EMPTY_PAYLOAD = 0x1;
5660

src/stream_base.cc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,8 @@ void CustomBufferJSListener::OnStreamRead(ssize_t nread, const uv_buf_t& buf) {
752752
void ReportWritesToJSStreamListener::OnStreamAfterReqFinished(
753753
StreamReq* req_wrap, int status) {
754754
StreamBase* stream = static_cast<StreamBase*>(stream_);
755+
if (stream == nullptr) return;
756+
if (req_wrap == nullptr) return;
755757
Environment* env = stream->stream_env();
756758
if (!env->can_call_into_js()) return;
757759
AsyncWrap* async_wrap = req_wrap->GetAsyncWrap();

src/stream_pipe.cc

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ void StreamPipe::Unpipe(bool is_in_deletion) {
5959
is_closed_ = true;
6060
is_reading_ = false;
6161
source()->RemoveStreamListener(&readable_listener_);
62-
if (pending_writes_ == 0)
62+
if (pending_writes_ == 0 || sink_destroyed_)
6363
sink()->RemoveStreamListener(&writable_listener_);
6464

6565
if (is_in_deletion) return;
@@ -159,13 +159,18 @@ void StreamPipe::WritableListener::OnStreamAfterWrite(WriteWrap* w,
159159
StreamPipe* pipe = ContainerOf(&StreamPipe::writable_listener_, this);
160160
pipe->pending_writes_--;
161161
if (pipe->is_closed_) {
162-
if (pipe->pending_writes_ == 0) {
162+
// If the sink has been destroyed, pending_writes_ may have been
163+
// reset and we should check <= 0 instead of == 0. Also guard
164+
// against the listener having already been removed.
165+
bool writes_done = pipe->sink_destroyed_ ? pipe->pending_writes_ <= 0
166+
: pipe->pending_writes_ == 0;
167+
if (writes_done) {
163168
Environment* env = pipe->env();
164169
HandleScope handle_scope(env->isolate());
165170
Context::Scope context_scope(env->context());
166171
if (pipe->MakeCallback(env->oncomplete_string(), 0, nullptr).IsEmpty())
167172
return;
168-
stream()->RemoveStreamListener(this);
173+
if (stream() != nullptr) stream()->RemoveStreamListener(this);
169174
}
170175
return;
171176
}
@@ -213,7 +218,9 @@ void StreamPipe::WritableListener::OnStreamDestroy() {
213218
StreamPipe* pipe = ContainerOf(&StreamPipe::writable_listener_, this);
214219
pipe->sink_destroyed_ = true;
215220
pipe->is_eof_ = true;
216-
pipe->pending_writes_ = 0;
221+
// Don't reset pending_writes_ here; let OnStreamAfterWrite track
222+
// completion naturally. Unpipe() will remove this listener from the
223+
// sink regardless of pending_writes_ since sink_destroyed_ is set.
217224
pipe->Unpipe();
218225
}
219226

test/parallel/test-http2-binding.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const settings = http2.getDefaultSettings();
1717
assert.strictEqual(settings.headerTableSize, 4096);
1818
assert.strictEqual(settings.enablePush, true);
1919
assert.strictEqual(settings.maxConcurrentStreams, 4294967295);
20-
assert.strictEqual(settings.initialWindowSize, 65535);
20+
assert.strictEqual(settings.initialWindowSize, 4194304);
2121
assert.strictEqual(settings.maxFrameSize, 16384);
2222

2323
assert.strictEqual(binding.nghttp2ErrorString(-517),
@@ -239,7 +239,7 @@ const defaultSettings = {
239239
DEFAULT_SETTINGS_HEADER_TABLE_SIZE: 4096,
240240
DEFAULT_SETTINGS_ENABLE_PUSH: 1,
241241
DEFAULT_SETTINGS_MAX_CONCURRENT_STREAMS: 4294967295,
242-
DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 65535,
242+
DEFAULT_SETTINGS_INITIAL_WINDOW_SIZE: 4194304,
243243
DEFAULT_SETTINGS_MAX_FRAME_SIZE: 16384,
244244
DEFAULT_SETTINGS_MAX_HEADER_LIST_SIZE: 65535,
245245
DEFAULT_SETTINGS_ENABLE_CONNECT_PROTOCOL: 0

test/parallel/test-http2-client-setLocalWindowSize.js

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,16 @@ const http2 = require('http2');
7373

7474
client.on('connect', common.mustCall(() => {
7575
const windowSize = 2 ** 20;
76-
const defaultSetting = http2.getDefaultSettings();
7776
client.setLocalWindowSize(windowSize);
7877

7978
assert.strictEqual(client.state.effectiveLocalWindowSize, windowSize);
80-
assert.strictEqual(client.state.localWindowSize, windowSize);
81-
assert.strictEqual(
82-
client.state.remoteWindowSize,
83-
defaultSetting.initialWindowSize
84-
);
79+
// localWindowSize returns the available connection window.
80+
// When decreasing from the default 33554432 to 1048576,
81+
// the available window stays at 33554432.
82+
assert.strictEqual(client.state.localWindowSize, 33554432);
83+
// remoteWindowSize is the connection-level send window,
84+
// which remains at the HTTP/2 default of 65535.
85+
assert.strictEqual(client.state.remoteWindowSize, 65535);
8586

8687
server.close();
8788
client.close();
@@ -101,18 +102,16 @@ const http2 = require('http2');
101102

102103
client.on('connect', common.mustCall(() => {
103104
const windowSize = 20;
104-
const defaultSetting = http2.getDefaultSettings();
105105
client.setLocalWindowSize(windowSize);
106106

107107
assert.strictEqual(client.state.effectiveLocalWindowSize, windowSize);
108-
assert.strictEqual(
109-
client.state.localWindowSize,
110-
defaultSetting.initialWindowSize
111-
);
112-
assert.strictEqual(
113-
client.state.remoteWindowSize,
114-
defaultSetting.initialWindowSize
115-
);
108+
// localWindowSize returns the available connection window.
109+
// When decreasing from the default 33554432 to 20,
110+
// the available window stays at 33554432.
111+
assert.strictEqual(client.state.localWindowSize, 33554432);
112+
// remoteWindowSize is the connection-level send window,
113+
// which remains at the HTTP/2 default of 65535.
114+
assert.strictEqual(client.state.remoteWindowSize, 65535);
116115

117116
server.close();
118117
client.close();

test/parallel/test-http2-getpackedsettings.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const http2 = require('http2');
99
const check = Buffer.from([0x00, 0x01, 0x00, 0x00, 0x10, 0x00,
1010
0x00, 0x02, 0x00, 0x00, 0x00, 0x01,
1111
0x00, 0x03, 0xff, 0xff, 0xff, 0xff,
12-
0x00, 0x04, 0x00, 0x00, 0xff, 0xff,
12+
0x00, 0x04, 0x00, 0x40, 0x00, 0x00,
1313
0x00, 0x05, 0x00, 0x00, 0x40, 0x00,
1414
0x00, 0x06, 0x00, 0x00, 0xff, 0xff,
1515
0x00, 0x08, 0x00, 0x00, 0x00, 0x00]);

test/parallel/test-http2-pack-end-stream-flag.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ function testRequest(path, targetFrameCount, callback) {
5252
});
5353
}
5454

55-
// SETTINGS => SETTINGS => HEADERS => DATA
56-
const MIN_FRAME_COUNT = 4;
55+
// SETTINGS => WINDOW_UPDATE => SETTINGS ACK => HEADERS => DATA
56+
// The WINDOW_UPDATE frame is sent because the default local connection
57+
// window is now increased to 32MB (see https://github.com/nodejs/node/issues/38426)
58+
const MIN_FRAME_COUNT = 5;
5759

5860
server.listen(0, () => {
5961
testRequest('/singleEnd', MIN_FRAME_COUNT, () => {

0 commit comments

Comments
 (0)