rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
99.1k stars 12.79k forks source link

Instant accounts for system sleep on Windows but not on linux #79462

Open mahkoh opened 4 years ago

mahkoh commented 4 years ago

On linux, Instant uses clock_gettime with CLOCK_MONOTONIC which does not count time spent in a sleep state such as hiberation or standby.

On Windows, Instant uses QueryPerformanceCounter which does count time spent in a sleep state:

QueryPerformanceCounter reads the performance counter and returns the total number of ticks that have occurred since the Windows operating system was started, including the time when the machine was in a sleep state such as standby, hibernate, or connected standby.

Neither behavior is currently documented.

Consider the following code (adapted from similar code in Tokio):

use std::time::{Duration, SystemTime, Instant};

fn main() {
    let mut next = Instant::now();
    loop {
        let now = Instant::now();
        if next > now {
            std::thread::sleep(next - now);
        }
        next += Duration::from_secs(1);
        println!("{:?} tick", SystemTime::UNIX_EPOCH.elapsed().unwrap().as_millis());
    }
}

Behavior on Linux: Ticks during sleep are lost.

Behavior on Windows: Ticks during sleep occur in rapid succession after resume.

mahkoh commented 4 years ago

The behavior of CLOCK_MONOTONIC appears to vary between implementations:

[1] During the 4.17 development cycle CLOCK_MONOTONIC was changed to behave like CLOCK_BOOTTIME. But this broke userspace and was reverted. [2] https://lists.freebsd.org/pipermail/freebsd-hackers/2018-June/052899.html [3] https://github.com/openbsd/src/blob/e71a3804998757589450d9364e961ab21db849c9/sys/kern/kern_time.c#L121-L124 [4] http://www.manpagez.com/man/3/clock_gettime/

the8472 commented 4 years ago

From the docs

An instant may jump forwards or experience time dilation (slow down or speed up), but it will never go backwards.

I think this leeway exists to allow whichever timer is the fastest to be used, not the most consistent or human-friendly one.

mahkoh commented 4 years ago

That comment is about NTP.

the8472 commented 4 years ago

The documentation does not restrict itself to that. NTP is only one possible cause for clocks jumping, but not the only one. Users can manually adjust clocks too. Sleep is just another possible source. VM migrations are a thing too.

mahkoh commented 4 years ago

Please spare me this academic citing of the documentation. This issue is about a serious difference in behavior on different platforms. Your pedantic reading would make an implementation that has Instant::new return a constant value valid. This is neither what the authors of the API intended nor how downstream is using it.

the8472 commented 4 years ago

The point is that Instant boils down to rdtsc on many systems (give or take some cleanups/backsliding protection). rdtsc quality has changed with cpu generations. E.g. in some models it did not tick when the cpu was in some sleep states (which would be similar to what you're experiencing) and this changed in other models (see the nonstop_tsc cpu capability, among others).

Your pedantic reading would make an implementation that has Instant::new return a constant value valid.

Indeed. But implementations strive to do better than that on a best-effort basis. That doesn't mean they promise to do better than that. It definitely does not promise wall-time.

This is neither what the authors of the API intended

The intent of Instant is to have a low-overhead, fine-grained elapsed time measurement. Whether elapsed time includes sleeps or not isn't specified. Calling to the system clock that provides wall-time is high-overhead or low-granularity on some platforms.

This issue is about a serious difference in behavior on different platforms.

Well, at least the documentation could be improved.

mahkoh commented 4 years ago

Both CLOCK_MONOTONIC and CLOCK_BOOTTIME have the same associated costs on x86_64 linux. Therefore there is nothing inherent on x86_64 that makes one mode more expensive than the other one. Nobody was talking about wall-time.

the8472 commented 4 years ago

CLOCK_BOOTTIME was introduced in kernel 2.6.39. Rust's minimum supported kernel version was recently bumped to 2.6.32. It might be possible to work around this by probing with different flags, but that would only provide this behavior on a best-effort basis and not guarantee it which means downstream should not be relying on it. And as you mention yourself there's no CLOCK_BOOTTIME for FreeBSD.

So the smallest common denominator is and remains that system suspend may or may not be accounted for.

asnare commented 2 years ago

I just ran into this on macOS, where the implementation is based on mach_absolute_time() and (also) doesn't advance during sleep.

Is there any interest in updating the documentation to specifically mention that measured durations may or may not include time spent during system suspend/sleep? It's clearly a significant difference in behaviour amongst the supported platforms…

DXist commented 1 year ago

I've published boot-time crate that provides Instant struct based on CLOCK_BOOTTIME.