Generalize `#sleep` for monotonic and wall clock

Maybe there could be a Time::Instant type to represent a point in time? Either an abstract struct or a module, so specific clocks (Time::Monotonic, Time::Realtime, …) could inherit most of their implementation, since time operations (add, sub, <=>, to_f, …) don’t depend on the clock.

If you need to use the Linux specific CLOCK_TAI you’d just create a struct, inherit from it, and implement the .now : self method.

Maybe we could have a sleep(until: Time::Instant) and Fiber.timeout(until: Time::Instant) that might work for any clock :thinking:

2 Likes

A generalization might be a good idea. I’m not quite sure how useful it would be because the different clocks generally serve very different purposes.
Time::Instant should work as a name for that.

I don’t think it’s a good idea to integrate wall clock into scheduling primitives.
The wall clock is volatile and might show erratic behaviour sometimes.

Also the behaviour of such a method would not be entirely obvious: Does it convert the wall clock into a monotonic clock right away or does it somehow monitor the wall cloc to trigger when it reached the given instant? The former would probably be better to explicitly convert to monotonic time at the call site. The latter seems way to complicated for a scheduler function.

This reminds me of this thread about how even the monotonic clock might ignore hibernation on certain platforms.

It would never convert a wall time to a monotonic time. It’s impossible anyway, we can’t compare or convert a monotonic time to a wall time and vice versa.

The timer needs to know which clock the expiration is related to and compare the expiration with that clock’s now.

For example timer_create(2) requires to specify the clock id.

What I mean with resolving the wall block is this: when it’s 16:16 right now and I want to sleep until 17:30, that would equal a timeout of 73 minutes.
That of course won’t take into account whether the wall clock changes during that time (it can anticipate expected clock shifts in the time zone, but not arbitrary changes).
But it’s a valid strategy for “sleep until wall clock”.

The alternative is to regularly check the wall clock whether the target time has arrived. That needs to happen relatively frequently for accuracy.
There could still be weird sync issues for example when the scheduled time arrives, but an unrelated change to the local time zone happens before we can check the clock.

There are too many quirks that I think it’s best to avoid wall clock for scheduling. At least for now. We can consider that as a future enhancement if a need arises.

that would equal a timeout of 73 minutes.

Well, it’s not equal. The timer shall be set to “today at 17:30“ and trigger when “today at 17:30“ <= wall clock now, not set to 73 minutes. If anything changes the wall clock (NTP, TZ, DST, …), then the timer might trigger sooner or later than 73 minutes (even immediately).

If the timer is set to 73 minutes from now, then the timer is still “today at 17:30” and again might trigger sooner or later than those 73 minutes.

All that’s needed is to know which clock to ask for “now” when expiring each timer —for efficiency we can keep a collection of timers per clock type.

Note: timers are already absolute internally. The timer is set to Time.monotonic + timeout and then compared against Time.monotonic. Using the wall clock would basically set Time.now + timeout and compare against Time.now instead.

The event loops would also have to setup a timer event per clock type (except io_uring that shall register an event per timer).

Again, Windows is probably going to ruin the point: we can’t specify a clock to CreateWaitableTimer or SetWaitableTimer :enraged_face:

1 Like

With a monotonic clock, it’s trivial to calculate now + span. That defines a clear instant when the timer is supposed to expire. We can pass that to the operating system. There’s no ambiguity.

But the wall clock isn’t monotonic and thus we cannot calculate a clear end time. We can only approximate. Either with a monotizised (is that a word?) now + span, or by regularly checking whether we’ve met the condition. The former has a good chance of being very inaccurate, and the latter depends on check frequency. Which influences efficiency.
How should a good, general purpose implementation for that work?

Waiting for a wall clock timer sounds like a problem that’s best solved in the application domain with application-specific parameters.

1 Like

Waiting for a wall clock timer sounds like a problem that’s best solved in the application domain with application-specific parameters.

I’m in agreement with your conclusion if it’s a case of either/or. It’s interesting to note that Linux syscalls give you the option. It’s also maybe helpful to consider the distinction between sleep and timer methods - for both primitives, Linux provides the option to pass either a wall clock or a monotonic clock.

clock_nanosleep(2) - Linux manual page

The most frequently used syscall sleep() uses the wall clock. IMO, as a systems language, it would be great if Crystal could default to monotonic or be configurable the way the syscalls are.

timer_create() also has its choice of wall clock or monotonic clock:

timer_create(2) - Linux manual page

For my own use cases monotonic time keeping is a hard requirement; a monotonic sleep function on the Fiber would save me from implementing this against raw syscalls which I’ve already mapped out per the above.

In terms of implementation, as I understand it, the kernel code looks for the most reliable hardware source of the monotonic clock which usually defaults to the CPU so they’re able to get away with not having to sleep/wakeup to count-down manually or in a separate thread or something. I haven’t looked at that code and I’m hardly any kind of Linux expert, just sharing research I’ve done for timing sources for my own work. I’m sure Windows exposes a similar API for monotonic primitives but I don’t know the specific calls, unfortunately. fwiw

one last note: I meant to point out some of the debate here around wall-clock vs. monotonic when suspend might be in play for laptops. the kernel differentiates different clocks just for that thing:

   CLOCK_MONOTONIC
          A nonsettable monotonically increasing clock that measures
          time from some unspecified point in the past that does not
          change after system startup.

   CLOCK_PROCESS_CPUTIME_ID (since Linux 2.6.12)
          A clock that measures (user and system) CPU time consumed
          by (all of the threads in) the calling process.

   CLOCK_THREAD_CPUTIME_ID (since Linux 2.6.12)
          A clock that measures (user and system) CPU time consumed
          by the calling thread.

   CLOCK_BOOTTIME (Since Linux 2.6.39)
          Like CLOCK_MONOTONIC, this is a monotonically increasing
          clock.  However, whereas the CLOCK_MONOTONIC clock does not
          measure the time while a system is suspended, the
          CLOCK_BOOTTIME clock does include the time during which the
          system is suspended.  This is useful for applications that
          need to be suspend-aware.  CLOCK_REALTIME is not suitable
          for such applications, since that clock is affected by
          discontinuous changes to the system clock.
1 Like

How should a good, general purpose implementation for that work?

Just like the current monotonic-only timers: the evloop arms a system timer with a specific clock to interrupt a blocking evloop run when the next absolute time is reached + the evloop regularly the events for expired timers when their wake at <= now. It might only accept absolute times, so relative times are fully explicit at call sites: sleep(until: 5.minutes.from_now).

Without that, the only solution is to have a fiber regularly tick, and to check on every tick if the next wall time is reached… this is inefficient compared to the above where the OS wakes up the process when needed (instead of on every tick).

Anyway: UNIX targets make it easy while Windows will be a pain (probably needs a thread that ticks to manually interrupt the IOCP evloops) :enraged_face:

When the OS supports realtime sleep, this should be easy.
The problematic case is when it doesn’t.

But I suppose this should even work on Windows with SetWaitableTimer: If passed a negative value, it’s interpreted as a relative time span. We’re currently doing that in Crystal::System::WaitableTimer.
A positive value indicates absolute time in UTC.

Actually, the Win32 API seems to only support absolute timouts with realtime clock.
It does not provide any means to wait on a monotonic clock.

So a monotonic instant always needs to be converted to a relative timespan.

Well, that allows to use both monotonic and realtime clocks, by converting absolute monotonic to the relative duration from monotonic now, and relative realtime to absolute realtime at UTC :grin:

1 Like