Skip to content

Commit b4a2038

Browse files
committed
Describe comparable time marks and their use cases
1 parent 59d1209 commit b4a2038

File tree

1 file changed

+102
-9
lines changed

1 file changed

+102
-9
lines changed

proposals/stdlib/durations-and-time-measurement.md

Lines changed: 102 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ and solves the problem of specifying its unit.
5555
While `measureTime` function is convenient to measure time of executing a block of code, there are cases when
5656
the beginning and end of a time interval measurement cannot be placed in the scope of a function. These cases require
5757
to notch the moment of the beginning, store it somewhere and then calculate the elapsed time from that moment.
58-
Also the elapsed time can be noted not only once, as it is with `measureTime`, but multiple times.
58+
Also, the elapsed time can be noted not only once, as it is with `measureTime`, but multiple times.
5959

6060
The interface `TimeSource` is a basic building block both for implementing `measureTime` and for covering the case above.
6161
It allows to obtain a `TimeMark` that captures the current instant of the time source. That `TimeMark` can be queried later
@@ -72,6 +72,34 @@ timeout is not expired, and positive values if it is. For convenience, instead o
7272
a negative duration one can use `hasPassedNow`/`hasNotPassedNow` functions of the `TimeMark`.
7373
This way timeout can be represented by a single `TimeMark` instead of a `TimeMark` and a `Duration`.
7474

75+
#### Noting time between two time marks
76+
77+
When measuring elapsed time from a single origin point, it's enough to get a `TimeMark` at the origin point
78+
and then use its `elapsedNow` function.
79+
80+
However, if a use case requires measuring at some point several elapsed time values from several origin points,
81+
it is hard to do consistently using only the `elapsedNow` function because each its call may return slightly different
82+
value depending on the *current* time in the time source produced these origin time marks.
83+
84+
```kotlin
85+
val elapsed1 = originMark1.elapsedNow()
86+
val elapsed2 = originMark2.elapsedNow()
87+
// the difference between elapsed1 and elapsed2 depends on the order of calls and
88+
// on the unpredictable delay between these two calls
89+
```
90+
91+
When, for example, doing several animation calculations, it's important to measure time elapsed from the start
92+
of each animation with regard to the frame rendering time moment consistently, and such unpredictable difference may
93+
result in unwanted visual artifacts.
94+
95+
To support this case, a time source can return time marks comparable and subtractable with each other, so it is possible
96+
to mark the time moment of a frame once and then calculate all animation elapsed times consistently:
97+
98+
```kotlin
99+
val markNow = timeSource.markNow()
100+
val elapsed1 = markNow - originMark1
101+
val elapsed2 = markNow - originMark2
102+
```
75103

76104
## Similar API review
77105

@@ -111,6 +139,11 @@ Another approach that was considered is introducing the class `TimeStamp` and th
111139
return `Duration`. However, it was concluded to be error-prone because it would be too easy to mix two timestamps taken
112140
from unrelated time sources in a single expression and get nonsense in the result.
113141

