Make TimeMark returned by TimeSource.Monotonic a value class

This value class wraps Long on JVM and Native thus reducing allocations
in time measurement scenarios when the default monotonic time source is
statically known.

KT-46132
This commit is contained in:
Ilya Gorbunov
2022-01-25 00:47:21 +03:00
committed by Space
parent 5b45ea9bc9
commit fede70d0d5
11 changed files with 281 additions and 49 deletions
+33 -6
View File
@@ -142,10 +142,18 @@ public inline operator fun kotlin.time.TimeMark.compareTo(other: kotlin.time.Tim
@kotlin.time.ExperimentalTime
public inline fun kotlin.time.TimeSource.measureTime(block: () -> kotlin.Unit): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.7")
@kotlin.time.ExperimentalTime
public inline fun kotlin.time.TimeSource.Monotonic.measureTime(block: () -> kotlin.Unit): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.3")
@kotlin.time.ExperimentalTime
public inline fun <T> kotlin.time.TimeSource.measureTimedValue(block: () -> T): kotlin.time.TimedValue<T>
@kotlin.SinceKotlin(version = "1.7")
@kotlin.time.ExperimentalTime
public inline fun <T> kotlin.time.TimeSource.Monotonic.measureTimedValue(block: () -> T): kotlin.time.TimedValue<T>
@kotlin.time.ExperimentalTime
@kotlin.SinceKotlin(version = "1.3")
@kotlin.internal.InlineOnly
@@ -198,6 +206,27 @@ public abstract class AbstractLongTimeSource : kotlin.time.TimeSource {
protected abstract fun read(): kotlin.Long
}
@kotlin.time.ExperimentalTime
@kotlin.SinceKotlin(version = "1.7")
@kotlin.jvm.JvmInline
public final inline class DefaultTimeMark : kotlin.time.TimeMark {
public open override fun elapsedNow(): kotlin.time.Duration
public open override operator fun equals(other: kotlin.Any?): kotlin.Boolean
public open override fun hasNotPassedNow(): kotlin.Boolean
public open override fun hasPassedNow(): kotlin.Boolean
public open override fun hashCode(): kotlin.Int
public open override operator fun minus(duration: kotlin.time.Duration): kotlin.time.DefaultTimeMark
public open override operator fun plus(duration: kotlin.time.Duration): kotlin.time.DefaultTimeMark
public open override fun toString(): kotlin.String
}
@kotlin.SinceKotlin(version = "1.6")
@kotlin.WasExperimental(markerClass = {kotlin.time.ExperimentalTime::class})
@kotlin.jvm.JvmInline
@@ -550,14 +579,12 @@ public final class TestTimeSource : kotlin.time.AbstractLongTimeSource {
@kotlin.SinceKotlin(version = "1.3")
@kotlin.time.ExperimentalTime
public abstract class TimeMark {
public constructor TimeMark()
public interface TimeMark {
public abstract fun elapsedNow(): kotlin.time.Duration
public final fun hasNotPassedNow(): kotlin.Boolean
public open fun hasNotPassedNow(): kotlin.Boolean
public final fun hasPassedNow(): kotlin.Boolean
public open fun hasPassedNow(): kotlin.Boolean
public open operator fun minus(duration: kotlin.time.Duration): kotlin.time.TimeMark
@@ -573,7 +600,7 @@ public interface TimeSource {
}
public object Monotonic : kotlin.time.TimeSource {
public open override fun markNow(): kotlin.time.TimeMark
public open override fun markNow(): kotlin.time.DefaultTimeMark
public open override fun toString(): kotlin.String
}
+33 -6
View File
@@ -142,10 +142,18 @@ public inline operator fun kotlin.time.TimeMark.compareTo(other: kotlin.time.Tim
@kotlin.time.ExperimentalTime
public inline fun kotlin.time.TimeSource.measureTime(block: () -> kotlin.Unit): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.7")
@kotlin.time.ExperimentalTime
public inline fun kotlin.time.TimeSource.Monotonic.measureTime(block: () -> kotlin.Unit): kotlin.time.Duration
@kotlin.SinceKotlin(version = "1.3")
@kotlin.time.ExperimentalTime
public inline fun <T> kotlin.time.TimeSource.measureTimedValue(block: () -> T): kotlin.time.TimedValue<T>
@kotlin.SinceKotlin(version = "1.7")
@kotlin.time.ExperimentalTime
public inline fun <T> kotlin.time.TimeSource.Monotonic.measureTimedValue(block: () -> T): kotlin.time.TimedValue<T>
@kotlin.time.ExperimentalTime
@kotlin.SinceKotlin(version = "1.3")
@kotlin.internal.InlineOnly
@@ -198,6 +206,27 @@ public abstract class AbstractLongTimeSource : kotlin.time.TimeSource {
protected abstract fun read(): kotlin.Long
}
@kotlin.time.ExperimentalTime
@kotlin.SinceKotlin(version = "1.7")
@kotlin.jvm.JvmInline
public final inline class DefaultTimeMark : kotlin.time.TimeMark {
public open override fun elapsedNow(): kotlin.time.Duration
public open override operator fun equals(other: kotlin.Any?): kotlin.Boolean
public open override fun hasNotPassedNow(): kotlin.Boolean
public open override fun hasPassedNow(): kotlin.Boolean
public open override fun hashCode(): kotlin.Int
public open override operator fun minus(duration: kotlin.time.Duration): kotlin.time.DefaultTimeMark
public open override operator fun plus(duration: kotlin.time.Duration): kotlin.time.DefaultTimeMark
public open override fun toString(): kotlin.String
}
@kotlin.SinceKotlin(version = "1.6")
@kotlin.WasExperimental(markerClass = {kotlin.time.ExperimentalTime::class})
@kotlin.jvm.JvmInline
@@ -550,14 +579,12 @@ public final class TestTimeSource : kotlin.time.AbstractLongTimeSource {
@kotlin.SinceKotlin(version = "1.3")
@kotlin.time.ExperimentalTime
public abstract class TimeMark {
public constructor TimeMark()
public interface TimeMark {
public abstract fun elapsedNow(): kotlin.time.Duration
public final fun hasNotPassedNow(): kotlin.Boolean
public open fun hasNotPassedNow(): kotlin.Boolean
public final fun hasPassedNow(): kotlin.Boolean
public open fun hasPassedNow(): kotlin.Boolean
public open operator fun minus(duration: kotlin.time.Duration): kotlin.time.TimeMark
@@ -573,7 +600,7 @@ public interface TimeSource {
}
public object Monotonic : kotlin.time.TimeSource {
public open override fun markNow(): kotlin.time.TimeMark
public open override fun markNow(): kotlin.time.DefaultTimeMark
public open override fun toString(): kotlin.String
}
@@ -7,12 +7,23 @@ package kotlin.time
import org.w3c.performance.GlobalPerformance
import org.w3c.performance.Performance
import kotlin.time.Duration.Companion.milliseconds
@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility
internal actual typealias DefaultTimeMarkReading = Any
@ExperimentalTime
internal interface DefaultTimeSource : TimeSource {
override fun markNow(): DefaultTimeMark
fun elapsedFrom(timeMark: DefaultTimeMark): Duration
fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark
}
@SinceKotlin("1.3")
@ExperimentalTime
internal actual object MonotonicTimeSource : TimeSource {
internal actual object MonotonicTimeSource : DefaultTimeSource, TimeSource { // TODO: interface should not be required here
private val actualSource: TimeSource = run {
private val actualSource: DefaultTimeSource = run {
val isNode: Boolean = js("typeof process !== 'undefined' && process.versions && !!process.versions.node")
if (isNode)
@@ -23,7 +34,10 @@ internal actual object MonotonicTimeSource : TimeSource {
}
override fun markNow(): TimeMark = actualSource.markNow()
actual override fun markNow(): DefaultTimeMark = actualSource.markNow()
actual override fun elapsedFrom(timeMark: DefaultTimeMark): Duration = actualSource.elapsedFrom(timeMark)
actual override fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark =
actualSource.adjustReading(timeMark, duration)
}
internal external interface Process {
@@ -32,27 +46,47 @@ internal external interface Process {
@SinceKotlin("1.3")
@ExperimentalTime
internal class HrTimeSource(val process: Process) : TimeSource {
internal class HrTimeSource(private val process: Process) : DefaultTimeSource {
override fun markNow(): DefaultTimeMark = DefaultTimeMark(process.hrtime())
override fun elapsedFrom(timeMark: DefaultTimeMark): Duration =
@Suppress("UNCHECKED_CAST")
process.hrtime(timeMark.reading as Array<Double>).let { (seconds, nanos) -> seconds.toDuration(DurationUnit.SECONDS) + nanos.toDuration(DurationUnit.NANOSECONDS) }
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)
}
}.let(::DefaultTimeMark)
override fun markNow(): TimeMark = object : TimeMark() {
val startedAt = process.hrtime()
override fun elapsedNow(): Duration =
process.hrtime(startedAt).let { (seconds, nanos) -> seconds.toDuration(DurationUnit.SECONDS) + nanos.toDuration(DurationUnit.NANOSECONDS) }
}
override fun toString(): String = "TimeSource(process.hrtime())"
}
@SinceKotlin("1.3")
@ExperimentalTime
internal class PerformanceTimeSource(val performance: Performance) : AbstractDoubleTimeSource(unit = DurationUnit.MILLISECONDS) {
override fun read(): Double = performance.now()
internal class PerformanceTimeSource(val performance: Performance) : DefaultTimeSource { // AbstractDoubleTimeSource(unit = DurationUnit.MILLISECONDS) {
private fun read(): Double = performance.now()
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))
override fun toString(): String = "TimeSource(self.performance.now())"
}
@SinceKotlin("1.3")
@ExperimentalTime
internal object DateNowTimeSource : AbstractDoubleTimeSource(unit = DurationUnit.MILLISECONDS) {
override fun read(): Double = kotlin.js.Date.now()
internal object DateNowTimeSource : DefaultTimeSource {
private fun read(): Double = kotlin.js.Date.now()
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))
override fun toString(): String = "TimeSource(Date.now())"
}
@@ -5,9 +5,24 @@
package kotlin.time
import kotlin.time.Duration.Companion.nanoseconds
@SinceKotlin("1.3")
@ExperimentalTime
internal actual object MonotonicTimeSource : AbstractLongTimeSource(unit = DurationUnit.NANOSECONDS), TimeSource { // TODO: interface should not be required here
override fun read(): Long = System.nanoTime()
internal actual object MonotonicTimeSource : TimeSource {
private fun read(): Long = System.nanoTime()
override fun toString(): String = "TimeSource(System.nanoTime())"
}
actual override fun markNow(): DefaultTimeMark = DefaultTimeMark(read())
actual fun elapsedFrom(timeMark: DefaultTimeMark): Duration = (read() - timeMark.reading).nanoseconds
// may have questionable contract
actual fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark =
DefaultTimeMark(timeMark.reading + duration.inWholeNanoseconds)
}
@Suppress("ACTUAL_WITHOUT_EXPECT") // visibility
internal actual typealias DefaultTimeMarkReading = Long
+28 -3
View File
@@ -5,6 +5,8 @@
package kotlin.time
import kotlin.jvm.JvmInline
/**
* A source of time for measuring time intervals.
*
@@ -30,7 +32,8 @@ public interface TimeSource {
* This time source returns its readings from a source of monotonic time when it is available in a target platform,
* and resorts to a non-monotonic time source otherwise.
*/
public object Monotonic : TimeSource by MonotonicTimeSource {
public object Monotonic : TimeSource {
override fun markNow(): DefaultTimeMark = MonotonicTimeSource.markNow()
override fun toString(): String = MonotonicTimeSource.toString()
}
@@ -46,7 +49,7 @@ public interface TimeSource {
*/
@SinceKotlin("1.3")
@ExperimentalTime
public abstract class TimeMark {
public interface TimeMark {
/**
* Returns the amount of time passed from this mark measured with the time source from which this mark was taken.
*
@@ -86,6 +89,28 @@ public abstract class TimeMark {
public fun hasNotPassedNow(): Boolean = elapsedNow().isNegative()
}
/**
* A specialized [TimeMark] returned by [TimeSource.Monotonic].
*
* This time mark is implemented as an inline value class wrapping a platform-dependent
* time reading value of the default monotonic time source, thus allowing to avoid additional boxing
* of that value.
*
* The operations [plus] and [minus] are also specialized to return [DefaultTimeMark] type.
*/
@ExperimentalTime
@SinceKotlin("1.7")
@JvmInline
public value class DefaultTimeMark internal constructor(internal val reading: DefaultTimeMarkReading) : TimeMark {
override fun elapsedNow(): Duration = MonotonicTimeSource.elapsedFrom(this)
override fun plus(duration: Duration): DefaultTimeMark = MonotonicTimeSource.adjustReading(this, duration)
override fun minus(duration: Duration): DefaultTimeMark = MonotonicTimeSource.adjustReading(this, -duration)
override fun hasPassedNow(): Boolean = !elapsedNow().isNegative()
override fun hasNotPassedNow(): Boolean = elapsedNow().isNegative()
}
internal expect class DefaultTimeMarkReading
@ExperimentalTime
@SinceKotlin("1.3")
@@ -109,7 +134,7 @@ public inline operator fun TimeMark.compareTo(other: TimeMark): Int = throw Erro
@ExperimentalTime
private class AdjustedTimeMark(val mark: TimeMark, val adjustment: Duration) : TimeMark() {
private class AdjustedTimeMark(val mark: TimeMark, val adjustment: Duration) : TimeMark {
override fun elapsedNow(): Duration = mark.elapsedNow() - adjustment
override fun plus(duration: Duration): TimeMark = AdjustedTimeMark(mark, adjustment + duration)
@@ -7,7 +7,11 @@ package kotlin.time
@SinceKotlin("1.3")
@ExperimentalTime
internal expect object MonotonicTimeSource : TimeSource
internal expect object MonotonicTimeSource : TimeSource {
override fun markNow(): DefaultTimeMark
fun elapsedFrom(timeMark: DefaultTimeMark): Duration
fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark
}
/**
* An abstract class used to implement time sources that return their readings as [Long] values in the specified [unit].
@@ -23,7 +27,7 @@ public abstract class AbstractLongTimeSource(protected val unit: DurationUnit) :
*/
protected abstract fun read(): Long
private class LongTimeMark(private val startedAt: Long, private val timeSource: AbstractLongTimeSource, private val offset: Duration) : TimeMark() {
private class LongTimeMark(private val startedAt: Long, private val timeSource: AbstractLongTimeSource, private val offset: Duration) : TimeMark {
override fun elapsedNow(): Duration = (timeSource.read() - startedAt).toDuration(timeSource.unit) - offset
override fun plus(duration: Duration): TimeMark = LongTimeMark(startedAt, timeSource, offset + duration)
}
@@ -45,7 +49,7 @@ public abstract class AbstractDoubleTimeSource(protected val unit: DurationUnit)
*/
protected abstract fun read(): Double
private class DoubleTimeMark(private val startedAt: Double, private val timeSource: AbstractDoubleTimeSource, private val offset: Duration) : TimeMark() {
private class DoubleTimeMark(private val startedAt: Double, private val timeSource: AbstractDoubleTimeSource, private val offset: Duration) : TimeMark {
override fun elapsedNow(): Duration = (timeSource.read() - startedAt).toDuration(timeSource.unit) - offset
override fun plus(duration: Duration): TimeMark = DoubleTimeMark(startedAt, timeSource, offset + duration)
}
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.
*/
@@ -39,6 +39,23 @@ public inline fun TimeSource.measureTime(block: () -> Unit): Duration {
return mark.elapsedNow()
}
/**
* Executes the given function [block] and returns the duration of elapsed time interval.
*
* The elapsed time is measured with the specified `this` [TimeSource.Monotonic] instance.
*/
@SinceKotlin("1.7")
@ExperimentalTime
public inline fun TimeSource.Monotonic.measureTime(block: () -> Unit): Duration {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val mark = markNow()
block()
return mark.elapsedNow()
}
/**
* Data class representing a result of executing an action, along with the duration of elapsed time interval.
@@ -83,3 +100,21 @@ public inline fun <T> TimeSource.measureTimedValue(block: () -> T): TimedValue<T
val result = block()
return TimedValue(result, mark.elapsedNow())
}
/**
* Executes the given [block] and returns an instance of [TimedValue] class, containing both
* the result of function execution and the duration of elapsed time interval.
*
* The elapsed time is measured with the specified `this` [TimeSource.Monotonic] instance.
*/
@SinceKotlin("1.7")
@ExperimentalTime
public inline fun <T> TimeSource.Monotonic.measureTimedValue(block: () -> T): TimedValue<T> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
val mark = markNow()
val result = block()
return TimedValue(result, mark.elapsedNow())
}
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.
*/
@@ -13,10 +13,12 @@ import kotlin.time.Duration.Companion.nanoseconds
class MeasureTimeTest {
private fun longRunningCalc(): String = buildString {
repeat(10) {
while (Random.nextDouble() >= 0.001);
append(('a'..'z').random())
companion object {
fun longRunningCalc(): String = buildString {
repeat(10) {
while (Random.nextDouble() >= 0.001);
append(('a'..'z').random())
}
}
}
+21 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
* 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.
*/
@@ -8,6 +8,7 @@ package test.time
import kotlin.test.*
import kotlin.time.*
import kotlin.time.Duration.Companion.microseconds
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.nanoseconds
@@ -57,4 +58,23 @@ class TimeMarkTest {
markPast1.assertHasPassed(true)
}
@Test
fun defaultTimeMarkAdjustment() {
val baseMark = TimeSource.Monotonic.markNow()
var markBefore = baseMark
markBefore -= 100.microseconds
markBefore -= 100.microseconds
val markAfter = baseMark + 100.microseconds
MeasureTimeTest.longRunningCalc()
val elapsedAfter = markAfter.elapsedNow()
val elapsedBase = baseMark.elapsedNow()
val elapsedBefore = markBefore.elapsedNow()
assertTrue(elapsedBefore >= elapsedBase + 200.microseconds)
assertTrue(elapsedAfter <= elapsedBase - 100.microseconds)
}
}
@@ -26,4 +26,14 @@ internal actual object MonotonicTimeSource : TimeSource, AbstractDoubleTimeSourc
override fun toString(): String =
if (performance != null) "TimeSource(globalThis.performance.now())" else "TimeSource(Date.now())"
}
}
@SinceKotlin("1.3")
@ExperimentalTime
internal actual object MonotonicTimeSource : TimeSource {
actual override fun markNow(): DefaultTimeMark = TODO("Wasm stdlib: MonotonicTimeSource::markNow")
actual fun elapsedFrom(timeMark: DefaultTimeMark): Duration = TODO("Wasm stdlib: MonotonicTimeSource")
actual fun adjustReading(timeMark: DefaultTimeMark, duration: Duration): DefaultTimeMark = TODO("Wasm stdlib: MonotonicTimeSource")
}
internal actual class DefaultTimeMarkReading // TODO: Long?