KT-58046 Use saturated math in LongTimeMark implementation

- keep offset in range (-1..+1) of time source units
- when adding big or infinite offset, saturate startedAt instead
- displace initial reading to zero, similar to MonotonicTimeSource
- fix the zero reading in TestTimeSource by calling markNow in constructor
- use more precise reading adjustment in TestTimeSource for big durations
- add a note about comparable time marks for AbstractLongTimeSource and TestTimeSource
This commit is contained in:
Ilya Gorbunov
2023-04-10 18:04:11 +02:00
committed by Space Team
parent 7a6947ad35
commit 1014434475
5 changed files with 193 additions and 120 deletions
+66 -46
View File
@@ -5,9 +5,7 @@
package kotlin.time
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.math.sign
@SinceKotlin("1.3")
@ExperimentalTime
@@ -21,6 +19,9 @@ internal expect object MonotonicTimeSource : TimeSource.WithComparableMarks {
/**
* An abstract class used to implement time sources that return their readings as [Long] values in the specified [unit].
*
* Time marks returned by this time source can be compared for difference with other time marks
* obtained from the same time source.
*
* @property unit The unit in which this time source's readings are expressed.
*/
@SinceKotlin("1.3")
@@ -29,58 +30,62 @@ public abstract class AbstractLongTimeSource(protected val unit: DurationUnit) :
/**
* This protected method should be overridden to return the current reading of the time source expressed as a [Long] number
* in the unit specified by the [unit] property.
*
* Note that the value returned by this method when [markNow] is called the first time is used as "zero" reading
* and the difference from this "zero" reading is calculated for subsequent values.
* Therefore, it's not recommended to return values farther than `±Long.MAX_VALUE` from the first returned reading
* as this will cause this time source flip over future/past boundary for the returned time marks.
*/
protected abstract fun read(): Long
private val zero by lazy { read() }
private fun adjustedRead(): Long = read() - zero
private class LongTimeMark(private val startedAt: Long, private val timeSource: AbstractLongTimeSource, private val offset: Duration) : ComparableTimeMark {
override fun elapsedNow(): Duration = if (offset.isInfinite()) -offset else (timeSource.read() - startedAt).toDuration(timeSource.unit) - offset
override fun plus(duration: Duration): ComparableTimeMark = LongTimeMark(startedAt, timeSource, offset + duration)
override fun elapsedNow(): Duration =
saturatingOriginsDiff(timeSource.adjustedRead(), startedAt, timeSource.unit) - offset
override fun plus(duration: Duration): ComparableTimeMark {
val unit = timeSource.unit
if (duration.isInfinite()) {
val newValue = saturatingAdd(startedAt, unit, duration)
return LongTimeMark(newValue, timeSource, Duration.ZERO)
}
val durationInUnit = duration.truncateTo(unit)
val rest = (duration - durationInUnit) + offset
var sum = saturatingAdd(startedAt, unit, durationInUnit)
val restInUnit = rest.truncateTo(unit)
sum = saturatingAdd(sum, unit, restInUnit)
var restUnderUnit = rest - restInUnit
val restUnderUnitNs = restUnderUnit.inWholeNanoseconds
if (sum != 0L && restUnderUnitNs != 0L && (sum xor restUnderUnitNs) < 0L) {
// normalize offset to be the same sign as new startedAt
val correction = restUnderUnitNs.sign.toDuration(unit)
sum = saturatingAdd(sum, unit, correction)
restUnderUnit -= correction
}
val newValue = sum
val newOffset = if (newValue.isSaturated()) Duration.ZERO else restUnderUnit
return LongTimeMark(newValue, timeSource, newOffset)
}
override fun minus(other: ComparableTimeMark): Duration {
if (other !is LongTimeMark || this.timeSource != other.timeSource)
throw IllegalArgumentException("Subtracting or comparing time marks from different time sources is not possible: $this and $other")
// val thisValue = this.effectiveDuration()
// val otherValue = other.effectiveDuration()
// if (thisValue == otherValue && thisValue.isInfinite()) return Duration.ZERO
// return thisValue - otherValue
if (this.offset == other.offset && this.offset.isInfinite()) return Duration.ZERO
val offsetDiff = this.offset - other.offset
val startedAtDiff = (this.startedAt - other.startedAt).toDuration(timeSource.unit)
// println("$startedAtDiff, $offsetDiff")
return if (startedAtDiff == -offsetDiff) Duration.ZERO else startedAtDiff + offsetDiff
val startedAtDiff = saturatingOriginsDiff(this.startedAt, other.startedAt, timeSource.unit)
return startedAtDiff + (offset - other.offset)
}
override fun equals(other: Any?): Boolean =
other is LongTimeMark && this.timeSource == other.timeSource && (this - other) == Duration.ZERO
internal fun effectiveDuration(): Duration {
if (offset.isInfinite()) return offset
val unit = timeSource.unit
if (unit >= DurationUnit.MILLISECONDS) {
return startedAt.toDuration(unit) + offset
}
val scale = convertDurationUnit(1L, DurationUnit.MILLISECONDS, unit)
val startedAtMillis = startedAt / scale
val startedAtRem = startedAt % scale
override fun hashCode(): Int = offset.hashCode() * 37 + startedAt.hashCode()
return offset.toComponents { offsetSeconds, offsetNanoseconds ->
val offsetMillis = offsetNanoseconds / NANOS_IN_MILLIS
val offsetRemNanos = offsetNanoseconds % NANOS_IN_MILLIS
// add component-wise
(startedAtRem.toDuration(unit) + offsetRemNanos.nanoseconds) +
(startedAtMillis + offsetMillis).milliseconds +
offsetSeconds.seconds
}
}
override fun hashCode(): Int = effectiveDuration().hashCode()
override fun toString(): String = "LongTimeMark($startedAt${timeSource.unit.shortName()} + $offset (=${effectiveDuration()}), $timeSource)"
override fun toString(): String = "LongTimeMark($startedAt${timeSource.unit.shortName()} + $offset, $timeSource)"
}
override fun markNow(): ComparableTimeMark = LongTimeMark(read(), this, Duration.ZERO)
override fun markNow(): ComparableTimeMark = LongTimeMark(adjustedRead(), this, Duration.ZERO)
}
/**
@@ -137,6 +142,9 @@ public abstract class AbstractDoubleTimeSource(protected val unit: DurationUnit)
* timeSource += 10.seconds
* ```
*
* Time marks returned by this time source can be compared for difference with other time marks
* obtained from the same time source.
*
* Implementation note: the current reading value is stored as a [Long] number of nanoseconds,
* thus it's capable to represent a time range of approximately ±292 years.
* Should the reading value overflow as the result of [plusAssign] operation, an [IllegalStateException] is thrown.
@@ -146,6 +154,10 @@ public abstract class AbstractDoubleTimeSource(protected val unit: DurationUnit)
public class TestTimeSource : AbstractLongTimeSource(unit = DurationUnit.NANOSECONDS) {
private var reading: Long = 0L
init {
markNow() // fix zero reading in the super time source
}
override fun read(): Long = reading
/**
@@ -159,17 +171,25 @@ public class TestTimeSource : AbstractLongTimeSource(unit = DurationUnit.NANOSEC
*/
public operator fun plusAssign(duration: Duration) {
val longDelta = duration.toLong(unit)
reading = if (longDelta != Long.MIN_VALUE && longDelta != Long.MAX_VALUE) {
if (!longDelta.isSaturated()) {
// when delta fits in long, add it as long
val newReading = reading + longDelta
if (reading xor longDelta >= 0 && reading xor newReading < 0) overflow(duration)
newReading
reading = newReading
} else {
val delta = duration.toDouble(unit)
// when delta is greater than long, add it as double
val newReading = reading + delta
if (newReading > Long.MAX_VALUE || newReading < Long.MIN_VALUE) overflow(duration)
newReading.toLong()
val half = duration / 2
if (!half.toLong(unit).isSaturated()) {
val readingBefore = reading
try {
plusAssign(half)
plusAssign(duration - half)
} catch (e: IllegalStateException) {
reading = readingBefore
throw e
}
} else {
overflow(duration)
}
}
}
@@ -76,5 +76,5 @@ private fun saturatingFiniteDiff(value1: Long, value2: Long, unit: DurationUnit)
}
@Suppress("NOTHING_TO_INLINE")
private inline fun Long.isSaturated(): Boolean =
internal inline fun Long.isSaturated(): Boolean =
(this - 1) or 1 == Long.MAX_VALUE // == either MAX_VALUE or MIN_VALUE