diff --git a/lib/http2.c b/lib/http2.c index 3520b544a4..302f2feff7 100644 --- a/lib/http2.c +++ b/lib/http2.c @@ -216,9 +216,9 @@ static uint32_t cf_h2_initial_win_size(struct Curl_easy *data) /* If the transfer has a rate-limit lower than the default initial * stream window size, use that. It needs to be at least 8k or servers * may be unhappy. */ - if(data->progress.dl.rlimit.rate_per_step && - (data->progress.dl.rlimit.rate_per_step < H2_STREAM_WINDOW_SIZE_INITIAL)) - return CURLMAX((uint32_t)data->progress.dl.rlimit.rate_per_step, 8192); + curl_off_t rps = Curl_rlimit_per_step(&data->progress.dl.rlimit); + if((rps > 0) && (rps < H2_STREAM_WINDOW_SIZE_INITIAL)) + return CURLMAX((uint32_t)rps, 8192); #endif return H2_STREAM_WINDOW_SIZE_INITIAL; } diff --git a/lib/multi.c b/lib/multi.c index 4cd37ed466..0c99e1b653 100644 --- a/lib/multi.c +++ b/lib/multi.c @@ -1922,25 +1922,42 @@ static CURLcode multi_follow(struct Curl_easy *data, static CURLcode mspeed_check(struct Curl_easy *data) { - const struct curltime *pnow = Curl_pgrs_now(data); - timediff_t recv_wait_ms = 0; - timediff_t send_wait_ms = 0; + if(Curl_rlimit_active(&data->progress.dl.rlimit) || + Curl_rlimit_active(&data->progress.ul.rlimit)) { + /* check if our send/recv limits require idle waits */ + const struct curltime *pnow = Curl_pgrs_now(data); + timediff_t recv_ms, send_ms; - /* check if our send/recv limits require idle waits */ - send_wait_ms = Curl_rlimit_wait_ms(&data->progress.ul.rlimit, pnow); - recv_wait_ms = Curl_rlimit_wait_ms(&data->progress.dl.rlimit, pnow); + send_ms = Curl_rlimit_wait_ms(&data->progress.ul.rlimit, pnow); + recv_ms = Curl_rlimit_wait_ms(&data->progress.dl.rlimit, pnow); - if(send_wait_ms || recv_wait_ms) { - if(data->mstate != MSTATE_RATELIMITING) { - multistate(data, MSTATE_RATELIMITING); + if(send_ms || recv_ms) { + if(data->mstate != MSTATE_RATELIMITING) { + multistate(data, MSTATE_RATELIMITING); + } + Curl_expire(data, CURLMAX(send_ms, recv_ms), EXPIRE_TOOFAST); + Curl_multi_clear_dirty(data); + CURL_TRC_M(data, "[RLIMIT] waiting %" FMT_TIMEDIFF_T "ms", + CURLMAX(send_ms, recv_ms)); + return CURLE_AGAIN; + } + else { + /* when will the rate limits increase next? The transfer needs + * to run again at that time or it may stall. */ + send_ms = Curl_rlimit_next_step_ms(&data->progress.ul.rlimit, pnow); + recv_ms = Curl_rlimit_next_step_ms(&data->progress.dl.rlimit, pnow); + if(send_ms || recv_ms) { + timediff_t next_ms = CURLMIN(send_ms, recv_ms); + if(!next_ms) + next_ms = CURLMAX(send_ms, recv_ms); + Curl_expire(data, next_ms, EXPIRE_TOOFAST); + CURL_TRC_M(data, "[RLIMIT] next token update in %" FMT_TIMEDIFF_T "ms", + next_ms); + } } - Curl_expire(data, CURLMAX(send_wait_ms, recv_wait_ms), EXPIRE_TOOFAST); - Curl_multi_clear_dirty(data); - CURL_TRC_M(data, "[RLIMIT] waiting %" FMT_TIMEDIFF_T "ms", - CURLMAX(send_wait_ms, recv_wait_ms)); - return CURLE_AGAIN; } - else if(data->mstate != MSTATE_PERFORMING) { + + if(data->mstate != MSTATE_PERFORMING) { CURL_TRC_M(data, "[RLIMIT] wait over, continue"); multistate(data, MSTATE_PERFORMING); } diff --git a/lib/progress.c b/lib/progress.c index 3c9fa88b60..7daa4b4b3e 100644 --- a/lib/progress.c +++ b/lib/progress.c @@ -415,7 +415,7 @@ static bool progress_calc(struct Curl_easy *data, { struct Progress * const p = &data->progress; int i_next, i_oldest, i_latest; - timediff_t duration_ms; + timediff_t duration_us; curl_off_t amount; /* The time spent so far (from the start) in microseconds */ @@ -466,21 +466,19 @@ static bool progress_calc(struct Curl_easy *data, /* How much we transferred between oldest and current records */ amount = p->speed_amount[i_latest] - p->speed_amount[i_oldest]; /* How long this took */ - duration_ms = curlx_ptimediff_ms(&p->speed_time[i_latest], + duration_us = curlx_ptimediff_us(&p->speed_time[i_latest], &p->speed_time[i_oldest]); - if(duration_ms <= 0) - duration_ms = 1; + if(duration_us <= 0) + duration_us = 1; - if(amount > (CURL_OFF_T_MAX / 1000)) { + if(amount > (CURL_OFF_T_MAX / 1000000)) { /* the 'amount' value is bigger than would fit in 64 bits if - multiplied with 1000, so we use the double math for this */ + multiplied with 1000000, so we use the double math for this */ p->current_speed = - (curl_off_t)(((double)amount * 1000.0) / (double)duration_ms); + (curl_off_t)(((double)amount * 1000000.0) / (double)duration_us); } else { - /* the 'amount' value is small enough to fit within 32 bits even - when multiplied with 1000 */ - p->current_speed = amount * 1000 / duration_ms; + p->current_speed = amount * 1000000 / duration_us; } if((p->lastshow == pnow->tv_sec) && !data->req.done) diff --git a/lib/ratelimit.c b/lib/ratelimit.c index c7ba64615e..7abbde87cc 100644 --- a/lib/ratelimit.c +++ b/lib/ratelimit.c @@ -23,64 +23,20 @@ ***************************************************************************/ #include "curl_setup.h" +#include "urldata.h" +#include "curl_trc.h" +#include "progress.h" #include "ratelimit.h" -#define CURL_US_PER_SEC 1000000 -#define CURL_RLIMIT_MIN_CHUNK (16 * 1024) -#define CURL_RLIMIT_MAX_STEPS 2 /* 500ms interval */ +#define CURL_US_PER_SEC 1000000 +#define CURL_RLIMIT_MIN_RATE (4 * 1024) /* minimum step rate */ +#define CURL_RLIMIT_STEP_MIN_MS 2 /* minimum step duration */ -void Curl_rlimit_init(struct Curl_rlimit *r, - curl_off_t rate_per_s, - curl_off_t burst_per_s, - const struct curltime *pts) -{ - curl_off_t rate_steps; - - DEBUGASSERT(rate_per_s >= 0); - DEBUGASSERT(burst_per_s >= rate_per_s || !burst_per_s); - DEBUGASSERT(pts); - r->step_us = CURL_US_PER_SEC; - r->rate_per_step = rate_per_s; - r->burst_per_step = burst_per_s; - /* On rates that are multiples of CURL_RLIMIT_MIN_CHUNK, we reduce - * the interval `step_us` from 1 second to smaller steps with at - * most CURL_RLIMIT_MAX_STEPS. - * Smaller means more CPU, but also more precision. */ - rate_steps = rate_per_s / CURL_RLIMIT_MIN_CHUNK; - rate_steps = CURLMIN(rate_steps, CURL_RLIMIT_MAX_STEPS); - if(rate_steps >= 2) { - r->step_us /= rate_steps; - r->rate_per_step /= rate_steps; - r->burst_per_step /= rate_steps; - } - r->tokens = r->rate_per_step; - r->spare_us = 0; - r->ts = *pts; - r->blocked = FALSE; -} - -void Curl_rlimit_start(struct Curl_rlimit *r, const struct curltime *pts) -{ - r->tokens = r->rate_per_step; - r->spare_us = 0; - r->ts = *pts; -} - -bool Curl_rlimit_active(struct Curl_rlimit *r) -{ - return (r->rate_per_step > 0) || r->blocked; -} - -bool Curl_rlimit_is_blocked(struct Curl_rlimit *r) -{ - return r->blocked; -} - -static void ratelimit_update(struct Curl_rlimit *r, - const struct curltime *pts) +static void rlimit_update(struct Curl_rlimit *r, + const struct curltime *pts) { timediff_t elapsed_us, elapsed_steps; - curl_off_t token_gain; + int64_t token_gain; DEBUGASSERT(r->rate_per_step); if((r->ts.tv_sec == pts->tv_sec) && (r->ts.tv_usec == pts->tv_usec)) @@ -102,31 +58,151 @@ static void ratelimit_update(struct Curl_rlimit *r, r->spare_us = elapsed_us % r->step_us; /* How many tokens did we gain since the last update? */ - if(r->rate_per_step > (CURL_OFF_T_MAX / elapsed_steps)) - token_gain = CURL_OFF_T_MAX; + if(r->rate_per_step > (INT64_MAX / elapsed_steps)) + token_gain = INT64_MAX; else { token_gain = r->rate_per_step * elapsed_steps; } - /* Limit the token again by the burst rate per second (if set), so we + if((INT64_MAX - token_gain) > r->tokens) + r->tokens += token_gain; + else + r->tokens = INT64_MAX; + + /* Limit the token again by the burst rate (if set), so we * do not suddenly have a huge number of tokens after inactivity. */ - r->tokens += token_gain; if(r->burst_per_step && (r->tokens > r->burst_per_step)) { r->tokens = r->burst_per_step; } } -curl_off_t Curl_rlimit_avail(struct Curl_rlimit *r, - const struct curltime *pts) +static void rlimit_tune_steps(struct Curl_rlimit *r, + int64_t tokens_total) +{ + int64_t tokens_last, tokens_main, msteps; + + /* Tune the ratelimit at the start *if* we know how many tokens + * are expected to be consumed in total. + * The reason for tuning is that rlimit provides tokens to be consumed + * per "step" which starts out to be a second. The tokens may be consumed + * in full at the beginning of a step. The remainder of the second will + * have no tokens available, effectively blocking the consumption and + * so keeping the "step average" in line. + * This works will up to the last step. When no more tokens are needed, + * no wait will happen and the last step would be too fast. This is + * especially noticeable when only a few steps are needed. + * + * Example: downloading 1.5kb with a ratelimit of 1k could be done in + * roughly 1 second (1k in the first second and the 0.5 at the start of + * the second one). + * + * The tuning tries to make the last step small, using only + * 1 percent of the total tokens (at least 1). The rest of the tokens + * are to be consumed in the steps before by adjusting the duration of + * the step and the amount of tokens it provides. */ + if(!r->rate_per_step || + (tokens_total <= 1) || + (tokens_total > (INT64_MAX / 1000))) + return; + + /* Calculate tokens for the last step and the ones before. */ + tokens_last = tokens_total / 100; + if(!tokens_last) /* less than 100 total, just use 1 */ + tokens_last = 1; + else if(tokens_last > CURL_RLIMIT_MIN_RATE) + tokens_last = CURL_RLIMIT_MIN_RATE; + DEBUGASSERT(tokens_last); + tokens_main = tokens_total - tokens_last; + DEBUGASSERT(tokens_main); + + /* how many milli-steps will it take to consume those, give the + * original rate limit per second? */ + DEBUGASSERT(r->step_us == CURL_US_PER_SEC); + + msteps = (tokens_main * 1000 / r->rate_per_step); + if(msteps < CURL_RLIMIT_STEP_MIN_MS) { + /* Steps this small will not work. Do not tune. */ + return; + } + else if(msteps < 1000) { + /* It needs less than one step to provide the needed tokens. + * Make it exactly that long and with exactly those tokens. */ + r->step_us = (timediff_t)msteps * 1000; + r->rate_per_step = tokens_main; + r->tokens = r->rate_per_step; + } + else { + /* More than 1 step. Spread the remainder milli steps and + * the tokens they need to provide across all steps. If integer + * arithmetic can do it. */ + curl_off_t ms_unaccounted = (msteps % 1000); + curl_off_t mstep_inc = (ms_unaccounted / (msteps / 1000)); + if(mstep_inc) { + curl_off_t rate_inc = ((r->rate_per_step * mstep_inc) / 1000); + if(rate_inc) { + r->step_us = CURL_US_PER_SEC + ((timediff_t)mstep_inc * 1000); + r->rate_per_step += rate_inc; + r->tokens = r->rate_per_step; + } + } + } + + if(r->burst_per_step) + r->burst_per_step = r->rate_per_step; +} + +void Curl_rlimit_init(struct Curl_rlimit *r, + int64_t rate_per_sec, + int64_t burst_per_sec, + const struct curltime *pts) +{ + DEBUGASSERT(rate_per_sec >= 0); + DEBUGASSERT(burst_per_sec >= rate_per_sec || !burst_per_sec); + DEBUGASSERT(pts); + r->rate_per_step = rate_per_sec; + r->burst_per_step = burst_per_sec; + r->step_us = CURL_US_PER_SEC; + r->spare_us = 0; + r->tokens = r->rate_per_step; + r->ts = *pts; + r->blocked = FALSE; +} + +void Curl_rlimit_start(struct Curl_rlimit *r, const struct curltime *pts, + int64_t total_tokens) +{ + r->tokens = r->rate_per_step; + r->spare_us = 0; + r->ts = *pts; + rlimit_tune_steps(r, total_tokens); +} + +int64_t Curl_rlimit_per_step(struct Curl_rlimit *r) +{ + return r->rate_per_step; +} + +bool Curl_rlimit_active(struct Curl_rlimit *r) +{ + return (r->rate_per_step > 0) || r->blocked; +} + +bool Curl_rlimit_is_blocked(struct Curl_rlimit *r) +{ + return r->blocked; +} + +int64_t Curl_rlimit_avail(struct Curl_rlimit *r, + const struct curltime *pts) { if(r->blocked) return 0; else if(r->rate_per_step) { - ratelimit_update(r, pts); + rlimit_update(r, pts); return r->tokens; } else - return CURL_OFF_T_MAX; + return INT64_MAX; } void Curl_rlimit_drain(struct Curl_rlimit *r, @@ -136,20 +212,19 @@ void Curl_rlimit_drain(struct Curl_rlimit *r, if(r->blocked || !r->rate_per_step) return; - ratelimit_update(r, pts); -#if SIZEOF_CURL_OFF_T <= SIZEOF_SIZE_T - if(tokens > CURL_OFF_T_MAX) { - r->tokens = CURL_OFF_T_MIN; - return; + rlimit_update(r, pts); +#if 8 <= SIZEOF_SIZE_T + if(tokens > INT64_MAX) { + r->tokens = INT64_MAX; } else #endif { - curl_off_t val = (curl_off_t)tokens; - if((CURL_OFF_T_MIN + val) < r->tokens) + int64_t val = (int64_t)tokens; + if((INT64_MIN + val) < r->tokens) r->tokens -= val; else - r->tokens = CURL_OFF_T_MIN; + r->tokens = INT64_MIN; } } @@ -160,14 +235,18 @@ timediff_t Curl_rlimit_wait_ms(struct Curl_rlimit *r, if(r->blocked || !r->rate_per_step) return 0; - ratelimit_update(r, pts); + rlimit_update(r, pts); if(r->tokens > 0) return 0; /* How much time will it take tokens to become positive again? * Deduct `spare_us` and check against already elapsed time */ - wait_us = (1 + (-r->tokens / r->rate_per_step)) * r->step_us; - wait_us -= r->spare_us; + wait_us = r->step_us - r->spare_us; + if(r->tokens < 0) { + curl_off_t debt_pct = ((-r->tokens) * 100 / r->rate_per_step); + if(debt_pct) + wait_us += (r->step_us * debt_pct / 100); + } elapsed_us = curlx_ptimediff_us(pts, &r->ts); if(elapsed_us >= wait_us) @@ -176,6 +255,21 @@ timediff_t Curl_rlimit_wait_ms(struct Curl_rlimit *r, return (wait_us + 999) / 1000; /* in milliseconds */ } +timediff_t Curl_rlimit_next_step_ms(struct Curl_rlimit *r, + const struct curltime *pts) +{ + if(!r->blocked && r->rate_per_step) { + timediff_t elapsed_us, next_us; + + elapsed_us = curlx_ptimediff_us(pts, &r->ts) + r->spare_us; + if(r->step_us > elapsed_us) { + next_us = r->step_us - elapsed_us; + return (next_us + 999) / 1000; /* in milliseconds */ + } + } + return 0; +} + void Curl_rlimit_block(struct Curl_rlimit *r, bool activate, const struct curltime *pts) @@ -188,7 +282,7 @@ void Curl_rlimit_block(struct Curl_rlimit *r, if(!r->blocked) { /* Start rate limiting fresh. The amount of time this was blocked * does not generate extra tokens. */ - Curl_rlimit_start(r, pts); + Curl_rlimit_start(r, pts, -1); } else { r->tokens = 0; diff --git a/lib/ratelimit.h b/lib/ratelimit.h index ebef9a373e..7563734d65 100644 --- a/lib/ratelimit.h +++ b/lib/ratelimit.h @@ -28,8 +28,10 @@ struct Curl_easy; /* This is a rate limiter that provides "tokens" to be consumed - * per second with a "burst" rate limitation. Example: - * A rate limit of 1 megabyte per second with a burst rate of 1.5MB. + * per second. In the literature, this is referred to as a + * "token bucket" (https://en.wikipedia.org/wiki/Token_bucket). + * Example: + * A rate limit of 1 megabyte per second. * - initially 1 million tokens are available. * - these are drained in the first second. * - checking available tokens before the 2nd second will return 0. @@ -43,44 +45,55 @@ struct Curl_easy; * - setting "burst" to the same value as "rate" would make a * download always try to stay *at/below* the rate and slow times will * not generate extra tokens. + * * A rate limit can be blocked, causing the available tokens to become * always 0 until unblocked. After unblocking, the rate limiting starts * again with no history of the past. + * * Finally, a rate limiter with rate 0 will always have CURL_OFF_T_MAX * tokens available, unless blocked. */ struct Curl_rlimit { - curl_off_t rate_per_step; /* rate tokens are generated per step us */ - curl_off_t burst_per_step; /* burst rate of tokens per step us */ + int64_t rate_per_step; /* rate tokens generated per step us */ + int64_t burst_per_step; /* burst rate of tokens per step us */ timediff_t step_us; /* microseconds between token increases */ - curl_off_t tokens; /* tokens available in the next second */ + int64_t tokens; /* tokens available in the next second */ timediff_t spare_us; /* microseconds unaffecting tokens */ struct curltime ts; /* time of the last update */ BIT(blocked); /* blocking sets available tokens to 0 */ }; void Curl_rlimit_init(struct Curl_rlimit *r, - curl_off_t rate_per_s, - curl_off_t burst_per_s, + int64_t rate_per_sec, + int64_t burst_per_sec, const struct curltime *pts); -/* Start ratelimiting with the given timestamp. Resets available tokens. */ -void Curl_rlimit_start(struct Curl_rlimit *r, const struct curltime *pts); +/* Start ratelimiting with the given timestamp. Resets available tokens. + * `total_tokens` is either -1 or the number of total tokens expected + * to be consumed. */ +void Curl_rlimit_start(struct Curl_rlimit *r, const struct curltime *pts, + int64_t total_tokens); /* How many milliseconds to wait until token are available again. */ timediff_t Curl_rlimit_wait_ms(struct Curl_rlimit *r, const struct curltime *pts); +/* When the rate limit will update its tokens again */ +timediff_t Curl_rlimit_next_step_ms(struct Curl_rlimit *r, + const struct curltime *pts); + /* Return if rate limiting of tokens is active */ bool Curl_rlimit_active(struct Curl_rlimit *r); bool Curl_rlimit_is_blocked(struct Curl_rlimit *r); +int64_t Curl_rlimit_per_step(struct Curl_rlimit *r); /* Return how many tokens are available to spend, may be negative */ -curl_off_t Curl_rlimit_avail(struct Curl_rlimit *r, - const struct curltime *pts); +int64_t Curl_rlimit_avail(struct Curl_rlimit *r, + const struct curltime *pts); -/* Drain tokens from the ratelimit, return how many are now available. */ +/* Drain tokens from the ratelimit, give an estimate of how many tokens + * remain to be drained in the future (-1 for unknown). */ void Curl_rlimit_drain(struct Curl_rlimit *r, size_t tokens, const struct curltime *pts); diff --git a/lib/sendf.c b/lib/sendf.c index 402f9499f6..2cd62efb48 100644 --- a/lib/sendf.c +++ b/lib/sendf.c @@ -169,6 +169,7 @@ static size_t get_max_body_write_len(struct Curl_easy *data, curl_off_t limit) struct cw_download_ctx { struct Curl_cwriter super; BIT(started_response); + BIT(started_body); }; /* Download client writer in phase CURL_CW_PROTOCOL that @@ -185,7 +186,6 @@ static CURLcode cw_download_write(struct Curl_easy *data, if(!ctx->started_response && !(type & (CLIENTWRITE_INFO | CLIENTWRITE_CONNECT))) { Curl_pgrsTime(data, TIMER_STARTTRANSFER); - Curl_rlimit_start(&data->progress.dl.rlimit, Curl_pgrs_now(data)); ctx->started_response = TRUE; } @@ -198,6 +198,13 @@ static CURLcode cw_download_write(struct Curl_easy *data, return result; } + if(!ctx->started_body && + !(type & (CLIENTWRITE_INFO | CLIENTWRITE_CONNECT))) { + Curl_rlimit_start(&data->progress.dl.rlimit, Curl_pgrs_now(data), + data->req.size); + ctx->started_body = TRUE; + } + /* Here, we deal with REAL BODY bytes. All filtering and transfer * encodings have been applied and only the true content, e.g. BODY, * bytes are passed here. @@ -1189,7 +1196,7 @@ CURLcode Curl_client_read(struct Curl_easy *data, char *buf, size_t blen, DEBUGASSERT(data->req.reader_stack); } if(!data->req.reader_started) { - Curl_rlimit_start(&data->progress.ul.rlimit, Curl_pgrs_now(data)); + Curl_rlimit_start(&data->progress.ul.rlimit, Curl_pgrs_now(data), -1); data->req.reader_started = TRUE; } diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index 87db325770..1d17d4a832 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -231,7 +231,8 @@ struct h3_stream_ctx { size_t sendbuf_len_in_flight; /* sendbuf amount "in flight" */ 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 rx_offset; /* current receive offset */ + uint64_t rx_offset_max; /* allowed receive offset */ 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) */ @@ -273,6 +274,9 @@ static CURLcode h3_data_setup(struct Curl_cfilter *cf, return CURLE_OUT_OF_MEMORY; stream->id = -1; + stream->rx_offset = 0; + stream->rx_offset_max = H3_STREAM_WINDOW_SIZE_INITIAL; + /* on send, we control how much we put into the buffer */ Curl_bufq_initp(&stream->sendbuf, &ctx->stream_bufcp, H3_STREAM_SEND_CHUNKS, BUFQ_OPT_NONE); @@ -610,20 +614,15 @@ static int cb_recv_stream_data(ngtcp2_conn *tconn, uint32_t flags, { struct Curl_cfilter *cf = user_data; struct cf_ngtcp2_ctx *ctx = cf->ctx; - nghttp3_ssize nconsumed; + nghttp3_ssize rc; + uint64_t nconsumed; int fin = (flags & NGTCP2_STREAM_DATA_FLAG_FIN) ? 1 : 0; struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data); (void)offset; - nconsumed = - nghttp3_conn_read_stream(ctx->h3conn, stream_id, buf, buflen, fin); - if(!data) - data = CF_DATA_CURRENT(cf); - if(data) - CURL_TRC_CF(data, cf, "[%" PRId64 "] read_stream(len=%zu) -> %zd", - stream_id, buflen, nconsumed); - if(nconsumed < 0) { - struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data); + rc = nghttp3_conn_read_stream(ctx->h3conn, stream_id, buf, buflen, fin); + if(rc < 0) { if(data && stream) { CURL_TRC_CF(data, cf, "[%" PRId64 "] error on known stream, " "reset=%d, closed=%d", @@ -631,13 +630,18 @@ static int cb_recv_stream_data(ngtcp2_conn *tconn, uint32_t flags, } return NGTCP2_ERR_CALLBACK_FAILURE; } - - /* number of bytes inside buflen which consists of framing overhead - * including QPACK HEADERS. In other words, it does not consume payload of - * DATA frame. */ - ngtcp2_conn_extend_max_stream_offset(tconn, stream_id, (uint64_t)nconsumed); - ngtcp2_conn_extend_max_offset(tconn, (uint64_t)nconsumed); - + nconsumed = (uint64_t)rc; + if(nconsumed) { + /* number of bytes inside buflen which consists of framing overhead + * including QPACK HEADERS. In other words, it does not consume payload of + * DATA frame. */ + ngtcp2_conn_extend_max_stream_offset(tconn, stream_id, nconsumed); + ngtcp2_conn_extend_max_offset(tconn, nconsumed); + if(stream) { + stream->rx_offset += nconsumed; + stream->rx_offset_max += nconsumed; + } + } return 0; } @@ -1044,57 +1048,50 @@ 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, +static void cf_ngtcp2_upd_rx_win(struct Curl_cfilter *cf, struct Curl_easy *data, struct h3_stream_ctx *stream) { struct cf_ngtcp2_ctx *ctx = cf->ctx; - curl_off_t avail; - uint64_t ack_len = 0; - - /* How many byte to ack on the stream? */ + uint64_t cur_win, wanted_win = H3_STREAM_WINDOW_SIZE_MAX; /* how much does rate limiting allow us to acknowledge? */ - avail = Curl_rlimit_avail(&data->progress.dl.rlimit, - Curl_pgrs_now(data)); - if(avail == CURL_OFF_T_MAX) { /* no rate limit, ack all */ - ack_len = stream->download_unacked; - } - else if(avail > 0) { - ack_len = CURLMIN(stream->download_unacked, (uint64_t)avail); + if(Curl_rlimit_active(&data->progress.dl.rlimit)) { + int64_t avail; + + /* start rate limit updates only after first bytes arrived */ + if(!stream->rx_offset) + return; + + avail = Curl_rlimit_avail(&data->progress.dl.rlimit, + Curl_pgrs_now(data)); + if(avail <= 0) { + /* nothing available, do not extend the rx offset */ + CURL_TRC_CF(data, cf, "[%" PRId64 "] dl rate limit exhausted (%" PRId64 + " tokens)", stream->id, avail); + return; + } + wanted_win = CURLMIN((uint64_t)avail, H3_STREAM_WINDOW_SIZE_MAX); } - if(ack_len) { - CURL_TRC_CF(data, cf, "[%" PRId64 "] ACK %" PRIu64 - "/%" PRIu64 " bytes of DATA", stream->id, - ack_len, stream->download_unacked); - ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream->id, ack_len); - stream->download_unacked -= ack_len; + if(stream->rx_offset_max < stream->rx_offset) { + DEBUGASSERT(0); + return; } + cur_win = stream->rx_offset_max - stream->rx_offset; - cf_ngtcp2_stream_update_window(cf, data, stream); + if(wanted_win > cur_win) { + uint64_t delta = wanted_win - cur_win; + + if(UINT64_MAX - delta < stream->rx_offset_max) + delta = UINT64_MAX - stream->rx_offset_max; + if(delta) { + CURL_TRC_CF(data, cf, "[%" PRId64 "] rx window, extend by %" PRIu64 + " bytes", stream->id, delta); + stream->rx_offset_max += delta; + ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream->id, delta); + } + } } static int cb_h3_recv_data(nghttp3_conn *conn, int64_t stream3_id, @@ -1113,15 +1110,15 @@ static int cb_h3_recv_data(nghttp3_conn *conn, int64_t stream3_id, return NGHTTP3_ERR_CALLBACK_FAILURE; h3_xfer_write_resp(cf, data, stream, (const char *)buf, blen, FALSE); - CURL_TRC_CF(data, cf, "[%" PRId64 "] DATA len=%zu", stream->id, blen); ngtcp2_conn_extend_max_offset(ctx->qconn, blen); - if(UINT64_MAX - blen < stream->download_unacked) - stream->download_unacked = UINT64_MAX; /* unlikely */ - else - stream->download_unacked += blen; + stream->rx_offset += blen; + if(stream->rx_offset_max < stream->rx_offset) + stream->rx_offset_max = stream->rx_offset; - cf_ngtcp2_ack_stream(cf, data, stream); + CURL_TRC_CF(data, cf, "[%" PRId64 "] DATA len=%zu, rx win=%" PRId64, + stream->id, blen, stream->rx_offset_max - stream->rx_offset); + cf_ngtcp2_upd_rx_win(cf, data, stream); return 0; } @@ -1131,13 +1128,18 @@ static int cb_h3_deferred_consume(nghttp3_conn *conn, int64_t stream3_id, { struct Curl_cfilter *cf = user_data; struct cf_ngtcp2_ctx *ctx = cf->ctx; + struct Curl_easy *data = stream_user_data; + struct h3_stream_ctx *stream = H3_STREAM_CTX(ctx, data); (void)conn; - (void)stream_user_data; /* nghttp3 has consumed bytes on the QUIC stream and we need to * tell the QUIC connection to increase its flow control */ ngtcp2_conn_extend_max_stream_offset(ctx->qconn, stream3_id, consumed); ngtcp2_conn_extend_max_offset(ctx->qconn, consumed); + if(stream) { + stream->rx_offset += consumed; + stream->rx_offset_max += consumed; + } return 0; } @@ -1418,7 +1420,7 @@ static CURLcode cf_ngtcp2_recv(struct Curl_cfilter *cf, struct Curl_easy *data, goto out; } - cf_ngtcp2_ack_stream(cf, data, stream); + cf_ngtcp2_upd_rx_win(cf, data, stream); /* first check for results/closed already known without touching * the connection. For an already failed/closed stream, errors on @@ -1669,7 +1671,7 @@ static CURLcode h3_stream_open(struct Curl_cfilter *cf, goto out; } - cf_ngtcp2_stream_update_window(cf, data, stream); + cf_ngtcp2_upd_rx_win(cf, data, stream); if(Curl_trc_is_verbose(data)) { infof(data, "[HTTP/3] [%" PRId64 "] OPENED stream for %s", diff --git a/tests/http/scorecard.py b/tests/http/scorecard.py index 621d437454..6fe6d3c9ba 100644 --- a/tests/http/scorecard.py +++ b/tests/http/scorecard.py @@ -64,11 +64,34 @@ class Card: if val is None or val < 0: return '--' if val >= (1024*1024): - return f'{val/(1024*1024):0.000f} MB/s' + return f'{val/(1024*1024):.3g} MB/s' elif val >= 1024: - return f'{val / 1024:0.000f} KB/s' + return f'{val / 1024:.3g} KB/s' else: - return f'{val:0.000f} B/s' + return f'{val:.3g} B/s' + + @classmethod + def fmt_speed(cls, val): + if val is None or val < 0: + return '--' + if val >= (10*1024*1024): + return f'{(val/(1024*1024)):.3f} MB/s' + elif val >= (10*1024): + return f'{val/1024:.3f} KB/s' + else: + return f'{val:.3f} B/s' + + @classmethod + def fmt_speed_result(cls, val, limit): + if val is None or val < 0: + return '--' + pct = ((val / limit) * 100) - 100 + if val >= (10*1024*1024): + return f'{(val/(1024*1024)):.3f} MB/s, {pct:+.1f}%' + elif val >= (10*1024): + return f'{val/1024:.3f} KB/s, {pct:+.1f}%' + else: + return f'{val:.3f} B/s, {pct:+.1f}%' @classmethod def fmt_reqs(cls, val): @@ -87,6 +110,19 @@ class Card: cell['errors'] = errors return cell + @classmethod + def mk_speed_cell(cls, samples, profiles, errors, limit): + val = mean(samples) if len(samples) else -1 + cell = { + 'val': val, + 'sval': Card.fmt_speed_result(val, limit) if val >= 0 else '--', + } + if len(profiles): + cell['stats'] = RunProfile.AverageStats(profiles) + if len(errors): + cell['errors'] = errors + return cell + @classmethod def mk_reqs_cell(cls, samples, profiles, errors): val = mean(samples) if len(samples) else -1 @@ -211,7 +247,23 @@ class ScoreRunner: self._upload_parallel = upload_parallel self._with_flame = with_flame self._socks_args = socks_args + self._limit_rate_num = 0 self._limit_rate = limit_rate + if self._limit_rate: + m = re.match(r'(\d+(\.\d+)?)([gmkb])?', self._limit_rate.lower()) + if not m: + raise Exception(f'unrecognised limit-rate: {self._limit_rate}') + self._limit_rate_num = float(m.group(1)) + if m.group(3) == 'g': + self._limit_rate_num *= (1024*1024*1024) + elif m.group(3) == 'm': + self._limit_rate_num *= (1024*1024) + elif m.group(3) == 'k': + self._limit_rate_num *= (1024) + elif m.group(3) == 'b': + pass + else: + raise Exception(f'unrecognised limit-rate: {self._limit_rate}') self.suppress_cl = suppress_cl def info(self, msg): @@ -306,11 +358,18 @@ class ScoreRunner: err = self._check_downloads(r, count) if err: errors.append(err) + elif self._limit_rate: + total_speed = sum([s['speed_download'] for s in r.stats]) + samples.append(total_speed / len(r.stats)) + profiles.append(r.profile) else: total_size = sum([s['size_download'] for s in r.stats]) samples.append(total_size / r.duration.total_seconds()) profiles.append(r.profile) - return Card.mk_mbs_cell(samples, profiles, errors) + if self._limit_rate: + return Card.mk_speed_cell(samples, profiles, errors, self._limit_rate_num) + else: + return Card.mk_mbs_cell(samples, profiles, errors) def dl_serial(self, url: str, count: int, nsamples: int = 1): samples = [] @@ -328,11 +387,18 @@ class ScoreRunner: err = self._check_downloads(r, count) if err: errors.append(err) + elif self._limit_rate: + total_speed = sum([s['speed_download'] for s in r.stats]) + samples.append(total_speed / len(r.stats)) + profiles.append(r.profile) else: total_size = sum([s['size_download'] for s in r.stats]) samples.append(total_size / r.duration.total_seconds()) profiles.append(r.profile) - return Card.mk_mbs_cell(samples, profiles, errors) + if self._limit_rate: + return Card.mk_speed_cell(samples, profiles, errors, self._limit_rate_num) + else: + return Card.mk_mbs_cell(samples, profiles, errors) def dl_parallel(self, url: str, count: int, nsamples: int = 1): samples = [] @@ -355,11 +421,18 @@ class ScoreRunner: err = self._check_downloads(r, count) if err: errors.append(err) + elif self._limit_rate: + total_speed = sum([s['speed_download'] for s in r.stats]) + samples.append(total_speed / len(r.stats)) + profiles.append(r.profile) else: total_size = sum([s['size_download'] for s in r.stats]) samples.append(total_size / r.duration.total_seconds()) profiles.append(r.profile) - return Card.mk_mbs_cell(samples, profiles, errors) + if self._limit_rate: + return Card.mk_speed_cell(samples, profiles, errors, self._limit_rate_num) + else: + return Card.mk_mbs_cell(samples, profiles, errors) def downloads(self, count: int, fsizes: List[int], meta: Dict[str, Any]) -> Dict[str, Any]: nsamples = meta['samples'] @@ -370,7 +443,10 @@ class ScoreRunner: if count > 1: cols.append(f'serial({count})') if count > 1: - cols.append(f'parallel({count}x{max_parallel})') + if max_parallel == 1: + cols.append(f'serial({count})') + else: + cols.append(f'parallel({count}x{max_parallel})') rows = [] for fsize in fsizes: row = [{ @@ -387,7 +463,10 @@ class ScoreRunner: row.append(self.dl_parallel(url=url, count=count, nsamples=nsamples)) rows.append(row) self.info('done.\n') - title = f'Downloads from {meta["server"]}' + if self._limit_rate: + title = f'Download Speed ({self.protocol}), limit={Card.fmt_speed(self._limit_rate_num)}, from {meta["server"]}' + else: + title = f'Downloads ({self.protocol})from {meta["server"]}' if self._socks_args: title += f' via {self._socks_args}' return { diff --git a/tests/http/test_02_download.py b/tests/http/test_02_download.py index 269ff6cf26..bb0f2f31f1 100644 --- a/tests/http/test_02_download.py +++ b/tests/http/test_02_download.py @@ -395,20 +395,18 @@ class TestDownload: # speed limited download @pytest.mark.parametrize("proto", Env.http_protos()) def test_02_24_speed_limit(self, env: Env, httpd, nghttpx, proto): + if proto == 'h3' and not env.curl_uses_lib('ngtcp2'): + pytest.skip("precise h3 rate limits only with ngtcp2") count = 1 url = f'https://{env.authority_for(env.domain1, proto)}/data-1m' curl = CurlClient(env=env) - speed_limit = 256 * 1024 + speed_limit = 512 * 1024 r = curl.http_download(urls=[url], alpn_proto=proto, extra_args=[ '--limit-rate', f'{speed_limit}' ]) r.check_response(count=count, http_status=200) dl_speed = r.stats[0]['speed_download'] - # speed limit is only exact on long durations. Ideally this transfer - # would take 4 seconds, but it may end just after 3 because then - # we have downloaded the rest and will not wait for the rate - # limit to increase again. - assert dl_speed <= ((1024*1024)/3), f'{r.stats[0]}' + assert dl_speed <= (speed_limit * 1.1), f'{r.stats[0]}' # make extreme parallel h2 upgrades, check invalid conn reuse # before protocol switch has happened diff --git a/tests/unit/unit3216.c b/tests/unit/unit3216.c index f65355f362..4c7b276229 100644 --- a/tests/unit/unit3216.c +++ b/tests/unit/unit3216.c @@ -58,9 +58,9 @@ static CURLcode test_unit3216(const char *arg) ts.tv_usec += 1000; /* 1ms */ Curl_rlimit_drain(&r, 3, &ts); fail_unless(Curl_rlimit_avail(&r, &ts) == -1, "drain to -1"); - fail_unless(Curl_rlimit_wait_ms(&r, &ts) == 999, "wait 999ms"); + fail_unless(Curl_rlimit_wait_ms(&r, &ts) == 1099, "wait 1099ms"); ts.tv_usec += 1000; /* 1ms */ - fail_unless(Curl_rlimit_wait_ms(&r, &ts) == 998, "wait 998ms"); + fail_unless(Curl_rlimit_wait_ms(&r, &ts) == 1098, "wait 1098ms"); ts.tv_sec += 1; fail_unless(Curl_rlimit_avail(&r, &ts) == 9, "10 inc per sec"); ts.tv_sec += 1;