Open eric-stokes-architect opened 1 year ago
Tokio's timer APIs are intended to be coase-grained. They are very efficient to great / cancel, but are not precise. If you need a high resolution timer, use an OS-specific API and integrate it with TOkio via AsyncFD. There probably are already crates out there.
Sorry, I'm not saying that the timer wakes up at the wrong time, or should be more precise, I'm well aware we're working with course grained timers here. I'm saying that poll ON the timer takes 40 microseconds. Just calling poll, not calling it until it returns Ready, just calling it once, can take from 4 - 40 microseconds.
I know this because I put timing statements in the interval timer's poll function and then ran the above program using my local copy of tokio, I'm sorry I didn't mention that in the bug report, I thought I would let the example program stand for itself.
Can you include a snippet that times includes the poll time by instrumenting the poll start / stop? You can either implement a future manually or use github.com/tokio-rs/tokio-metrics/
Yes, of course. Earlier during a long conversation on discord with Alice, we instrumented various time functions in tokio, here is output I just generated by recompiling against the instrumented tokio.
start Interval::tick
start Interval::poll_tick
start Interval::poll_tick::poll_delay
start Sleep::poll
start TimerEntry::poll_elapsed
check shutdown 133ns
check registered 134ns
poll inner 332ns
TimerEntry::poll_elapsed 9.481µs
finish Sleep::poll 15.024µs
finish Interval::poll_tick::poll_delay 18.625µs
interval reregister 294ns
finish Interval::poll_tick: 24.853µs
finish Interval::tick: 28.9µs
Here is the instrumented Interval::tick method, as an example of how the instrumentation was done.
pub async fn tick(&mut self) -> Instant {
#[cfg(all(tokio_unstable, feature = "tracing"))]
let resource_span = self.resource_span.clone();
#[cfg(all(tokio_unstable, feature = "tracing"))]
let instant = trace::async_op(
|| poll_fn(|cx| self.poll_tick(cx)),
resource_span,
"Interval::tick",
"poll_tick",
false,
);
#[cfg(not(all(tokio_unstable, feature = "tracing")))]
let instant = poll_fn(|cx| {
println!("start Interval::tick");
let st = Instant::now();
let r = self.poll_tick(cx);
println!("finish Interval::tick: {:?}", st.elapsed());
r
});
instant.await
}
And here is an example where we took the early return from poll_tick because the timer isn't ready
start Interval::tick
start Interval::poll_tick
start Interval::poll_tick::poll_delay
start Sleep::poll
start TimerEntry::poll_elapsed
check shutdown 42ns
check registered 2.291µs
poll inner 251ns
TimerEntry::poll_elapsed 7.225µs
finish Sleep::poll 10.075µs
finish Interval::poll_tick::poll_delay 12.922µs
finish Interval::tick: 17.07µs
Version
cargo tree | grep tokio
└── tokio v1.32.0 (/home/eric/proj/tokio/tokio) └── tokio-macros v2.1.0 (proc-macro) (/home/eric/proj/tokio/tokio-macros)Platform Linux kagura.ryu-oh.org 6.4.6-76060406-generic #202307241739~1692717645~22.04~5597803 SMP PREEMPT_DYNAMIC Tue A x86_64 x86_64 x86_64 GNU/Linux
Description
Just calling poll in a timer (interval, sleep, etc) takes between 4 and 40 microseconds.
Minimal example:
test.rs
Cargo.toml
establishing a baseline of task to task communication via a channel
Clock events, tasks and channels are all critical components of modular async applications. Tasks and channels are fast enough to write low latency applications, but at the moment clock events are not. This is very unfortunate, as clock events are very hard to avoid, even if purging them from one's own code were possible, numerous widely used libraries depend on them. Moreover it is very common to find clock events in a select loop where various other IO operations are happening often, and this adds a massive tax to all of those operations, since very time an event in the select is ready this will cause the clock event will be polled again.