tokio-rs / tokio

A runtime for writing reliable asynchronous applications with Rust. Provides I/O, networking, scheduling, timers, ...
https://tokio.rs
MIT License
27.1k stars 2.49k forks source link

time: implicit 1ms delay in `sleep_until` #6866

Open SmnTin opened 1 month ago

SmnTin commented 1 month ago

Version I tested it with 1.35, 1.36, 1.37, and 1.38.

Platform Ubuntu 20.04.6 LTS, Linux 5.4.0, x86_64.

Description

It seems that Tokio adds an implicit delay of 1ms to any sleeping operation, even if the time is paused:

use tokio::time::{Instant, Duration};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    tokio::time::pause();

    let now = Instant::now();
    tokio::time::sleep_until(now).await;
    dbg!(Instant::now() - now); // 1 ms

    let next_at = now + Duration::from_secs(1);
    tokio::time::sleep_until(next_at).await;
    dbg!(Instant::now() - next_at); // 1 ms
}

This behavior is unexpected, to say the least.

Link to the playground

Related to #6390.

Darksonn commented 1 month ago

Tokio's timer is only precise up to 1ms. Any durations less than that are rounded up to the next millisecond.

SmnTin commented 1 month ago

@Darksonn In other words, the duration should be a multiple of 1 ms. Otherwise, it is rounded up. For example, 17ns should be rounded to 1ms, but 0ms should be rounded to 0ms, not 1ms. Moreover, in the second case, 1s is already a multiple of 1ms, yet the time advances for an extra ms. Note that the time is paused, so there shouldn't be any issues with imprecise sleeping.

Darksonn commented 1 month ago

Ah I see where the confusion comes from. The problem is the amount of time it takes from the start of the test until the call to time::pause(). Compare with this version that has no time pass before the test is paused:

use tokio::time::{Instant, Duration};

#[tokio::main(flavor = "current_thread", start_paused = true)]
async fn main() {
    let now = Instant::now();
    tokio::time::sleep_until(now).await;
    dbg!(Instant::now() - now); // 0 ns

    let next_at = now + Duration::from_secs(1);
    tokio::time::sleep_until(next_at).await;
    dbg!(Instant::now() - next_at); // 0 ns
}

It's not durations that are rounded up to the next millisecond. Rather, the deadline of the sleep is rounded up to the next millisecond "tick" since the runtime started. If some time has already passed since the start of the test, then even a sleep of duration zero is rounded up.