[Pitch (back from revision)] Clock, Instant, and Duration

Overall, I love the updated pitch. I think it's done great job of incorporating the feedback from the original pitch and review threads.

I do have a question about it, though. Originally WallClock was pitched as part of the standard library at least partially to facilitate passing deadlines across distributed actors (and other multi-machine systems). Now that it's been removed in favour of UTCClock, is the expectation that any sort of cross-machine deadline handling will require importing Foundation (or some other library) rather than relying only on the standard library?

If I squint just right, I can make a decent guess that the answer is "yes" from the alternatives considered section, but even so, I feel that it should be mentioned more explicitly.

Fair point that the distributed actor clock usage should have perhaps a more distinct call-out. I will add that to my list of additional things to add in January when I get back to working on this stuff. Partially, I consider that particular clock usage should perhaps be best served by the implementation of the distributed actors themselves - since the requirements are a bit specific; they need a shared agreement of an epoch, and a continuous and non adjusted sense of elapsed time. Those requirements are not able to really be appropriately handled by any system forms of time uniformly across all supported platforms without additional information that the distributed actor transport has. CLOCK_MONOTONIC is not a sharable epoch, CLOCK_REALTIME can be adjusted by the user, CLOCK_UPTIME is machine specific and again is not shareable. So really it needs its own custom clock; with an epoch of the continuous clock's instant when connected and elapsed durations from that instant.

So in short; I feel that custom clock can be defined by the protocol and can likely be implemented in terms of composition from the existing clocks.

6 Likes

Thanks for the work on this proposal. It looks like this is really coming together. I haven't fully thought about everything yet but one thing stood out that IMHO should be fixed.

There are tolerance parameters which are of type Instant.Interval?, e.g.

func sleep(until deadline: Instant, tolerance: Instant.Interval?) async throws

The tolerance parameters are later documented as

Not specifying a tolerance infers to the implementor of the clock that the tolerance is up to the implementation details of that clock to choose an appropriate value.

which to me sounds like not passing tolerance or passing nil effectively means "default tolerance". But .none is one of the values of Optionals and this leads to this issue

sleep(until: .now.advanced(by: .seconds(3)), tolerance: .none)

to me reads like there is no tolerance, ie. the system must try as much as it can to hit the deadline exactly. Unfortunately, the real meaning is likely quite the opposite because the "default" tolerance is probably not trying to be as exact as possible.

In AsyncHTTPClient we hit a similar issue with redirectConfiguration: RedirectConfiguration? = nil where redirectConfiguration: .none does not mean "don't follow redirects" but rather means "default". So redirectConfiguration: .none means to follow something like "up to 5 redirects" which is super confusing.

So whilst most people would usually write nil over .none, Xcode's completion system actually offers .none above nil:

So typically I now recommend making the parameter non-optional and to instead provide a public static var default: Self = ... to the type which serves as the default value.

Something like

func sleep(until deadline: Instant, tolerance: Instant.Interval = .default) async throws
22 Likes

I’m not sure how often one would need to sleep until a set deadline, rather than some relative duration from now. But given the prevalence of examples like the following:

sleep(until: .now.advanced(by: .seconds(3)))

… maybe we could add a convenience like:

sleep(for: .seconds(3))
3 Likes

i know this is probably important, so i think a concrete example would be helpful for a lot of us :)

in my experience, time-related APIs end up being closely related to decimal-related APIs, so arithmetic-like functionality should probably wait until we have more experience with Decimal<T> in the standard library or a standard library-adjacent module. which is okay! we don’t need to tackle everything in one proposal, the arithmetic stuff can probably be separated out from the fundamental stuff.

I really like where this is going! Having Instant and Duration be types on the clock (rather than say expecting them to be a fixed type like a Double) both allows for adjustments to represent the clock with the level of precision desired, and allows for the clocks to have instants and durations which are not cross-compatible.

For an example, the UTCClock I imagine may act like a "Unix Time", incorporating leap seconds such that it is non-continuous and ambiguous on some systems (where a negative leap second 'repeats'). Some other non-unix systems may have a native TAIClock which counts leap seconds as something which needs adjustment in the calendaring system. Having he two types of clocks have different instant types means that you don't risk your times being mysteriously off a few dozen seconds.

That said, the sleep(until:,tolerance:) on Clock does not seem appropriate if a Clock may represent durations in some other unit such as frames. Instead, I would expect the Task itself to know what sort of Clock is appropriate for sleeping, delegating to the underlying scheduler.

Is the expectation currently that I can sleep(until:,tolerance:) using either ContinousClock, SuspendingClock, or UTCClock?

2 Likes

I am a big supporter of having Instant be an associatedtype vended by the clock implementation, this is a big +1 from me!

I think the fact that it is this difficult to argue why we should or should not support the arithmetic operators is an argument against including them in this proposal, at least until we have seen more real world usage of this new feature.

1 Like

Just a short note regarding the Kotlin comparison at the begin:
Kotlin has a function (measureTime) to measure the elapsed time returning a Duration, and also a tuple (TimedValue) returning the duration and the calculated value. The duration is took from a TimeSource, mainly from a monotonic one, which returns a TimeMark. It is comparable with a regular stopwatch, marking the start and the end to calculate the elapsed time.
Kotlinx-datetime containing (Wall-) Clock, Instant, LocalDate etc is not defined in the standard lib, but in an official library. This clock is not monotonic, but supports user/NTP updates.

