progress: fx CURLINFO time reporting

Whack the times reported for a transfer (see
https://curl.se/libcurl/c/curl_easy_getinfo.html#TIMES) into order for
all variations of up-/download, http/ftp etc. Make sure they are
reported in the documented order.

There is still the *possibility* of PRETRANSFER being longer then
POSTTRANSFER, if a server sends a response before an upload is done.
POST is the time the first response byte is received, and PRE is the
time the last byte was sent by curl.

This may happen with more likelihood on HTTP/2 and 3 for a server
rejected upload. But for successful uploads, the answer will almost over
come afterwards.

Undo the previous twists in lib500.c tests, adjust pytest timeline
checks.

Fixes #21828
Reported-by: BazaarAcc32 on github
Closes #21843
This commit is contained in:
Stefan Eissing 2026-06-02 11:10:10 +02:00 committed by Daniel Stenberg
parent 317bf7e8a8
commit fbcf10ab84
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
8 changed files with 159 additions and 109 deletions

View file

@ -106,20 +106,56 @@ static const struct curltime *multi_now(struct Curl_multi *multi)
return &multi->now;
}
/* function pointer called once when switching TO a state */
typedef void (*init_multistate_func)(struct Curl_easy *data);
/* function pointer called once when entering a state */
typedef void (*mstate_enter_func)(struct Curl_easy *data,
CURLMstate from_state);
/* called in DID state, before PERFORMING state */
static void before_perform(struct Curl_easy *data)
static void mstate_enter_connect(struct Curl_easy *data,
CURLMstate from_state)
{
data->req.chunk = FALSE;
Curl_pgrsTime(data, TIMER_PRETRANSFER);
(void)from_state;
Curl_init_CONNECT(data);
}
static void init_completed(struct Curl_easy *data)
static void mstate_enter_did(struct Curl_easy *data,
CURLMstate from_state)
{
/* this is a completed transfer */
(void)from_state;
data->req.chunk = FALSE;
Curl_pgrsTime(data, TIMER_PRETRANSFER);
if(!CURL_REQ_WANT_SEND(data))
Curl_pgrsTime(data, TIMER_POSTRANSFER);
}
static void mstate_enter_done(struct Curl_easy *data,
CURLMstate from_state)
{
(void)from_state;
CURLM_NTFY(data, CURLMNOTIFY_EASY_DONE);
}
static void mstate_enter_completed(struct Curl_easy *data,
CURLMstate from_state)
{
/* we sometimes directly jump to COMPLETED, trigger things
* we then missed. */
if(from_state < MSTATE_DID) {
Curl_pgrsTime(data, TIMER_PRETRANSFER);
Curl_pgrsTime(data, TIMER_POSTRANSFER);
Curl_pgrsTime(data, TIMER_STARTTRANSFER);
}
Curl_pgrsCompleted(data);
if(from_state < MSTATE_DONE)
CURLM_NTFY(data, CURLMNOTIFY_EASY_DONE);
/* changing to COMPLETED means it is in process and needs to go */
DEBUGASSERT(Curl_uint32_bset_contains(&data->multi->process, data->mid));
Curl_uint32_bset_remove(&data->multi->process, data->mid);
Curl_uint32_bset_remove(&data->multi->pending, data->mid); /* to be sure */
if(Curl_uint32_bset_empty(&data->multi->process)) {
/* free the transfer buffer when we have no more active transfers */
multi_xfer_bufs_free(data->multi);
}
/* Important: reset the conn pointer so that we do not point to memory
that could be freed anytime */
Curl_detach_connection(data);
@ -134,23 +170,23 @@ static void mstate(struct Curl_easy *data, CURLMstate state
)
{
CURLMstate oldstate = data->mstate;
static const init_multistate_func finit[MSTATE_LAST] = {
NULL, /* INIT */
NULL, /* PENDING */
NULL, /* SETUP */
Curl_init_CONNECT, /* CONNECT */
NULL, /* CONNECTING */
NULL, /* PROTOCONNECT */
NULL, /* PROTOCONNECTING */
NULL, /* DO */
NULL, /* DOING */
NULL, /* DOING_MORE */
before_perform, /* DID */
NULL, /* PERFORMING */
NULL, /* RATELIMITING */
NULL, /* DONE */
init_completed, /* COMPLETED */
NULL /* MSGSENT */
static const mstate_enter_func state_enter[MSTATE_LAST] = {
NULL, /* INIT */
NULL, /* PENDING */
NULL, /* SETUP */
mstate_enter_connect, /* CONNECT */
NULL, /* CONNECTING */
NULL, /* PROTOCONNECT */
NULL, /* PROTOCONNECTING */
NULL, /* DO */
NULL, /* DOING */
NULL, /* DOING_MORE */
mstate_enter_did, /* DID */
NULL, /* PERFORMING */
NULL, /* RATELIMITING */
mstate_enter_done, /* DONE */
mstate_enter_completed, /* COMPLETED */
NULL /* MSGSENT */
};
if(oldstate == state)
@ -166,32 +202,8 @@ static void mstate(struct Curl_easy *data, CURLMstate state
/* really switching state */
data->mstate = state;
switch(state) {
case MSTATE_DONE:
CURLM_NTFY(data, CURLMNOTIFY_EASY_DONE);
break;
case MSTATE_COMPLETED:
/* we sometimes directly jump to COMPLETED, trigger also a notification
* in that case. */
if(oldstate < MSTATE_DONE)
CURLM_NTFY(data, CURLMNOTIFY_EASY_DONE);
/* changing to COMPLETED means it is in process and needs to go */
DEBUGASSERT(Curl_uint32_bset_contains(&data->multi->process, data->mid));
Curl_uint32_bset_remove(&data->multi->process, data->mid);
Curl_uint32_bset_remove(&data->multi->pending, data->mid); /* to be sure */
if(Curl_uint32_bset_empty(&data->multi->process)) {
/* free the transfer buffer when we have no more active transfers */
multi_xfer_bufs_free(data->multi);
}
break;
default:
break;
}
/* if this state has an init-function, run it */
if(finit[state])
finit[state](data);
if(state_enter[state])
state_enter[state](data, oldstate);
}
#ifndef DEBUGBUILD

View file

@ -240,6 +240,30 @@ void Curl_pgrsSendPause(struct Curl_easy *data, bool enable)
}
}
#ifdef CURLVERBOSE
static const char * const pgrs_timer_names[] = {
"PGRS-NONE",
"PGRS-STARTOP",
"PGRS-STARTSINGLE",
"PGRS-POSTQUEUE",
"PGRS-NAMELOOKUP",
"PGRS-CONNECT",
"PGRS-APPCONNECT",
"PGRS-PRETRANSFER",
"PGRS-STARTTRANSFER",
"PGRS-POSTRANSFER",
"PGRS-STARTACCEPT",
"PGRS-REDIRECT",
};
static const char *pgrs_timer_name(timerid timer)
{
if((size_t)timer < CURL_ARRAYSIZE(pgrs_timer_names))
return pgrs_timer_names[(size_t)timer];
return "?";
}
#endif /* CURLVERBOSE */
/*
* Curl_pgrsTimeWas(). Store the timestamp time at the given label.
*/
@ -285,7 +309,6 @@ void Curl_pgrsTimeWas(struct Curl_easy *data, timerid timer,
delta = &data->progress.t_pretransfer;
break;
case TIMER_STARTTRANSFER:
delta = &data->progress.t_starttransfer;
/* prevent updating t_starttransfer unless:
* 1. this is the first time we are setting t_starttransfer
* 2. a redirect has occurred since the last time t_starttransfer was set
@ -293,12 +316,12 @@ void Curl_pgrsTimeWas(struct Curl_easy *data, timerid timer,
* changing the t_starttransfer time.
*/
if(data->progress.is_t_startransfer_set) {
CURL_TRC_M(data, "[%s] ignored", pgrs_timer_name(timer));
return;
}
else {
data->progress.is_t_startransfer_set = TRUE;
break;
}
data->progress.is_t_startransfer_set = TRUE;
delta = &data->progress.t_starttransfer;
break;
case TIMER_POSTRANSFER:
delta = &data->progress.t_posttransfer;
break;
@ -314,7 +337,11 @@ void Curl_pgrsTimeWas(struct Curl_easy *data, timerid timer,
if(us < 1)
us = 1; /* make sure at least one microsecond passed */
*delta += us;
CURL_TRC_M(data, "[%s] added %" FMT_TIMEDIFF_T "ns",
pgrs_timer_name(timer), us);
}
else
CURL_TRC_M(data, "[%s] set", pgrs_timer_name(timer));
}
/*
@ -703,3 +730,9 @@ void Curl_pgrsUpdate_nometer(struct Curl_easy *data)
{
(void)progress_calc(data, Curl_pgrs_now(data));
}
void Curl_pgrsCompleted(struct Curl_easy *data)
{
struct Progress * const p = &data->progress;
p->timespent = curlx_ptimediff_us(Curl_pgrs_now(data), &p->start);
}

View file

@ -83,4 +83,6 @@ void Curl_pgrsTimeWas(struct Curl_easy *data, timerid timer,
void Curl_pgrsEarlyData(struct Curl_easy *data, curl_off_t sent);
void Curl_pgrsCompleted(struct Curl_easy *data);
#endif /* HEADER_CURL_PROGRESS_H */

View file

@ -271,7 +271,8 @@ static CURLcode req_set_upload_done(struct Curl_easy *data)
data->req.upload_done = TRUE;
CURL_REQ_CLEAR_SEND(data);
Curl_pgrsTime(data, TIMER_POSTRANSFER);
if(data->mstate >= MSTATE_DID)
Curl_pgrsTime(data, TIMER_POSTRANSFER);
Curl_creader_done(data, data->req.upload_aborted);
if(data->req.upload_aborted) {

View file

@ -183,7 +183,8 @@ static CURLcode cw_download_write(struct Curl_easy *data,
bool is_connect = !!(type & CLIENTWRITE_CONNECT);
if(!ctx->started_response &&
!(type & (CLIENTWRITE_INFO | CLIENTWRITE_CONNECT))) {
!(type & CLIENTWRITE_CONNECT) &&
(!(type & CLIENTWRITE_INFO) || data->req.upload_done)) {
Curl_pgrsTime(data, TIMER_STARTTRANSFER);
ctx->started_response = TRUE;
}

View file

@ -540,23 +540,8 @@ class ExecResult:
ref_tl += ['time_namelookup', 'time_connect']
if url.startswith('https:'):
ref_tl += ['time_appconnect']
# what kind of transfer was it?
if s['size_upload'] == 0 and s['size_download'] > 0:
# this is a download
dl_tl = ['time_pretransfer']
if s['size_request'] > 0:
dl_tl = ['time_posttransfer'] + dl_tl
ref_tl += dl_tl
# the first byte of the response may arrive before we
# track the other times when the client is slow (CI).
somewhere_keys.extend(['time_starttransfer'])
elif s['size_upload'] > 0 and s['size_download'] == 0:
# this is an upload
ul_tl = ['time_pretransfer', 'time_posttransfer']
ref_tl += ul_tl
else:
# could be a 0-length upload or 0-length download, not sure
exact_match = False
ref_tl += ['time_pretransfer', 'time_posttransfer']
somewhere_keys.extend(['time_starttransfer'])
# always there at the end
ref_tl += ['time_total']

View file

@ -127,10 +127,7 @@ static CURLcode test_lib500(const char *URL)
(time_pretransfer / 1000000),
(long)(time_pretransfer % 1000000));
}
if(time_posttransfer > time_pretransfer) {
/* counter-intuitive: on a GET request, all bytes are sent *before*
* PRETRANSFER happens. Thus POSTTRANSFER has to be smaller.
* The reverse would be true for a POST/PUT. */
if(time_pretransfer > time_posttransfer) {
curl_mfprintf(moo, "pretransfer vs posttransfer: %"
CURL_FORMAT_CURL_OFF_T
".%06ld %" CURL_FORMAT_CURL_OFF_T ".%06ld\n",

View file

@ -25,6 +25,25 @@
#include "urldata.h"
#include "progress.h"
static CURLcode t1399_setup(struct Curl_easy **easy)
{
CURLcode result = CURLE_OK;
global_init(CURL_GLOBAL_ALL);
*easy = curl_easy_init();
if(!*easy) {
curl_global_cleanup();
return CURLE_OUT_OF_MEMORY;
}
return result;
}
static void t1399_stop(struct Curl_easy *easy)
{
curl_easy_cleanup(easy);
curl_global_cleanup();
}
/*
* Invoke Curl_pgrsTime for TIMER_STARTSINGLE to trigger the behavior that
* manages is_t_startransfer_set, but fake the t_startsingle time for purposes
@ -72,45 +91,45 @@ static void expect_timer_seconds(struct Curl_easy *data, int seconds)
* be 3 seconds. */
static CURLcode test_unit1399(const char *arg)
{
UNITTEST_BEGIN_SIMPLE
struct Curl_easy data;
struct Curl_easy *data;
struct curltime now = curlx_now();
data.multi = NULL;
data.progress.now = now;
data.progress.t_nslookup = 0;
data.progress.t_connect = 0;
data.progress.t_appconnect = 0;
data.progress.t_pretransfer = 0;
data.progress.t_starttransfer = 0;
data.progress.t_redirect = 0;
data.progress.start.tv_sec = now.tv_sec - 2;
data.progress.start.tv_usec = now.tv_usec;
fake_t_startsingle_time(&data, now, -2);
UNITTEST_BEGIN(t1399_setup(&data))
Curl_pgrsTime(&data, TIMER_NAMELOOKUP);
Curl_pgrsTime(&data, TIMER_CONNECT);
Curl_pgrsTime(&data, TIMER_APPCONNECT);
Curl_pgrsTime(&data, TIMER_PRETRANSFER);
Curl_pgrsTime(&data, TIMER_STARTTRANSFER);
data->multi = NULL;
data->progress.now = now;
data->progress.t_nslookup = 0;
data->progress.t_connect = 0;
data->progress.t_appconnect = 0;
data->progress.t_pretransfer = 0;
data->progress.t_starttransfer = 0;
data->progress.t_redirect = 0;
data->progress.start.tv_sec = now.tv_sec - 2;
data->progress.start.tv_usec = now.tv_usec;
fake_t_startsingle_time(data, now, -2);
expect_timer_seconds(&data, 2);
Curl_pgrsTime(data, TIMER_NAMELOOKUP);
Curl_pgrsTime(data, TIMER_CONNECT);
Curl_pgrsTime(data, TIMER_APPCONNECT);
Curl_pgrsTime(data, TIMER_PRETRANSFER);
Curl_pgrsTime(data, TIMER_STARTTRANSFER);
expect_timer_seconds(data, 2);
/* now simulate the redirect */
data.progress.t_redirect = data.progress.t_starttransfer + 1;
fake_t_startsingle_time(&data, now, -1);
data->progress.t_redirect = data->progress.t_starttransfer + 1;
fake_t_startsingle_time(data, now, -1);
Curl_pgrsTime(&data, TIMER_NAMELOOKUP);
Curl_pgrsTime(&data, TIMER_CONNECT);
Curl_pgrsTime(&data, TIMER_APPCONNECT);
Curl_pgrsTime(&data, TIMER_PRETRANSFER);
Curl_pgrsTime(data, TIMER_NAMELOOKUP);
Curl_pgrsTime(data, TIMER_CONNECT);
Curl_pgrsTime(data, TIMER_APPCONNECT);
Curl_pgrsTime(data, TIMER_PRETRANSFER);
/* ensure t_starttransfer is only set on the first invocation by attempting
* to set it twice */
Curl_pgrsTime(&data, TIMER_STARTTRANSFER);
Curl_pgrsTime(&data, TIMER_STARTTRANSFER);
Curl_pgrsTime(data, TIMER_STARTTRANSFER);
Curl_pgrsTime(data, TIMER_STARTTRANSFER);
expect_timer_seconds(&data, 3);
expect_timer_seconds(data, 3);
UNITTEST_END_SIMPLE
UNITTEST_END(t1399_stop(data))
}