SE-0329 (Second Review): Clock, Instant, and Duration

I don't think I'm well versed enough in time APIs to have a strong opinion on this topic, but the proposed solution seem pretty solid to me!

WRT clocks in the standard library, would a clock relative to 1970 for unix times make sense to include in the standard library or would it make more sense to include this clock in swift-system. I ask specifically because I'm working on a wrapper for stat(2), but would like to provide a nicer API over timespec for mtime (and friends).

1 Like

IIRC the version I saw before used 32 bits for nanosecond field, what is the reason to switch to 64 bits for this field?

Are durations equatable? Are these durations equal?

Duration(seconds: 0, nanoseconds: 1000 * 10^9 + 1)
Duration(seconds: 1, nanoseconds: 999 * 10^9 + 1)
Duration(seconds: 2, nanoseconds: 998 * 10^9 + 1)
Duration(seconds: 1000, nanoseconds: 1)

Note that this version of the proposal doesn’t specify the underlying storage at all; the 64-bit properties are read-only and are not stated to be stored properties. There is no initializer like what you write the above and the user doesn’t have access to any APIs which would allow alternative representations of the same duration.

4 Likes

Any thoughts on what idiomatic usage of the built-in clocks would be for consumers?

The sample code in the proposal abstracts away the specific concrete Clock type:

try await someClock.sleep(until: .now.advanced(by: .seconds(3))

Would we expect folks to write something like this in practice?

try await ContinuousClock().sleep(until: .now.advanced(by: .seconds(3))

This would be equivalently expressed as

try await Task.sleep(for: .seconds(3)) // omitting `for` if my suggestion is adopted

As a side note, it was indicated in the description of revisions that sleep(for:) would use a continuous clock, but the text under review describes how sleep(nanoseconds:) uses different clocks on Darwin and Linux and does not specify that sleep(for:) uses strictly the continuous clock—I think this needs to be clarified.

6 Likes

I still think that the optional tolerance parameters lead to an unfortunate API where

sleep(..., tolerance: .none)

doesn't mean "no tolerance" but "default tolerance" which is quite confusing. This confusion could be easily avoided by making the parameter non-optional and requiring some .default or so value.

20 Likes

The naming of the optional parameter will occur any place that a type such as Duration is listed - for example if an Int? parameter is labeled as "count" the same issue occurs someFunction(count: .none) is equally as misleading, yet a nil parameter of an Int is quite meaningful and useful.

That being said; an indefinite duration can be made out of the underpinnings, and that indefinite value could be a requirement of the DurationProtocol. It would just semantically mean a value that is greater than any other reasonable duration. Which I think is sensible for any DurationProtocol adoption.

1 Like

That was proposed initially however due to the issues surrounding Date and it's naming that was decided to not be placed into the standard library. Sinking Date (the type that represents what you are talking about; a fixed point in time that is relative to an epoch) was considered - however the name was determined to be unsavory since it can easily be conflated with a calendrical date when the Calendar type is not present. Additionally there were some strong concerns with regards to leap seconds. The end result is that a clock and instant that functions as you are wanting will exist as a modification to Foundation - UTCClock.

as @xwu correctly pointed out: those initializers don't exist (and I presume you are meaning 1e9 not 10 xor 9).

let a: Duration = .seconds(0) + .nanoseconds(1000 * 1000000000 + 1)
let b: Duration = .seconds(1) + .nanoseconds(999 * 1000000000 + 1)
let c: Duration = .seconds(2) + .nanoseconds(998 * 1000000000 + 1)
let d: Duration = .seconds(1000) + .nanoseconds(1)

Those are all equal to each other; and share the same memory pattern. If you are interested in the backing storage the initial draft of the proposal can be found here: [WIP] Initial draft at v2 Clock/Instant/Duration by phausler · Pull Request #40609 · apple/swift · GitHub

3 Likes

Any thoughts on defining something like:

enum DurationTolerance<Duration : DurationProtocol> {
   case default
   case zero
   case coalescedWithin(Duration) 
}

and then passing that enum as the parameter to sleep(tolerance:)? That removes the .none ambiguity of Optional and gives some more specific names to the other cases (case names are just an example, but I do think the none ambiguity should be avoided if possible).

2 Likes

That isn't too bad; the only issue with that approach is that it would cost at least 2 bits more than the stored duration; which I think it is reasonable to ask for types that implement duration to specify an indefinite.

Given the two choices of wrapping an enum or requiring a specific value representing an indefinite tolerance. I am leaning to the latter. Which I think is a reasonable condition on acceptance for this.

Could we not work-around the leap seconds issue by introducing a SystemClock, as the system's notion of the current time, with whatever leap seconds handling it has. I don't know of any other languages' standard libraries which provide access to monotonic clocks and uptime clocks but not the system's reckoning of the current time. Do you know of any?

We could avoid the naming issues for now by simply referring to it as SystemClock.Instant. It could later be given a nicer name, such as Timestamp.

There are a lot of libraries which parse timestamps and need the ability to return those values as a convenient type. For example, async-http-client parses cookie expiration times, and UniqueID is able to extract the embedded timestamps from time-ordered UUIDs.

Whilst Foundation has split off FoundationNetworking and FoundationXML as separate modules, Date still lives in the main Foundation library, alongside types such as Calendar and DateFormatter which themselves depend on ICU. With the standard library now having dropped ICU, importing Foundation just to have Date as a currency type comes with a higher cost than ever before.

If this doesn't make it in to the standard library, it is inevitable that 3rd-party packages will step in to fill the gap. But that is not a better outcome for the Swift ecosystem, IMO - we would be better-served by a currency type which lives in the standard library. Providing those currency types is, after all, one of the standard library's main jobs.

IMO, defining the Clock protocol but leaving the system clock to other packages is like defining the Collection protocol but asking developers to bring their own Array.

2 Likes

A handful of minor issues aside, I think the proposal is great. I particularly appreciate the improvements over the initial review.

I only see three rough edges here:

  • Echoing @xwu's comment, I think adding a Duration type alias to Clock and then defining Task's convenience method as sleep(_: ContinuousClock.Duration) would significantly improve the clarity of the API — developers would see which clock is being used from the type signature without having to refer to external documentation.

  • I find the default behaviour of Clock.sleep allowing a clock-defined tolerance very surprising. I don't think I've ever come across a sleep-like API that doesn't default to "as close to the requested duration as possible". I definitely see the benefits of giving the implementation some leeway, but on the other hand I expect developers new to this API to be surprised by that, and then it'll become one more oddity that they'd need to keep track of as they switch between APIs.

    IIUC, I think there's even an example of that in the proposal text itself — the deprecation warnings for the existing Task.sleep methods should instead suggest that they be replaced with sleep(for: _, tolerance: .seconds(0)) to maintain their current behaviour. Otherwise, callers will be getting a default tolerance where previously they weren't.

  • I also find the tolerance: .none construct confusing. I'd instead suggest something along these lines for that functionality:

    protocol Clock {
      func sleep(until deadline: Instant, tolerance: Instant.Duration) async throws 
    
      var defaultTolerance: Instant.Duration { get }
    }
    
    extension Clock {
      func sleep(until deadline: Instant) async {
        sleep(until: deadline, tolerance: .defaultTolerance)
      }
    }
    

    The key point being that a Clock implementation would be required to provide its default tolerance in addition to its minimum resolution, and then that value is used as a default value rather than Optional.none.

    In any case, I think having a Clock make its default tolerance programmatically visible would be a great improvement.

I think so, yes.

I've worked with timing APIs in C, Java, C#, Obj-C, and Swift for task scheduling and timeout handling since the late 90s. I think this proposal compares very favourably to any other timing API I've used.

I've followed both pitch threads (and their related proposals) very closely, but only managed to skim the original review thread.

One more point on tolerances — whichever way that discussion ends up, I think it'd be helpful to include a static .zero property on Duration.

That is already a thing: AdditiveArithmetic has a requirement of .zero

1 Like

As I’d commented before during the pitch phase, this seems appropriate for a setup analogous to URL remaining in Foundation and FilePath coming up in Swift System.

To be clear; this approach is not possible since the default tolerances are not a knowable value since they are determined at runtime (potentially at layers that processes don't even have access to). On Darwin for example, the kernel decides what the unspecified tolerance is for timers based on QoS and many other factors. To actually extract that number; the calling process needs root level privileges - so I don't think we can feasibly expose that as an actual value.

Right, of course. Sorry about that, I find it hard to remember all the various protocol requirements without Xcode running nearby.

Sorry if this has been brought up already, but couldn't Tolerance be an enum with cases default and specified(Duration)?

1 Like

You can allocate a certain duration constant value (e.g. all bits set) to represent default duration value, no? And if the notion of infinite duration is needed it can be modelled as, say, next smaller number (or vice versa).