1 Like

Just had a look at it, and want to point out that the source of the library does not contain the term "wall" at all (afaics, their wording is "system clock").
So I'm really happy that it looks like Swift won't try to establish a WallClock with irritating semantics (and I think it best to not carry on with that name in discussion ;-)

Hopefully folks had a good bit of time to enjoy the holidays and/or time off.

Yea, I agree - I wonder... per your suggestion about the ExpressibleBy family: what about DurationType?

The problem is that for common things it is needed - one adjustment that seems reasonable is to remove the requirement of BinaryInteger from those operations and just use a concrete type - The issue with using Double for all of those is that the durations may be calculations against durations from the epoch in sub-second resolution. That can easily go past 2**53.

That API is specifically to surface the resultants of things like clock_getres. It is intended for the minimum viable resolution the clock can potentially measure. For example on my laptop it is 42 nanoseconds for the continuous clock. minResolution is a fine shortening in my book - it still conveys the source of that value while indicating this is a value that may not be the guaranteed per tick interval.

The idea is that folks ought to be able to use clock themselves to compose existing clocks, build their own or use the provided ones. It may not be the most common API to adopt into a custom type, but it has numerous good use cases for games, simulations, specialized communication synchronization, and many other tasks that a custom clock for scheduling work is reasonable. However I am not sure how FilePath comes to play here. Clocks really don't have much to do with the file system (shy of chron).

Yea that is perhaps a gnarly bit of how Optional works. There is a semantical difference between none and zero. None is to infer the absence of any requested tolerance, zero is to infer a strict requirement of no leeway to that tolerance. The only way to adjust that is to make it non optional and have some sort of placeholder value representing the absence of a requested tolerance. Which I am not sure that is reasonable to implement for all potential duration types. To me this particular case is where we need to be super clear with documentation that this particular value is expected to have that none meaning nil, aka no request of a value and zero meaning a hard deadline.

That pattern is VERY common when doing things like debouncing or throttling. However for simple code it is a pretty common code to consider that as sleeping for a set number of seconds. I will consider this and work up some details this week to see what type of impact that will have.

Sleeping until a frame deadline is quite reasonable to me; this is how many rendering systems work. They sleep until the next v-sync deadline after work is done and give a window of some sort of duration between the instant the v-sync deadline is ready and when that rendering window closes. If Task is the primary implementor of sleep it means that only specific clocks could ever be used for sleep - thusly closing off any potential custom clocks, whereas if we let the clock be the determinate of sleep it means that the task can relegate sleep to the clock, and the implementor of that clock can handle any continuation for tasks.

Could we do the same thing with sleep as we did with the Random API's in the stdlib? I mean having sleep be a function on Task that takes a Clock i.e. Task.sleep<..., C>(until:,tolerance:,usingClock: C) where ..., C: Clock, instead of having it directly available on the clock. What if we want a synchronous version of sleep that doesn't use a Task, should that one take a clock instead of being defined on the Clock itself? I feel like this API doesn't belong in Clock.

A benefit this would bring would be the ability for people to extend Clock with properties vending their own clock implementations which would be available at the call site using . syntax and autocomplete: Task.sleep(until: ..., tolerance: ..., usingClock: .continuous) // or .suspending or .myGameClock, people now don't wouldn't have to remember which types are clocks, just choose from the list.

The other benefit, of course, is that people are already familiarising themselves with the current Task.sleep(nanoseconds:), so it would be the natural place to find this version of sleep that takes a clock.

2 Likes

That is precisely the implementation and proposed behavior:

Task.sleep:306

Perhaps that needs to be highlighted more strongly in the proposal?

1 Like

I can't seem to find any mention of Task.sleep on the proposal linked in the first post, is there a more recent version of the proposal?

Ok, I see now that you posted up above with a WIP and that the new revised proposal is still not ready, sorry for the noise then!

I have updated the proposal by taking some more of the feedback and incorporating it.

Highlights of the changes are:

DurationProtocol no longer uses BinaryInteger for the *, / operations. Instead it now just uses Int, which after discussing with @scanon this seems like the most reasonable course of action. It leaves the requirements clear for folks to implement while allowing for the algorithmic utility of things like back-off or debounce to follow the most reasonable mathematical fast paths.

The alternatives considered section now contains some more details on DurationProtocol and the choices per naming.

The associated type on InstantProtocol is no longer named Interval and instead named Duration. The conclusion was reached that implementing your own clock is considerably less common than using a clock, and that burden of needing to write Swift.Duration is acceptable for the cases where that is needed (when defining a custom Instant that uses the standard provided Duration).

In previous cleanups the sections about Task.sleep were lost - these were added back in and indications of deprecations were made as well. (The exact deployment availability is yet to be determined). Task also got a shorthand to be able to sleep for a given duration (by using the ContinuousClock). More fine grained control still exists on Task to sleep given a clock, tolerance, and deadline instant.

Per the back-deployment story - not all functionality (e.g. controlling tolerance) may be available without operating system runtime support. This means that back-deploying may not have as fully functional behavior as forward deployment.

12 Likes

Just wanted to say thanks for this. I was afraid that with these new time functionality APIs got more complex than necessary for the trivial cases. I'm glad is not like that :slight_smile:

1 Like

This proposal is now under review.

1 Like