Re: Clock precision and accuracy Marc Feeley 11 May 2019 14:36 UTC

Note that this time abstraction is prior art (circa 2000) from SRFI 18 (multithreading support).  Several implementations of Scheme currently support SRFI 18, so it shouldn’t be controversial.  SRFI 18 has this description of time objects and timeouts:

Time objects and timeouts

A time object represents a point on the time line. Its resolution is implementation dependent (implementations are encouraged to implement at least millisecond resolution so that precise timing is possible). Using time->seconds and seconds->time, a time object can be converted to and from a real number which corresponds to the number of seconds from a reference point on the time line. The reference point is implementation dependent and does not change for a given execution of the program (e.g. the reference point could be the time at which the program started).

All synchronization primitives which take a timeout parameter accept three types of values as a timeout, with the following meaning:

- a time object represents an absolute point in time
- an exact or inexact real number represents a relative time in seconds from the moment the primitive was called
- #f means that there is no timeout

When a timeout denotes the current time or a time in the past, the synchronization primitive claims that the timeout has been reached only after the other synchronization conditions have been checked. Moreover the thread remains running (it does not enter the blocked state). For example, (mutex-lock! m 0) will lock mutex m and return #t if m is currently unlocked, otherwise #f is returned because the timeout is reached.

(current-time)                                        ;procedure

Returns the time object corresponding to the current time.

    (current-time)  ==>  a time object

(time? obj)                                           ;procedure

Returns #t if obj is a time object, otherwise returns #f.

    (time? (current-time))  ==>  #t
    (time? 123)             ==>  #f

(time->seconds time)                                  ;procedure

Converts the time object time into an exact or inexact real number representing the number of seconds elapsed since some implementation dependent reference point.

    (time->seconds (current-time))  ==>  955039784.928075

(seconds->time x)                                     ;procedure

Converts into a time object the exact or inexact real number x representing the number of seconds elapsed since some implementation dependent reference point.

    (seconds->time (+ 10 (time->seconds (current-time)))
       ==>  a time object representing 10 seconds in the future

The objections I have heard in the past to using flonums to represent (absolute or elapsed) time are:

1) small/compact implementations of Scheme for microcontrollers may not have a flonum type
2) most implementations of Scheme need to allocate memory for flonums, whereas for a fixnum no allocation is needed

For #1, I would say that recently developped microcontrollers, even the 1$-5$ variety, are becoming featurefull and have floating point arithmetic in hardware or libraries. So there isn’t much of an issue to having flonums on modern microcontrollers.  For #2, memory allocation of a 64 bit object is a small price to pay for a clean interface, and returning a pair of fixnums (or multiple values) probably allocates as much or more.

For the 0.10$ microcontrollers with very little ram and weak CPUs, it is very likely that the Scheme implementation will not support all Scheme libraries and features (inexact reals, rationals, continuations, files, etc).  In this case, time objects will probably be represented internally with an integer counting milliseconds, ticks, etc. For speed, the time object could be dropped so that this integer is used raw (so the time? procedure and relative timeouts would have to be dropped).  In other words the really tiny microcontrollers live in a different world and programming is not 100% compatible with the bigger Schemes.  I think that is acceptable for a niche use case.

Marc

> On May 11, 2019, at 8:34 AM, Lassi Kortela <xxxxxx@lassi.io> wrote:
>
>> The precision (nanosecond, millisecond, etc) should not be part of the time API.  Doing so leads to the kind of issues you mention and endless discussion of what is the most appropriate precision.  Time should be an abstract object that hides the precision (which can depend on the specifics of the lowlevel interface).
>> Gambit has the time->seconds and seconds->time procedures to convert between time objects and a flonum giving the elapsed time since a reference point (unix epoch).  Internally Gambit uses flonums for time calculations such as I/O timeouts, thread scheduling quantums, etc
>
> This is actually a very nice abstraction. It means implementations have a lot of leeway to choose the easiest/fastest representation (or representations) and users can have a portable and extensible API.
>
> E.g. beside time->seconds you could add time->milliseconds, time->microseconds and time->nanoseconds without disturbing the old API at all. And the reverse: nanoseconds->time, etc.
>
> Ditto with decomposed versions: time->seconds-and-nanoseconds, etc.
>
> And different epochs: time->windows-filetime ("100-nanosecond intervals since January 1, 1601 UTC", an interesting choice).
>
> And none of these functions promise or demand a particular sub-second precision for the internal representation, but as you say and as we seem to happily agree, that's the point. Time precision is somewhat of a cargo-cult thing: APIs like to say what the precision is but rarely do they go back to the source of the timestamp to ascertain that the source is capable of measuring time so precisely.
>
> If multiple internal representations are permitted, lossless round-tripping of timestamps also becomes possible. E.g. Unix stat() returns seconds and nanoseconds as separate integers. If a (secs . nsecs) representation can be used then e.g. the following will set "foo" to have the exact same timestamp as "bar":
>
> (set-file-time "foo" (get-file-time "bar"))
>
> While it still remains possible to do things like (time->seconds (get-file-time "bar")) etc.
>
> The only problem is which of these procedures to include in the SRFI. We could perhaps have time->os-filetime and os-filetime->time?
>
>> As time passes, the (fixed) 52 significant bits of a 64 bit flonum will represent the seconds since the epoch with increased error.  Currently and for the next 20 years, the integer part of the time takes 31 bits and there are 21 bits left to represent the fraction of a second, which means the error is sub-microsecond for the next 20 years.  By that time architectures will probably have evolved to 128 bit flonums (alternatively a new epoch could be defined to reset the error).
>
> Enlightening. I had never thought of it that way.
>
>> Here are a few examples from Gambit:
>>> (file-info ".")
>> #<file-info #2
>>    type: directory
>>    device: 16777220
>>    inode: 8606475805
>>    mode: 493
>>    number-of-links: 46
>>    owner: 501
>>    group: 20
>>    size: 1472
>>    last-access-time: #<time #3>
>>    last-modification-time: #<time #4>
>>    last-change-time: #<time #5>
>>    attributes: 16
>>    creation-time: #<time #6>>
>>> (time->seconds (file-info-last-modification-time (file-info ".")))
>> 1557425438.
>>> (time->seconds (current-time))
>> 1557573958.072262
>>> (- (time->seconds (current-time)) (time->seconds (current-time)))
>> -9.5367431640625e-7
>>> (real-time)
>> 166.17429184913635
>>> (cpu-time)
>> .035474
>
>