[Pitch] Clock, Instant, Date, and Duration

The shadows do work, but they pose an ergonomic issue; for example the global function for measure becomes ambiguous w/ XCTest's instance method of measure - so it then makes folks need to write either Swift.measure {...} or self.measure {...} which is a sub-par experience especially since it would make sense that performance measurement would be something that interfacing with clocks would be ideal.

1 Like

Amazing, this is great improvement for Swift.

Just one question: are all of these new features supposed to be implemented in a framework, like it was made with SwiftCollections?
I think we need time to stabilize and polish API.

i think a lot of people here are under the misconception that the less-significant bits of the nanosecond field are not important and can be discarded at will. at least in the field of finance, this is emphatically not true. the exact decimal value of a timestamp is actually very significant, and is used for things like computing checksums and enforcing a consistent sort order. the fact that the less-significant bits have no physical meaning is irrelevant. they are used as common reference points where all participants in a transaction can agree upon exactly “when” a particular event occurred, even if that timestamp has no basis in reality. a useful way to think of it is to think of the first 80 or so bits as the time part of a timestamp, and the last 16 bits as the stamp part of the timestamp.

this is a big reason why i have always rolled my own Date type, the Double-backed representation is simply not usable in this context in its current form. if the nanosecond field gets truncated by going through Codable, that would be very unfortunate.

this is also slightly off-topic, but it would be useful to include a leap flag to represent second #60, which sorts after 59.999_999_999. in my custom implementation, i just have it as a Bool coming after the Int32 nanosecond field, since a 12-byte alignment is kind of weird, and would fall under a 16-byte stride anyways. but it could also be packed into the Int32 nanosecond field.

6 Likes

What happens to the flag if two leap seconds are inserted?

two leap seconds have never occurred in the same minute. we have actually not had two leap seconds in the same year since 1972. in my opinion, a lot of the discussion around time APIs has revolved around things that will never happen, with exactly 0 probability.

along the same lines, the planet will be underwater in 3 centuries.

It cannot exist outside of the concurrency and standard library due to the interaction with how task schedules sleep.

Also worth noting: that this is not targeting swift 5.5, but instead targeting a subsequent release.

In that case, would you want a DateProtocol in the standard library — to which types such as yours, and Foundation.Date, and DispatchWallTime can all conform?

public protocol DateProtocol: InstantProtocol {
  init?(durationSince1970: Duration)
  var durationSince1970: Duration { get }

  static var now: Self { get }
  static var distantFuture: Self { get }
}
1 Like

sure. but i don’t really see why we shouldn’t just get this right in the standard library to begin with. there are not that many degrees of freedom in this design space.

3 Likes

I thought of a few examples just for fun.

NTP servers can be used as a clock when it's potentially unsafe to trust the system clock (such as when system time can be modified by the user, as in the case of iOS or macOS).

struct NTPServerConnection: ClockProtocol {
    public typealias Instant = Date
    init(url: URL) { ... }
}

Game engines may manage their own concept of "game time" that is separate from "real world" time. For example, I may want to trigger an event after 10 minutes of play time, not including time when the game was paused by the user. If I rely on the system clock I have to stop and restart timers when the game is paused. Relying on the game clock (which pauses with the game engine) would make this much easier.

struct GameEngineClock: ClockProtocol {
     //monotonically increasing value to track game time
     public typealias Instant = Int 
     //trigger after a certain amount of active play time (not including pauses)
     static func sleep(until deadline: Instant) async throws
}
6 Likes

I am well aware of that. However, it is perfectly plausible for two leap seconds to be inserted at some point in the future, and if you’re going to go through the trouble of representing leap seconds in Date as part of the standard library, you might as well “get this right to begin with,” as you say.

5 Likes

i’m not sure why this is a point of contention. two leap seconds in the same minute will never happen. the only way it could possibly happen is if an asteroid knocks the planet off its axis, in which case we will all be too dead to care about leap seconds. trying to account for multiple leap seconds would needlessly complicate the implementation, for no perceivable benefit.

leap seconds happen. that is why it is valuable to represent them in the standard library. multiple leap seconds do not happen. that’s the key difference here.

i will also add that virtually every format specification with a time field specifies seconds as being in the range 0 ... 60 (as opposed to 0 ... 59).

I think this is outside of the scope. Leap is the concern of Calendar-like APIs.

3 Likes

I’m not sure how you specifically have such confidence that multiple leap seconds will never happen, when my understanding is that literally no one is able to predict the frequency of leap second insertions. As far as I can tell, if the earth wobbles slightly more in a year than in 1972, then multiple consecutive leap seconds will happen even under the current rules.

