time-keeping: keep timestamp in multi, always update

Always use curlx_now() when calling Curl_pgrs_now(data). Tests with the
"manual" updates to now proved differ more then 100ms in parallel testing.

Add `curlx_nowp()` to set current time into a struct curltime.
Add `curlx_ptimediff_ms() and friends, passing pointers.

Update documentation.

Closes #19998
This commit is contained in:
Stefan Eissing 2025-12-18 13:55:07 +01:00 committed by Daniel Stenberg
parent 308c347c8b
commit b4be1f271e
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
61 changed files with 471 additions and 502 deletions

View file

@ -11,10 +11,6 @@ time function is `curlx_now()` and it uses a **monotonic** clock on most platfor
ensures that time only ever increases (the timestamps it gives are however not the "real"
world clock).
The simplest handling of transfer time would be to just always call `curlx_now()`. However
there is a performance penalty to that - varying by platform - so this is not a desirable
strategy. Processing thousands of transfers in a loop needs a smarter approach.
## Initial Approach (now historic)
The loop processing functions called `curlx_now()` at the beginning and then passed
@ -34,49 +30,16 @@ in the correct order: *queue -> nameloopup -> connect -> appconnect ->...*.)
The strategy of handling transfer's time is now:
* Keep a "now" timestamp in `data->progress.now`.
* Perform time checks and event recording using `data->progress.now`.
* Set `data->progress.now` at the start of API calls (e.g. `curl_multi_perform()`, etc.).
* Set `data->progress.now` when recorded events happen (for precision).
* Set `data->progress.now` on multi state changes.
* Set `data->progress.now` in `pingpong` timeout handling, since `pingpong` is old and not always non-blocking.
* Keep a "now" timestamp in the multi handle. Keep a fallback "now" timestamp in the easy handle.
* Always use `Curl_pgrs_now(data)` to get the current time of a transfer.
* Do not use `curlx_now()` directly for transfer handling (exceptions apply for loops).
In addition to *setting* `data->progress.now` this timestamp can be *advanced* using 2 new methods:
This has the following advantages:
* `Curl_pgrs_now_at_least(data, &now)`: code that has a "now" timestamp can progress the `data`'s own "now" to be at least as new. If `data->progress.now` is already newer, no change is done. A transfer never goes **back**.
* `Curl_pgrs_now_update(data1, data2)`: update the "now" in `data1` to be at least as new as the one in `data2`. If it already is newer, nothing changes.
* No need to pass a `struct curltime` around or pass a pointer to an outdated timestamp to other functions.
* No need to calculate the exact `now` until it is really used.
* Passing a `const` pointer is better than struct passing. Updating and passing a pointer to the same memory location for all transfers is even better.
### Time Advancing Loops
Caveats:
This advancing is used in the following way in loop like `curl_multi_perform()`:
```C
struct curltime now = curlx_now(); /* start of API call */
forall data in transfers {
Curl_pgrs_set_at_least(data, now);
progress(data); /* may update "now" */
now = data->progress.now;
}
```
Transfers that update their "now" pass that timestamp to the next transfer processed.
### Transfers triggering other transfers
In HTTP/2 and HTTP/3 processing, incoming data causes actions on transfers other than
the calling one. The protocols may receive data for any transfer on the connection and need
to dispatch it:
* a Close/Reset comes in for another transfer. That transfer is marked as "dirty", making sure it is processed in a timely manner.
* Response Data arrives: this data is written out to the client. Before this is done, the "now" timestamp is updated via `Curl_pgrs_now_update(data, calling)` from the "calling" transfer.
## Blocking Operations
We still have places in `libcurl` where we do blocking operations. We should always use `Curl_pgrs_now_set(data)` afterwards since we cannot be sure how much time has passed. Since loop processing passed an updated "now" to the next transfer, a delay due to blocking is passed on.
There are other places where we may lose track of time:
* Cache/Pool Locks: no "now" updates happen after a lock has been acquired. These locks should not be kept for a longer time.
* User Callbacks: no "now" updates happen after callbacks have been invoked. The expectation is that those do not take long.
Should these assumptions prove wrong, we need to add updates.
* do not store the pointer returned by `Curl_pgrs_now(data)` anywhere that outlives the current code invocation.