142+
**Update:** after considering use cases (see [Noting time between two time marks](#noting-time-between-two-time-marks)),
143+
we decided to introduce a subtype of `TimeMark`, `ComparableTimeMark`, that allows arithmetic operations
144+
(subtraction, comparison) on time marks obtained from the same time source,
145+
even though mixing time marks from different time sources would lead to a runtime exception.
146+
114147
## API details
115148

116149
### Duration
@@ -282,9 +315,64 @@ if (expirationMark.hasPassedNow()) {
282315
Instances of `TimeMark` are usually not serializable because it isn't possible to restore the captured time point upon deserialization
283316
in a meaningful way.
284317

318+
### Comparable time marks
319+
320+
`ComparableTimeMark` interface extends the `TimeMark` interface with the functions to compare two time marks with each other
321+
and to calculate time elapsed between them.
322+
323+
```kotlin
324+
interface ComparableTimeMark : TimeMark, Comparable<ComparableTimeMark> {
325+
abstract override operator fun plus(duration: Duration): ComparableTimeMark
326+
open override operator fun minus(duration: Duration): ComparableTimeMark = plus(-duration)
327+
operator fun minus(other: ComparableTimeMark): Duration
328+
override operator fun compareTo(other: ComparableTimeMark): Int = (this - other) compareTo (Duration.ZERO)
329+
330+
override fun equals(other: Any?): Boolean
331+
override fun hashCode(): Int
332+
}
333+
```
334+
335+
In order to represent a time source from which comparable time marks can be obtained, a specialized time source interface is introduced:
336+
337+
```kotlin
338+
interface TimeSource {
339+
interface WithComparableMarks : TimeSource {
340+
override fun markNow(): ComparableTimeMark
341+
}
342+
}
343+
```
344+
345+
The comparison `timeMark1 < timeMark2` returns true if `timeMark1` represents the moment earlier than `timeMark2`.
346+
If the time marks were obtained from different time sources, both comparison and the `minus` operator throw an `IllegalArgumentException`.
347+
348+
Comparable time marks also implement structural equality contract with the `equals` and `hashCode` functions consistent with
349+
the `compareTo` operator, so that if `timeMark1 == timeMark2`, then `timeMark1 compareTo timeMark2 == 0`.
350+
However, the equality operator doesn't throw an exception when time marks are from different time sources, it just returns `false`.
351+
352+
#### Alternatives considered
353+
354+
- Instead of introducing separate interfaces for comparable time marks and time sources returning them, introduce functions
355+
in the `TimeMark` base interface.
356+
- Comparing time marks is not always needed, but supporting it in the base interface would complicate all `TimeSource`
357+
implementations.
358+
359+
- Instead of introducing specialized time source for comparable time marks, parametrize the base `TimeSource` interface
360+
with a generic time mark type, then `TimeSource<*>`, `TimeSource<TimeMark>`, and `TimeSource<ComparableTimeMark>` types
361+
can be used.
362+
363+
```kotlin
364+
interface TimeSource<out M : TimeMark> {
365+
fun markNow(): M
366+
}
367+
```
368+
- We do not expect many different parametrizations of `TimeSource` interface, so dealing with pesky `<>` brackets in
369+
common use cases would be tedious.
370+
- Parametrization only affects the return type of one function, so it doesn't bring much value compared to covariant
371+
override in a more specialized interface.
372+
285373
### Monotonic TimeSource
286374

287-
`TimeSource` has the nested object `Monotonic` that implements `TimeSource` and provides the default source of monotonic
375+
`TimeSource` has the nested object `Monotonic` that implements `TimeSource.WithComparableMarks` and provides the default source of monotonic
288376
time in the platform.
289377

290378
Different platforms provide different sources of monotonic time:
@@ -325,17 +413,20 @@ This allows to make such time mark an inline value class:
325413

326414
```kotlin
327415
public interface TimeSource {
328-
public object Monotonic {
416+
public object Monotonic : TimeSource.WithComparableMarks {
329417

330418
override fun markNow(): ValueTimeMark = ...
331419

332420

333-
public value class ValueTimeMark internal constructor(internal val reading: ValueTimeMarkReading) : TimeMark {
421+
public value class ValueTimeMark internal constructor(internal val reading: ValueTimeMarkReading) : ComparableTimeMark {
334422
override fun elapsedNow(): Duration = ...
335423
override fun plus(duration: Duration): ValueTimeMark = ...
336424
override fun minus(duration: Duration): ValueTimeMark = ...
337425
override fun hasPassedNow(): Boolean = !elapsedNow().isNegative()
338426
override fun hasNotPassedNow(): Boolean = elapsedNow().isNegative()
427+
428+
override fun minus(other: ValueTimeMark): Duration = ...
429+
operator fun compareTo(other: ValueTimeMark): Int = ...
339430
}
340431
}
341432
}
@@ -353,16 +444,18 @@ and thus working with `TimeSource.Monotonic` through its `TimeSource` interface
353444
The function `measureTime` without a `TimeSource` also benefits from that, as it obtains a time mark from the default monotonic
354445
time source.
355446

356-
### AbstractLongTimeSource/AbstractDoubleTimeSource
447+
`ValueTimeMark` is a `ComparableTimeMark`, thus it allows comparing it with other `ValueTimeMark` values.
448+
449+
### AbstractLongTimeSource
357450

358-
These two abstract classes are provided to make it easy implementing own `TimeSource`
359-
from a source that returns the current timestamp as a number.
451+
This abstract class is provided to make it easy implementing own `TimeSource`
452+
from a source that returns the current timestamp as an integer number.
360453

361454
```kotlin
362-
public abstract class AbstractLongTimeSource(protected val unit: DurationUnit) : TimeSource {
455+
public abstract class AbstractLongTimeSource(protected val unit: DurationUnit) : TimeSource.WithComparableMarks {
363456
protected abstract fun read(): Long
364457

365-
override fun markNow(): TimeMark = ...
458+
override fun markNow(): ComparableTimeMark = ...
366459
}
367460
```
368461

0 commit comments

Comments
 (0)