]> git.feebdaed.xyz Git - 0xmirror/curl.git/commitdiff
ngtcp2: retune window sizes
authorStefan Eissing <stefan@eissing.org>
Fri, 19 Dec 2025 10:53:51 +0000 (11:53 +0100)
committerDaniel Stenberg <daniel@haxx.se>
Fri, 19 Dec 2025 12:56:07 +0000 (13:56 +0100)
With 24b36fd stream flow control window sizes have been set too
restrictive, crippling transfer rates when no rate limit is in effect.

Disable ngtcp3 stream window auto-tuning and extend the stream window
from the small initial size to the effective rate limit. If no rate
limit is configured, extend stream window to maximum value right away.

This cannot shrink the stream window later, however. But growing the
limit or removing it, will work mid download.

Fixes #20030
Reported-by: koujaz on github
Closes #20033

lib/vquic/curl_ngtcp2.c

index 550e4b2befcfae1c5678ce09738648a61f1f9434..fd7f9a332421bdae3654c8945ad65cd34f326787 100644 (file)
 /* A stream window is the maximum amount we need to buffer for
  * each active transfer.
  * Chunk size is large enough to take a full DATA frame */
-#define H3_STREAM_WINDOW_SIZE (64 * 1024)
-#define H3_STREAM_CHUNK_SIZE  (16 * 1024)
+#define H3_STREAM_WINDOW_SIZE_INITIAL (32 * 1024)
+#define H3_STREAM_WINDOW_SIZE_MAX     (10 * 1024 * 1024)
+#define H3_CONN_WINDOW_SIZE_MAX       (100 * H3_STREAM_WINDOW_SIZE_MAX)
+
+#define H3_STREAM_CHUNK_SIZE  (64 * 1024)
 #if H3_STREAM_CHUNK_SIZE < NGTCP2_MAX_UDP_PAYLOAD_SIZE
 #error H3_STREAM_CHUNK_SIZE smaller than NGTCP2_MAX_UDP_PAYLOAD_SIZE
 #endif
  * The benefit of the pool is that stream buffer to not keep
  * spares. Memory consumption goes down when streams run empty,
  * have a large upload done, etc. */
-#define H3_STREAM_POOL_SPARES \
-  (H3_STREAM_WINDOW_SIZE / H3_STREAM_CHUNK_SIZE) / 2
+#define H3_STREAM_POOL_SPARES      2
 /* Receive and Send max number of chunks just follows from the
  * chunk size and window size */
 #define H3_STREAM_SEND_CHUNKS \
-  (H3_STREAM_WINDOW_SIZE / H3_STREAM_CHUNK_SIZE)
+  (H3_STREAM_WINDOW_SIZE_MAX / H3_STREAM_CHUNK_SIZE)
 
 /*
  * Store ngtcp2 version info in this buffer.
@@ -131,7 +133,6 @@ struct cf_ngtcp2_ctx {
   struct bufc_pool stream_bufcp;     /* chunk pool for streams */
   struct dynbuf scratch;             /* temp buffer for header construction */
   struct uint_hash streams;          /* hash `data->mid` to `h3_stream_ctx` */
-  size_t max_stream_window;          /* max flow window for one stream */
   uint64_t used_bidi_streams;        /* bidi streams we have opened */
   uint64_t max_bidi_streams;         /* max bidi streams we can open */
   size_t earlydata_max;              /* max amount of early data supported by
@@ -158,7 +159,6 @@ static void cf_ngtcp2_ctx_init(struct cf_ngtcp2_ctx *ctx)
   DEBUGASSERT(!ctx->initialized);
   ctx->qlogfd = -1;
   ctx->version = NGTCP2_PROTO_VER_MAX;
-  ctx->max_stream_window = H3_STREAM_WINDOW_SIZE;
   Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE,
                   H3_STREAM_POOL_SPARES);
   curlx_dyn_init(&ctx->scratch, CURL_MAX_HTTP_HEADER);
@@ -235,6 +235,7 @@ struct h3_stream_ctx {
   uint64_t error3;              /* HTTP/3 stream error code */
   curl_off_t upload_left;       /* number of request bytes left to upload */
   uint64_t download_unacked;    /* bytes not acknowledged yet */
+  uint64_t window_size_max;     /* max flow control window set for stream */
   int status_code;              /* HTTP status code */
   CURLcode xfer_result;         /* result from xfer_resp_write(_hd) */
   BIT(resp_hds_complete);       /* we have a complete, final response */
