Saturate overflowing values when adjusting time marks
KT-46132
This commit is contained in:
@@ -7,6 +7,7 @@ package kotlin.time
|
||||
|
||||
import org.w3c.performance.GlobalPerformance
|
||||
import org.w3c.performance.Performance
|
||||
import kotlin.math.truncate
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility
|
||||
@@ -56,8 +57,8 @@ internal class HrTimeSource(private val process: Process) : DefaultTimeSource {
|
||||
override fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark =
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
(timeMark.reading as Array<Double>).let { (seconds, nanos) ->
|
||||
duration.toComponents { addSeconds, addNanos ->
|
||||
arrayOf<Double>(seconds + addSeconds, nanos + addNanos)
|
||||
duration.toComponents { _, addNanos ->
|
||||
arrayOf<Double>(sumCheckNaN(seconds + truncate(duration.toDouble(DurationUnit.SECONDS))), nanos + addNanos)
|
||||
}
|
||||
}.let(::DefaultTimeMark)
|
||||
|
||||
@@ -73,7 +74,7 @@ internal class PerformanceTimeSource(val performance: Performance) : DefaultTime
|
||||
override fun markNow(): DefaultTimeMark = DefaultTimeMark(read())
|
||||
override fun elapsedFrom(timeMark: DefaultTimeMark): Duration = (read() - timeMark.reading as Double).milliseconds
|
||||
override fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark =
|
||||
DefaultTimeMark(timeMark.reading as Double + duration.toDouble(DurationUnit.MILLISECONDS))
|
||||
DefaultTimeMark(sumCheckNaN(timeMark.reading as Double + duration.toDouble(DurationUnit.MILLISECONDS)))
|
||||
|
||||
override fun toString(): String = "TimeSource(self.performance.now())"
|
||||
}
|
||||
@@ -86,7 +87,9 @@ internal object DateNowTimeSource : DefaultTimeSource {
|
||||
override fun markNow(): DefaultTimeMark = DefaultTimeMark(read())
|
||||
override fun elapsedFrom(timeMark: DefaultTimeMark): Duration = (read() - timeMark.reading as Double).milliseconds
|
||||
override fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark =
|
||||
DefaultTimeMark(timeMark.reading as Double + duration.toDouble(DurationUnit.MILLISECONDS))
|
||||
DefaultTimeMark(sumCheckNaN(timeMark.reading as Double + duration.toDouble(DurationUnit.MILLISECONDS)))
|
||||
|
||||
override fun toString(): String = "TimeSource(Date.now())"
|
||||
}
|
||||
}
|
||||
|
||||
private fun sumCheckNaN(value: Double): Double = value.also { if (it.isNaN()) throw IllegalArgumentException("Summing infinities of different signs") }
|
||||
@@ -5,23 +5,21 @@
|
||||
|
||||
package kotlin.time
|
||||
|
||||
import kotlin.time.Duration.Companion.nanoseconds
|
||||
|
||||
@SinceKotlin("1.3")
|
||||
@ExperimentalTime
|
||||
internal actual object MonotonicTimeSource : TimeSource {
|
||||
private fun read(): Long = System.nanoTime()
|
||||
private val zero: Long = System.nanoTime()
|
||||
private fun read(): Long = System.nanoTime() - zero
|
||||
override fun toString(): String = "TimeSource(System.nanoTime())"
|
||||
|
||||
actual override fun markNow(): DefaultTimeMark = DefaultTimeMark(read())
|
||||
actual fun elapsedFrom(timeMark: DefaultTimeMark): Duration = (read() - timeMark.reading).nanoseconds
|
||||
actual fun elapsedFrom(timeMark: DefaultTimeMark): Duration =
|
||||
saturatingDiff(read(), timeMark.reading)
|
||||
|
||||
// may have questionable contract
|
||||
actual fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark =
|
||||
DefaultTimeMark(timeMark.reading + duration.inWholeNanoseconds)
|
||||
DefaultTimeMark(saturatingAdd(timeMark.reading, duration))
|
||||
}
|
||||
|
||||
|
||||
@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility
|
||||
internal actual typealias DefaultTimeMarkReading = Long
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package test.time
|
||||
|
||||
import kotlin.test.*
|
||||
import kotlin.time.*
|
||||
import kotlin.time.Duration.Companion.nanoseconds
|
||||
|
||||
@OptIn(ExperimentalTime::class)
|
||||
class TimeMarkJVMTest {
|
||||
|
||||
@Test
|
||||
fun longDurationElapsed() {
|
||||
TimeMarkTest().testLongDisplacement(TimeSource.Monotonic, { waitDuration -> Thread.sleep(waitDuration.inWholeMilliseconds) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultTimeMarkAdjustmentInfinite() {
|
||||
val baseMark = TimeSource.Monotonic.markNow()
|
||||
val longDuration = Long.MAX_VALUE.nanoseconds
|
||||
|
||||
val pastMark = baseMark - longDuration
|
||||
val infiniteFutureMark1 = pastMark + longDuration * 3
|
||||
|
||||
assertEquals(-Duration.INFINITE, infiniteFutureMark1.elapsedNow())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -54,6 +54,10 @@ public interface TimeMark {
|
||||
* Returns the amount of time passed from this mark measured with the time source from which this mark was taken.
|
||||
*
|
||||
* Note that the value returned by this function can change on subsequent invocations.
|
||||
*
|
||||
* @throws IllegalArgumentException an implementation may throw if calculating the elapsed time involves
|
||||
* adding a positive infinite duration to an infinitely distant past time mark or
|
||||
* a negative infinite duration to an infinitely distant future time mark.
|
||||
*/
|
||||
public abstract fun elapsedNow(): Duration
|
||||
|
||||
@@ -61,6 +65,12 @@ public interface TimeMark {
|
||||
* Returns a time mark on the same time source that is ahead of this time mark by the specified [duration].
|
||||
*
|
||||
* The returned time mark is more _late_ when the [duration] is positive, and more _early_ when the [duration] is negative.
|
||||
*
|
||||
* If the time mark is adjusted too far in the past or in the future, it may saturate to an infinitely distant time mark.
|
||||
* In that case, [elapsedNow] will return an infinite duration elapsed from such infinitely distant mark.
|
||||
*
|
||||
* @throws IllegalArgumentException an implementation may throw if a positive infinite duration is added to an infinitely distant past time mark or
|
||||
* a negative infinite duration is added to an infinitely distant future time mark.
|
||||
*/
|
||||
public open operator fun plus(duration: Duration): TimeMark = AdjustedTimeMark(this, duration)
|
||||
|
||||
@@ -68,6 +78,12 @@ public interface TimeMark {
|
||||
* Returns a time mark on the same time source that is behind this time mark by the specified [duration].
|
||||
*
|
||||
* The returned time mark is more _early_ when the [duration] is positive, and more _late_ when the [duration] is negative.
|
||||
*
|
||||
* If the time mark is adjusted too far in the past or in the future, it may saturate to an infinitely distant time mark.
|
||||
* In that case, [elapsedNow] will return an infinite duration elapsed from such infinitely distant mark.
|
||||
*
|
||||
* @throws IllegalArgumentException an implementation may throw if a positive infinite duration is subtracted from an infinitely distant future time mark or
|
||||
* a negative infinite duration is subtracted from an infinitely distant past time mark.
|
||||
*/
|
||||
public open operator fun minus(duration: Duration): TimeMark = plus(-duration)
|
||||
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||
*/
|
||||
|
||||
package kotlin.time
|
||||
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.nanoseconds
|
||||
|
||||
// Long time reading saturation math, shared between JVM and Native
|
||||
|
||||
internal fun saturatingAdd(longNs: Long, duration: Duration): Long {
|
||||
val durationNs = duration.inWholeNanoseconds
|
||||
if ((longNs - 1) or 1 == Long.MAX_VALUE) { // MIN_VALUE or MAX_VALUE - the reading is infinite
|
||||
return checkInfiniteSumDefined(longNs, duration, durationNs)
|
||||
}
|
||||
if ((durationNs - 1) or 1 == Long.MAX_VALUE) { // duration doesn't fit in Long nanos
|
||||
return saturatingAddInHalves(longNs, duration)
|
||||
}
|
||||
|
||||
val result = longNs + durationNs
|
||||
if (((longNs xor result) and (durationNs xor result)) < 0) {
|
||||
return if (longNs < 0) Long.MIN_VALUE else Long.MAX_VALUE
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun checkInfiniteSumDefined(longNs: Long, duration: Duration, durationNs: Long): Long {
|
||||
if (duration.isInfinite() && (longNs xor durationNs < 0)) throw IllegalArgumentException("Summing infinities of different signs")
|
||||
return longNs
|
||||
}
|
||||
|
||||
private fun saturatingAddInHalves(longNs: Long, duration: Duration): Long {
|
||||
val half = duration / 2
|
||||
if ((half.inWholeNanoseconds - 1) or 1 == Long.MAX_VALUE) {
|
||||
// this will definitely saturate
|
||||
return (longNs + duration.toDouble(DurationUnit.NANOSECONDS)).toLong()
|
||||
} else {
|
||||
return saturatingAdd(saturatingAdd(longNs, half), half)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun saturatingDiff(valueNs: Long, originNs: Long): Duration {
|
||||
if ((originNs - 1) or 1 == Long.MAX_VALUE) { // MIN_VALUE or MAX_VALUE
|
||||
return -(originNs.toDuration(DurationUnit.DAYS)) // saturate to infinity
|
||||
}
|
||||
val result = valueNs - originNs
|
||||
if ((result xor valueNs) and (result xor originNs).inv() < 0) {
|
||||
val resultMs = valueNs / NANOS_IN_MILLIS - originNs / NANOS_IN_MILLIS
|
||||
val resultNs = valueNs % NANOS_IN_MILLIS - originNs % NANOS_IN_MILLIS
|
||||
return resultMs.milliseconds + resultNs.nanoseconds
|
||||
}
|
||||
return result.nanoseconds
|
||||
}
|
||||
@@ -59,6 +59,53 @@ class TimeMarkTest {
|
||||
|
||||
}
|
||||
|
||||
fun testAdjustmentInfinite(timeSource: TimeSource) {
|
||||
val baseMark = timeSource.markNow()
|
||||
val infiniteFutureMark = baseMark + Duration.INFINITE
|
||||
val infinitePastMark = baseMark - Duration.INFINITE
|
||||
|
||||
assertEquals(-Duration.INFINITE, infiniteFutureMark.elapsedNow())
|
||||
assertTrue(infiniteFutureMark.hasNotPassedNow())
|
||||
|
||||
assertEquals(Duration.INFINITE, infinitePastMark.elapsedNow())
|
||||
assertTrue(infinitePastMark.hasPassedNow())
|
||||
|
||||
assertFailsWith<IllegalArgumentException> { infiniteFutureMark - Duration.INFINITE }
|
||||
assertFailsWith<IllegalArgumentException> { infinitePastMark + Duration.INFINITE }
|
||||
|
||||
|
||||
val longDuration = Long.MAX_VALUE.nanoseconds
|
||||
val long2Duration = longDuration * 2
|
||||
|
||||
val pastMark = baseMark - longDuration
|
||||
val futureMark = pastMark + long2Duration
|
||||
val sameMark = futureMark - longDuration
|
||||
val elapsedDiff = sameMark.elapsedNow() - baseMark.elapsedNow()
|
||||
assertTrue(elapsedDiff < 1.milliseconds, "$elapsedDiff")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun adjustmentInfinite() {
|
||||
testAdjustmentInfinite(TestTimeSource())
|
||||
}
|
||||
|
||||
fun testLongDisplacement(timeSource: TimeSource, wait: (Duration) -> Unit) {
|
||||
val baseMark = timeSource.markNow()
|
||||
val longDuration = Long.MAX_VALUE.nanoseconds
|
||||
val waitDuration = 10.milliseconds
|
||||
val pastMark = baseMark - longDuration
|
||||
wait(waitDuration)
|
||||
val elapsed = pastMark.elapsedNow()
|
||||
assertTrue(elapsed > longDuration)
|
||||
assertTrue(elapsed >= longDuration + waitDuration)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun longDisplacement() {
|
||||
val timeSource = TestTimeSource()
|
||||
testLongDisplacement(timeSource, { waitDuration -> timeSource += waitDuration })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultTimeMarkAdjustment() {
|
||||
val baseMark = TimeSource.Monotonic.markNow()
|
||||
@@ -77,4 +124,32 @@ class TimeMarkTest {
|
||||
assertTrue(elapsedBefore >= elapsedBase + 200.microseconds)
|
||||
assertTrue(elapsedAfter <= elapsedBase - 100.microseconds)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun defaultTimeMarkAdjustmentInfinite() {
|
||||
testAdjustmentInfinite(TimeSource.Monotonic)
|
||||
|
||||
// do the same with specialized methods
|
||||
val baseMark = TimeSource.Monotonic.markNow()
|
||||
val infiniteFutureMark = baseMark + Duration.INFINITE
|
||||
val infinitePastMark = baseMark - Duration.INFINITE
|
||||
|
||||
assertEquals(-Duration.INFINITE, infiniteFutureMark.elapsedNow())
|
||||
assertTrue(infiniteFutureMark.hasNotPassedNow())
|
||||
|
||||
assertEquals(Duration.INFINITE, infinitePastMark.elapsedNow())
|
||||
assertTrue(infinitePastMark.hasPassedNow())
|
||||
|
||||
assertFailsWith<IllegalArgumentException> { infiniteFutureMark - Duration.INFINITE }
|
||||
assertFailsWith<IllegalArgumentException> { infinitePastMark + Duration.INFINITE }
|
||||
|
||||
val longDuration = Long.MAX_VALUE.nanoseconds
|
||||
val long2Duration = longDuration * 2
|
||||
|
||||
val pastMark = baseMark - longDuration
|
||||
val futureMark = pastMark + long2Duration
|
||||
val sameMark = futureMark - longDuration
|
||||
val elapsedDiff = sameMark.elapsedNow() - baseMark.elapsedNow()
|
||||
assertTrue(elapsedDiff < 1.milliseconds, "$elapsedDiff")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,11 +27,13 @@ internal actual object MonotonicTimeSource : TimeSource {
|
||||
actual override fun markNow(): DefaultTimeMark = DefaultTimeMark(read())
|
||||
actual fun elapsedFrom(timeMark: DefaultTimeMark): Duration = (read() - timeMark.reading as Double).milliseconds
|
||||
actual fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark =
|
||||
DefaultTimeMark(timeMark.reading as Double + duration.toDouble(DurationUnit.MILLISECONDS))
|
||||
DefaultTimeMark(sumCheckNaN(timeMark.reading as Double + duration.toDouble(DurationUnit.MILLISECONDS)))
|
||||
|
||||
override fun toString(): String =
|
||||
if (performance != null) "TimeSource(globalThis.performance.now())" else "TimeSource(Date.now())"
|
||||
}
|
||||
|
||||
@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility
|
||||
internal actual typealias DefaultTimeMarkReading = Double
|
||||
internal actual typealias DefaultTimeMarkReading = Double
|
||||
|
||||
private fun sumCheckNaN(value: Double): Double = value.also { if (it.isNaN()) throw IllegalArgumentException("Summing infinities of different signs") }
|
||||
Reference in New Issue
Block a user