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

So I have been looking into this because the naming discontinuity does bug me a bit too.
Doing so does not harm the general use of clocks; which quite honestly will be a VERY high majority of the code that will touch this stuff. The few cases where it will matter are the cases where someone is implementing a clock. It does make the implementation of clocks a touch more obtuse, but that difficulty is traded off by making the names line up nicer.

In short: if folks feel strongly about this - it is a tradeoff that we are willing to make.

6 Likes

Hi @Philippe_Hausler!

This is absolutely fantastic. I've let some time go by after reading the document once, and on second reading it's even better! I agree with @davedelong that the changes overall strengthen the final design significantly.

A few questions and points of feedback:

I agree with others that the Interval versus Duration terminology is weird. Additionally—and I don't think this has been mentioned—recall that in earlier versions of Swift Interval was a standard library type that got coalesced into Range: it would be nice not to resurrect that name for something that's totally different.

If another word would be useful for the associatedtype, might I suggest TimeSpan, which is obviously time-related unlike Interval.

However, I think it's quite fine not to use two different words: indeed, the standard library ExpressibleBy* types define associated types that have the same name as global type aliases (e.g., StringLiteralType is an associated type of ExpressibleByStringLiteral and also a global type alias).


I agree with @scanon re the weirdness of the DurationProtocol: if it's not needed, that would be fantastic.

I also have some questions regarding the operators it (and the concrete type Duration) exposes, which center around this: is there an implementation reason that multiplication by arbitrary integer types is a requirement, while division of two conforming types produces a Double result? It would seem logical that multiplication would be by Double values also (which as you know can represent all consecutive integers exactly up to 2 ** 53, and which I notice the concrete Duration type supports)... Do the integer overloads have to do with what you were referring to in your reply about "ensur[ing] fast paths for calculating certain durations"?


I think I understand the usefulness of the newly pitched minimumResolution API. Two minor points about the name:

  • First, I think there are two subtly different interpretations a user could have: either (1) "great! this clock guarantees that it's at least that accurate"; or (2) "oh! I shouldn't expect two points in time less than that to be distinguishable." Maybe I'm confusing myself and these two are essentially the same statement, but I think this is boiling down to accuracy versus precision, and I'm not exactly sure which one we're trying to represent here and whether there is a common term-of-art in computer timekeeping for it other than "resolution." The proposal also uses the term "granularity" to describe the API—I wonder if there's way to clarify this not only through the explanatory text but in the name of the property itself.
  • The standard library, usually eschewing abbreviations, has adopted min and max as the spelling of "minimum" and "maximum" throughout, with the sole exception of the floating-point IEEE "minimum" and "maximum" operations that are deliberately named distinctly from Swift.min since they handle NaN values differently. Even if we stick to the term "resolution," the API would be more consistently named minResolution.

I agree with @davedelong that it's (only a bit) unfortunate that we'd not have a standard library wall clock. I think the revised approach of not putting it in the standard library makes a lot of sense when interfacing with an unchanged Foundation.Date type is essential, while still allowing for use with these modern non-calendrical APIs via conformance to ClockProtocol. Overall, I like this approach very much so long as we don't close the door to something like the approach which swift-system takes with FilePath with a potentially separate system clock project (obviously without necessarily committing to such an approach)—based on what I'm seeing in the proposal, there shouldn't be any barriers to third-party libraries tackling this space, correct?

9 Likes

I am very much in favor of shifting responsibility to new packages rather than Foundation. Foundation is way too monolithic, and it is extremely difficult to tell whether code uses platform-specific behavior from it.

3 Likes

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