diff --git a/libraries/stdlib/js/src/kotlin/time/MonoTimeSource.kt b/libraries/stdlib/js/src/kotlin/time/MonoTimeSource.kt index bc491be0396..88bb68474c1 100644 --- a/libraries/stdlib/js/src/kotlin/time/MonoTimeSource.kt +++ b/libraries/stdlib/js/src/kotlin/time/MonoTimeSource.kt @@ -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).let { (seconds, nanos) -> - duration.toComponents { addSeconds, addNanos -> - arrayOf(seconds + addSeconds, nanos + addNanos) + duration.toComponents { _, addNanos -> + arrayOf(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())" -} \ No newline at end of file +} + +private fun sumCheckNaN(value: Double): Double = value.also { if (it.isNaN()) throw IllegalArgumentException("Summing infinities of different signs") } \ No newline at end of file diff --git a/libraries/stdlib/jvm/src/kotlin/time/MonoTimeSource.kt b/libraries/stdlib/jvm/src/kotlin/time/MonoTimeSource.kt index 5c0fada822a..9348052ddd3 100644 --- a/libraries/stdlib/jvm/src/kotlin/time/MonoTimeSource.kt +++ b/libraries/stdlib/jvm/src/kotlin/time/MonoTimeSource.kt @@ -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 diff --git a/libraries/stdlib/jvm/test/time/TimeMarkJVMTest.kt b/libraries/stdlib/jvm/test/time/TimeMarkJVMTest.kt new file mode 100644 index 00000000000..f60bd3b3f63 --- /dev/null +++ b/libraries/stdlib/jvm/test/time/TimeMarkJVMTest.kt @@ -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()) + } + +} \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/time/TimeSource.kt b/libraries/stdlib/src/kotlin/time/TimeSource.kt index 226e737f1b9..b86fafab991 100644 --- a/libraries/stdlib/src/kotlin/time/TimeSource.kt +++ b/libraries/stdlib/src/kotlin/time/TimeSource.kt @@ -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) diff --git a/libraries/stdlib/src/kotlin/time/longSaturatedMath.kt b/libraries/stdlib/src/kotlin/time/longSaturatedMath.kt new file mode 100644 index 00000000000..cb526891001 --- /dev/null +++ b/libraries/stdlib/src/kotlin/time/longSaturatedMath.kt @@ -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 +} diff --git a/libraries/stdlib/test/time/TimeMarkTest.kt b/libraries/stdlib/test/time/TimeMarkTest.kt index 7718452f284..3a740595f69 100644 --- a/libraries/stdlib/test/time/TimeMarkTest.kt +++ b/libraries/stdlib/test/time/TimeMarkTest.kt @@ -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 { infiniteFutureMark - Duration.INFINITE } + assertFailsWith { 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 { infiniteFutureMark - Duration.INFINITE } + assertFailsWith { 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") + } } diff --git a/libraries/stdlib/wasm/src/kotlin/time/TimeSources.kt b/libraries/stdlib/wasm/src/kotlin/time/TimeSources.kt index cad4c327b50..d9e380716ed 100644 --- a/libraries/stdlib/wasm/src/kotlin/time/TimeSources.kt +++ b/libraries/stdlib/wasm/src/kotlin/time/TimeSources.kt @@ -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 \ No newline at end of file +internal actual typealias DefaultTimeMarkReading = Double + +private fun sumCheckNaN(value: Double): Double = value.also { if (it.isNaN()) throw IllegalArgumentException("Summing infinities of different signs") } \ No newline at end of file