Moreover, these are human calendrical constructs, and the policies surrounding them are governed by international treaty and subject to amendment. Leap seconds might be abolished in 2023, for instance—which would make them multiple leap seconds in the future exceedingly unlikely. But if, say, a compromise is made to insert leap seconds only with 1 year’s advance notice instead of 6 months, or to batch them once a decade, then you’d be proven wrong rather more quickly.

3 Likes

To keep this discussion on track; the proposed WallClock only reports the same as the reported value from clock_gettime or mach_get_times etc. Calendrical calculations are out of scope of the proposal so leap adjustments may be reflected from network updates and such but not leap days etc.

If the host system can report a wall clock time that reflects now as adjusted to a leap second, the WallClock will report that. But since the Instant of that clock is just a UTC duration since 1970 it means that there is no need for adjustment to leap seconds and is relegated to locale aware APIs like Calendar, TimeZone and Locale etc (hence why I made it distinct this proposal is not about).

6 Likes

Both of those examples are quite compelling, and the other one mentioned is a test clock. Which I have been using to validate the implementations so far (and works amazingly well for the limited scope I am using it for).

1 Like

i have a related idea, let me run it through you.

when we go to multiplies of bytes we traditionally use KB/MB/GB/etc units:

2^10 = 1024^1 kilobyte (aka kibibyte to distinguish from metric units)
2^20 = 1024^2 megabyte (aka mebibyte)
2^30 = 1024^3 gigabyte (aka gibibyte)

i haven't seen a similar tradition used for fractions, but it's also possible and quite logical:

2^-10 = 1024^-1 codenamed "milbi", naming suggestions welcomed
2^-20 = 1024^-2 codenamed "micbi"
2^-30 = 1024^-3 codenamed "nabi" (or "nanbi"?)

inversely to multiple units, these binary fraction units are slightly less than their metric counterparts:
milbisecond is 0.0009765625(0) s (vs millisecond = 0.001 s)
micbisecond is 0.000000953674316(0) s (vs microsecond = 0.000001 s)
nabisecond is 0.000000000931323(0) s (vs nanosecond = 0.000000001 s)

to convert from nabiseconds to nanoseconds - when needed - one would use:
nanosecond = nabisecond * 1000_000_000 / 0x3FFFFFFF // approx x 0.93132257548284

how that relates to the topic at hand: we can express the new Date type as a "number of "nabiseconds" like so:

struct NewDate {
    var seconds: Int64
    var nabiseconds: UInt32 // 0 ... 0x3FFFFFFF
}

the benefit of nabiseconds compared to normal nanoseconds would be simplification of adding two nanosecond numbers together: normal nanoseconds would require integer division / remainder operations while two nabiseconds can be just added together (e.g. with 64-bit math) and any leftover bigger than 30 bits can be added as is to the number of full seconds.

update: alternatively we may store the full 32 bit number as a fraction of a second. the precision is 2^-32 which is ~4x of nanosecond precision - not a goal per se but still nice, the benefit is further simplification due to full bit allocation and absence of wrong dates: any bit pattern would be a valid date value, compared to the previous version that doesn't allow two high bits in the nabiseconds field.

on a related front - i believe that the issues of leap second calculus, or anything else that converts between NewDate <-> DateComponents or NewDateInterval <-> DateComponents can be resolved at a higher Calendar level, while the NewDate type itself can be as dumb as a number (similar to old Date). number of nanoseconds themselves (bi or normal) elapsed from some reference epoch date can not be affected by leap seconds or leap years.

1 Like

The same time, with multiple durations added in different orders, may not be equivalent.

You cannot represent a millisecond/nanosecond unless those are the units of the time system (e.g. Duration(1) represents 1 nanosecond). Instead, every operation and comparison and display involves rounding/heuristics.

1 Like

There was a proposal (which was not treated very favorably) to just consolidate them into 1h time changes since everyone is used to those.

Definitely the case. Full nanos would need to be lossless to make the type useable in the financial domain.

6 Likes

This is wrong. You can add 64s + 32ns just as easily (or at least, close enough that it doesn't matter):

func +(a: Thingy, b: Thingy) -> Thingy {
  let ns = a.ns &+ b.ns // cannot overflow
  if ns >= 1_000_000_000 {
    return Thingy(s: a.s + b.s + 1, ns: ns &- 1_000_000_000)
  }
  return Thingy(s: a.s + b.s, ns: ns)
}

No divisions or remainders are required.

9 Likes