@@ -279,6 +280,7 @@ static CURLcode h3_data_setup(struct Curl_cfilter *cf,
   Curl_bufq_initp(&stream->sendbuf, &ctx->stream_bufcp,
                   H3_STREAM_SEND_CHUNKS, BUFQ_OPT_NONE);
   stream->sendbuf_len_in_flight = 0;
+  stream->window_size_max = H3_STREAM_WINDOW_SIZE_INITIAL;
   Curl_h1_req_parse_init(&stream->h1, H1_PARSE_DEFAULT_MAX_LINE_LEN);
 
   if(!Curl_uint32_hash_set(&ctx->streams, data->mid, stream)) {
@@ -470,18 +472,18 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx,
   s->initial_ts = pktx->ts;
   s->handshake_timeout = (data->set.connecttimeout > 0) ?
     data->set.connecttimeout * NGTCP2_MILLISECONDS : QUIC_HANDSHAKE_TIMEOUT;
-  s->max_window = 100 * ctx->max_stream_window;
-  s->max_stream_window = ctx->max_stream_window;
+  s->max_window = H3_CONN_WINDOW_SIZE_MAX;
+  s->max_stream_window = 0; /* disable ngtcp2 auto-tuning of window */
   s->no_pmtud = FALSE;
 #ifdef NGTCP2_SETTINGS_V3
   /* try ten times the ngtcp2 defaults here for problems with Caddy */
   s->glitch_ratelim_burst = 1000 * 10;
   s->glitch_ratelim_rate = 33 * 10;
 #endif
-  t->initial_max_data = 10 * ctx->max_stream_window;
-  t->initial_max_stream_data_bidi_local = ctx->max_stream_window;
-  t->initial_max_stream_data_bidi_remote = ctx->max_stream_window;
-  t->initial_max_stream_data_uni = ctx->max_stream_window;
+  t->initial_max_data = s->max_window;
+  t->initial_max_stream_data_bidi_local = H3_STREAM_WINDOW_SIZE_INITIAL;
+  t->initial_max_stream_data_bidi_remote = H3_STREAM_WINDOW_SIZE_INITIAL;
+  t->initial_max_stream_data_uni = t->initial_max_data;
   t->initial_max_streams_bidi = QUIC_MAX_STREAMS;
   t->initial_max_streams_uni = QUIC_MAX_STREAMS;
   t->max_idle_timeout = 0; /* no idle timeout from our side */
@@ -1047,6 +1049,28 @@ static void h3_xfer_write_resp(struct Curl_cfilter *cf,
   }
 }
 
+static void cf_ngtcp2_stream_update_window(struct Curl_cfilter *cf,
+                                           struct Curl_easy *data,
+                                           struct h3_stream_ctx *stream)
+{
+  /* stream receive max window size for flow control. We can only
+   * grow it from the initial window size */
+  uint64_t swin_max = data->progress.dl.rlimit.rate_per_step ?
+    data->progress.dl.rlimit.rate_per_step : H3_STREAM_WINDOW_SIZE_MAX;
+  if(swin_max > stream->window_size_max) {
+    struct cf_ngtcp2_ctx *ctx = cf->ctx;
+    int rc = ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream->id,
+      swin_max - stream->window_size_max);
+    if(rc) {
+      CURL_TRC_CF(data, cf, "[%" PRId64 "] extend_max_stream_offset to %"
+                  PRIu64 " -> %s (%d)",
+                  stream->id, swin_max, ngtcp2_strerror(rc), rc);
+      DEBUGASSERT(0);
+    }
+    stream->window_size_max = swin_max;
+  }
+}
+
 static void cf_ngtcp2_ack_stream(struct Curl_cfilter *cf,
                                  struct Curl_easy *data,
                                  struct h3_stream_ctx *stream)
@@ -1074,6 +1098,8 @@ static void cf_ngtcp2_ack_stream(struct Curl_cfilter *cf,
     ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream->id, ack_len);
     stream->download_unacked -= ack_len;
   }
+
+  cf_ngtcp2_stream_update_window(cf, data, stream);
 }
 
 static int cb_h3_recv_data(nghttp3_conn *conn, int64_t stream3_id,
@@ -1635,6 +1661,8 @@ static CURLcode h3_stream_open(struct Curl_cfilter *cf,
     goto out;
   }
 
+  cf_ngtcp2_stream_update_window(cf, data, stream);
+
   if(Curl_trc_is_verbose(data)) {
     infof(data, "[HTTP/3] [%" PRId64 "] OPENED stream for %s",
           stream->id, Curl_bufref_ptr(&data->state.url));