From 37c5f2c54f32d5269bba1b010b17f9f709a23074 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Wed, 3 Apr 2019 20:43:22 +0300 Subject: [PATCH] Initial prototype of Duration and Clocks API --- .../stdlib/js/src/kotlin/time/DurationUnit.kt | 37 +++ .../stdlib/js/src/kotlin/time/MonoClock.kt | 12 + .../stdlib/js/src/kotlin/time/conversions.kt | 10 + .../js/src/kotlin/time/formatToDecimals.kt | 10 + .../jvm/src/kotlin/time/DurationUnitJvm.kt | 26 ++ .../stdlib/jvm/src/kotlin/time/MonoClock.kt | 12 + .../jvm/src/kotlin/time/formatToDecimals.kt | 11 + libraries/stdlib/src/kotlin/time/Clock.kt | 31 +++ libraries/stdlib/src/kotlin/time/Clocks.kt | 55 +++++ libraries/stdlib/src/kotlin/time/Duration.kt | 225 ++++++++++++++++++ .../stdlib/src/kotlin/time/DurationUnit.kt | 56 +++++ .../stdlib/src/kotlin/time/measureTime.kt | 48 ++++ 12 files changed, 533 insertions(+) create mode 100644 libraries/stdlib/js/src/kotlin/time/DurationUnit.kt create mode 100644 libraries/stdlib/js/src/kotlin/time/MonoClock.kt create mode 100644 libraries/stdlib/js/src/kotlin/time/conversions.kt create mode 100644 libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt create mode 100644 libraries/stdlib/jvm/src/kotlin/time/DurationUnitJvm.kt create mode 100644 libraries/stdlib/jvm/src/kotlin/time/MonoClock.kt create mode 100644 libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt create mode 100644 libraries/stdlib/src/kotlin/time/Clock.kt create mode 100644 libraries/stdlib/src/kotlin/time/Clocks.kt create mode 100644 libraries/stdlib/src/kotlin/time/Duration.kt create mode 100644 libraries/stdlib/src/kotlin/time/DurationUnit.kt create mode 100644 libraries/stdlib/src/kotlin/time/measureTime.kt diff --git a/libraries/stdlib/js/src/kotlin/time/DurationUnit.kt b/libraries/stdlib/js/src/kotlin/time/DurationUnit.kt new file mode 100644 index 00000000000..6f0f161665a --- /dev/null +++ b/libraries/stdlib/js/src/kotlin/time/DurationUnit.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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 + +public actual enum class DurationUnit { + /** + * Time unit representing one nanosecond, which is 1/1000 of a microsecond. + */ + NANOSECONDS, + /** + * Time unit representing one microsecond, which is 1/1000 of a millisecond. + */ + MICROSECONDS, + /** + * Time unit representing one millisecond, which is 1/1000 of a second. + */ + MILLISECONDS, + /** + * Time unit representing one second. + */ + SECONDS, + /** + * Time unit representing one minute. + */ + MINUTES, + /** + * Time unit representing one hour. + */ + HOURS, + /** + * Time unit representing one day, which always equals 24 hours. + */ + DAYS; +} \ No newline at end of file diff --git a/libraries/stdlib/js/src/kotlin/time/MonoClock.kt b/libraries/stdlib/js/src/kotlin/time/MonoClock.kt new file mode 100644 index 00000000000..d558dcefe78 --- /dev/null +++ b/libraries/stdlib/js/src/kotlin/time/MonoClock.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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 + +public actual object MonoClock : Clock { + override fun mark(initialElapsed: Duration): ClockMark { + TODO("not implemented: base on high-res browser or node perf counter") + } +} \ No newline at end of file diff --git a/libraries/stdlib/js/src/kotlin/time/conversions.kt b/libraries/stdlib/js/src/kotlin/time/conversions.kt new file mode 100644 index 00000000000..98021b14bbf --- /dev/null +++ b/libraries/stdlib/js/src/kotlin/time/conversions.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. 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 + +public actual fun convertDurationUnit(value: Double, sourceUnit: DurationUnit, targetUnit: DurationUnit): Double { + TODO("not implemented") +} \ No newline at end of file diff --git a/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt b/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt new file mode 100644 index 00000000000..218d3a132b7 --- /dev/null +++ b/libraries/stdlib/js/src/kotlin/time/formatToDecimals.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2010-2019 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 + +internal actual fun formatToDecimals(value: Double, decimals: Int, unitName: String): String { + TODO("not implemented") +} \ No newline at end of file diff --git a/libraries/stdlib/jvm/src/kotlin/time/DurationUnitJvm.kt b/libraries/stdlib/jvm/src/kotlin/time/DurationUnitJvm.kt new file mode 100644 index 00000000000..578b136c60d --- /dev/null +++ b/libraries/stdlib/jvm/src/kotlin/time/DurationUnitJvm.kt @@ -0,0 +1,26 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +@file:kotlin.jvm.JvmMultifileClass() +@file:kotlin.jvm.JvmName("DurationUnitKt") + +package kotlin.time + +//Actual typealias 'DurationUnit' has no corresponding expected declaration +//The following declaration is incompatible because modality is different: +// public final expect enum class DurationUnit : Enum + +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual typealias DurationUnit = java.util.concurrent.TimeUnit + + +public actual fun convertDurationUnit(value: Double, sourceUnit: DurationUnit, targetUnit: DurationUnit): Double { + val sourceInTargets = targetUnit.convert(1, sourceUnit) + if (sourceInTargets > 0) + return value * sourceInTargets + + val otherInThis = sourceUnit.convert(1, targetUnit) + return value / otherInThis +} \ No newline at end of file diff --git a/libraries/stdlib/jvm/src/kotlin/time/MonoClock.kt b/libraries/stdlib/jvm/src/kotlin/time/MonoClock.kt new file mode 100644 index 00000000000..502426d4682 --- /dev/null +++ b/libraries/stdlib/jvm/src/kotlin/time/MonoClock.kt @@ -0,0 +1,12 @@ +/* + * Copyright 2010-2019 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 + +public actual object MonoClock : LongReadingClock(), Clock { // TODO: interface should not be required here + override fun reading(): Long = System.nanoTime() + override fun toString(): String = "Clock(System.nanoTime())" + override val unit: DurationUnit = DurationUnit.NANOSECONDS +} \ No newline at end of file diff --git a/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt b/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt new file mode 100644 index 00000000000..4fb82932b48 --- /dev/null +++ b/libraries/stdlib/jvm/src/kotlin/time/formatToDecimals.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2010-2019 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 java.util.* + +internal actual fun formatToDecimals(value: Double, decimals: Int, unitName: String): String = + String.format(Locale.ROOT, "%.${decimals}f%s", value, unitName) \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/time/Clock.kt b/libraries/stdlib/src/kotlin/time/Clock.kt new file mode 100644 index 00000000000..8623f76c71f --- /dev/null +++ b/libraries/stdlib/src/kotlin/time/Clock.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2010-2019 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 + +public interface Clock { + + /** + * Marks a time point on this clock. + * + * @param initialElapsed An initial value of [ClockMark.elapsedFrom] property of the resulting [ClockMark]: + * + * - pass [Duration.ZERO] to mark a time point that is now. + * - pass a positive duration to mark a time point in the past. + * - pass a negative duration to mark a time point in the future. + * + */ + fun mark(initialElapsed: Duration = Duration.ZERO): ClockMark + + companion object { + val Default: Clock get() = MonoClock + } + +} + +public interface ClockMark { + val clock: Clock + val elapsedFrom: Duration +} \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/time/Clocks.kt b/libraries/stdlib/src/kotlin/time/Clocks.kt new file mode 100644 index 00000000000..c5198b98793 --- /dev/null +++ b/libraries/stdlib/src/kotlin/time/Clocks.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2010-2019 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.js.JsName + +public expect object MonoClock : Clock + + +public abstract class LongReadingClock : Clock { + abstract fun reading(): Long + abstract val unit: DurationUnit + + override fun mark(initialElapsed: Duration): ClockMark = object : ClockMark { + val startedAt = reading() + override val clock: Clock get() = this@LongReadingClock + override val elapsedFrom: Duration get() = Duration(reading() - startedAt, this@LongReadingClock.unit) + initialElapsed + } +} + +public abstract class DoubleReadingClock : Clock { + abstract fun reading(): Double + abstract val unit: DurationUnit + + override fun mark(initialElapsed: Duration): ClockMark = object : ClockMark { + val startedAt = reading() + override val clock: Clock get() = this@DoubleReadingClock + override val elapsedFrom: Duration get() = Duration(reading() - startedAt, this@DoubleReadingClock.unit) + initialElapsed + } +} + + + +public class TestClock( + @JsName("readingValue") + var reading: Long = 0L, + override val unit: DurationUnit = DurationUnit.NANOSECONDS +) : LongReadingClock() { + override fun reading(): Long = reading +} + +/* +public interface WallClock { + fun currentTimeMilliseconds(): Long + + companion object : WallClock, LongReadingClock() { + override fun currentTimeMilliseconds(): Long = System.currentTimeMillis() + override fun reading(): Long = System.currentTimeMillis() + override val unit: DurationUnit get() = DurationUnit.MILLISECONDS + } +} +*/ \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/time/Duration.kt b/libraries/stdlib/src/kotlin/time/Duration.kt new file mode 100644 index 00000000000..538419e9667 --- /dev/null +++ b/libraries/stdlib/src/kotlin/time/Duration.kt @@ -0,0 +1,225 @@ +/* + * Copyright 2010-2019 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 + +private val storageUnit = DurationUnit.MICROSECONDS + +@Suppress("NON_PUBLIC_PRIMARY_CONSTRUCTOR_OF_INLINE_CLASS") +public inline class Duration internal constructor(internal val _value: Double) : Comparable { +// TODO: backend fails on init block, wait for KT-28055 + +// init { +// require(_value.isNaN().not()) +// } + + companion object { + val ZERO: Duration = Duration(0.0) + val MAX_VALUE: Duration = Duration(Double.MAX_VALUE) + val MIN_VALUE: Duration = Duration(-Double.MAX_VALUE) + val INFINITE: Duration = Duration(Double.POSITIVE_INFINITY) + + + // constructing from number of units + // option 1: static-like constructor functions + + fun days(value: Int) = Duration(value, DurationUnit.DAYS) + fun days(value: Long) = Duration(value, DurationUnit.DAYS) + fun days(value: Double) = Duration(value, DurationUnit.DAYS) + fun hours(value: Int) = Duration(value, DurationUnit.HOURS) + fun hours(value: Long) = Duration(value, DurationUnit.HOURS) + fun hours(value: Double) = Duration(value, DurationUnit.HOURS) + fun minutes(value: Int) = Duration(value, DurationUnit.MINUTES) + fun minutes(value: Long) = Duration(value, DurationUnit.MINUTES) + fun minutes(value: Double) = Duration(value, DurationUnit.MINUTES) + fun seconds(value: Int) = Duration(value, DurationUnit.SECONDS) + fun seconds(value: Long) = Duration(value, DurationUnit.SECONDS) + fun seconds(value: Double) = Duration(value, DurationUnit.SECONDS) + fun milliseconds(value: Int) = Duration(value, DurationUnit.MILLISECONDS) + fun milliseconds(value: Long) = Duration(value, DurationUnit.MILLISECONDS) + fun milliseconds(value: Double) = Duration(value, DurationUnit.MILLISECONDS) + fun microseconds(value: Int) = Duration(value, DurationUnit.MICROSECONDS) + fun microseconds(value: Long) = Duration(value, DurationUnit.MICROSECONDS) + fun microseconds(value: Double) = Duration(value, DurationUnit.MICROSECONDS) + fun nanoseconds(value: Int) = Duration(value, DurationUnit.NANOSECONDS) + fun nanoseconds(value: Long) = Duration(value, DurationUnit.NANOSECONDS) + fun nanoseconds(value: Double) = Duration(value, DurationUnit.NANOSECONDS) + } + + + // constructing from number of units + // option 0: constructors + + // unit is lost after construction + + constructor(value: Int, unit: DurationUnit) : this(convertDurationUnit(value.toDouble(), unit, storageUnit)) + constructor(value: Long, unit: DurationUnit) : this(convertDurationUnit(value.toDouble(), unit, storageUnit)) + constructor(value: Double, unit: DurationUnit) : this(convertDurationUnit(value, unit, storageUnit)) + + + // arithmetic operators + + operator fun unaryMinus(): Duration = Duration(-_value) + operator fun plus(other: Duration): Duration = Duration(_value + other._value) + operator fun minus(other: Duration): Duration = Duration(_value - other._value) + + // should we declare symmetric extension operators? + + operator fun times(scale: Int): Duration = Duration(_value * scale) + operator fun times(scale: Double): Duration = Duration(_value * scale) + + operator fun div(scale: Int): Duration = Duration(_value / scale) + operator fun div(scale: Double): Duration = Duration(_value / scale) + + operator fun div(other: Duration): Double = this._value / other._value + + fun isNegative(): Boolean = _value < 0 + fun isInfinite(): Boolean = _value.isInfinite() + fun isFinite(): Boolean = _value.isFinite() + + fun absoluteValue(): Duration = if (isNegative()) -this else this + + + override fun compareTo(other: Duration): Int = this._value.compareTo(other._value) + + + // splitting to components + + // problem: withComponents can be confused with 'wither' function + // perhaps better name would be 'letComponents' + + inline fun withComponents(action: (hours: Int, minutes: Int, seconds: Int, nanoseconds: Int) -> T): T = + action(inHours.toInt(), minutesComponent.toInt(), secondsComponent.toInt(), (inNanoseconds % 1e9).toInt()) + + inline fun withComponents(action: (days: Int, hours: Int, minutes: Int, seconds: Int, nanoseconds: Int) -> T): T = + action(inDays.toInt(), hoursComponent.toInt(), minutesComponent.toInt(), secondsComponent.toInt(), (inNanoseconds % 1e9).toInt()) + + + @PublishedApi + internal val hoursComponent: Double get() = inHours % 24 + @PublishedApi + internal val minutesComponent: Double get() = inMinutes % 60 + @PublishedApi + internal val secondsComponent: Double get() = inSeconds % 60 + + + // conversion to units + + // option 1: in- properties + + val inDays: Double get() = inUnits(DurationUnit.DAYS) + val inHours: Double get() = inUnits(DurationUnit.HOURS) + val inMinutes: Double get() = inUnits(DurationUnit.MINUTES) + val inSeconds: Double get() = inUnits(DurationUnit.SECONDS) + val inMilliseconds: Double get() = inUnits(DurationUnit.MILLISECONDS) + val inMicroseconds: Double get() = inUnits(DurationUnit.MICROSECONDS) + val inNanoseconds: Double get() = inUnits(DurationUnit.NANOSECONDS) + + fun inUnits(unit: DurationUnit) = convertDurationUnit(_value, storageUnit, unit) + + // option 2: to- functions + + fun toDays(): Double = toUnits(DurationUnit.DAYS) + fun toHours(): Double = toUnits(DurationUnit.HOURS) + fun toMinutes(): Double = toUnits(DurationUnit.MINUTES) + fun toSeconds(): Double = toUnits(DurationUnit.SECONDS) + fun toMilliseconds(): Double = toUnits(DurationUnit.MILLISECONDS) + fun toMicroseconds(): Double = toUnits(DurationUnit.MICROSECONDS) + fun toNanoseconds(): Double = toUnits(DurationUnit.NANOSECONDS) + + fun toUnits(unit: DurationUnit): Double = convertDurationUnit(_value, storageUnit, unit) + + + override fun toString(): String = buildString { + if (isInfinite()) { + if (isNegative()) append('-') + append("INFINITY") + } else { + // TODO: Find the most appropriate representation + append(_value) + append(storageUnit.shortName()) + } + } + + fun toString(unit: DurationUnit, decimals: Int = 0): String { +// if (decimals == 0) return toString(unit) + return formatToDecimals(inUnits(unit), decimals, unit.shortName()) + } + + + fun toIsoString(): String = buildString { + if (isNegative()) append('-') + append('P') + absoluteValue().withComponents { days, hours, minutes, seconds, nanoseconds -> + if (days != 0) + append(days).append('D') + + + if (days == 0 || seconds != 0 || nanoseconds != 0 || minutes != 0 || hours != 0) { + append('T') + val hasHours = hours != 0 || days != 0 + val hasSeconds = seconds != 0 || nanoseconds != 0 + val hasMinutes = minutes != 0 || (hasSeconds && hasHours) + if (hasHours) { + append(hours).append('H') + } + if (hasMinutes) { + append(minutes).append('M') + } + if (hasSeconds || (!hasHours && !hasMinutes)) { + append(seconds) + if (nanoseconds != 0) { + append('.') + val nss = nanoseconds.toString().padStart(9, '0') + when { + nanoseconds % 1_000_000 == 0 -> append(nss, 0, 3) + nanoseconds % 1_000 == 0 -> append(nss, 0, 6) + else -> append(nss) + } + } + append('S') + } + } + } + } + + +} + +// constructing from number of units +// option 2: constructor extension functions + + +val Int.nanoseconds get() = Duration(this, DurationUnit.NANOSECONDS) +val Long.nanoseconds get() = Duration(this, DurationUnit.NANOSECONDS) +val Double.nanoseconds get() = Duration(this, DurationUnit.NANOSECONDS) + +val Int.microseconds get() = Duration(this, DurationUnit.MICROSECONDS) +val Long.microseconds get() = Duration(this, DurationUnit.MICROSECONDS) +val Double.microseconds get() = Duration(this, DurationUnit.MICROSECONDS) + +val Int.milliseconds get() = Duration(this, DurationUnit.MILLISECONDS) +val Long.milliseconds get() = Duration(this, DurationUnit.MILLISECONDS) +val Double.milliseconds get() = Duration(this, DurationUnit.MILLISECONDS) + +val Int.seconds get() = Duration(this, DurationUnit.SECONDS) +val Long.seconds get() = Duration(this, DurationUnit.SECONDS) +val Double.seconds get() = Duration(this, DurationUnit.SECONDS) + +val Int.minutes get() = Duration(this, DurationUnit.MINUTES) +val Long.minutes get() = Duration(this, DurationUnit.MINUTES) +val Double.minutes get() = Duration(this, DurationUnit.MINUTES) + +val Int.hours get() = Duration(this, DurationUnit.HOURS) +val Long.hours get() = Duration(this, DurationUnit.HOURS) +val Double.hours get() = Duration(this, DurationUnit.HOURS) + +val Int.days get() = Duration(this, DurationUnit.DAYS) +val Long.days get() = Duration(this, DurationUnit.DAYS) +val Double.days get() = Duration(this, DurationUnit.DAYS) + + + +internal expect fun formatToDecimals(value: Double, decimals: Int, unitName: String): String diff --git a/libraries/stdlib/src/kotlin/time/DurationUnit.kt b/libraries/stdlib/src/kotlin/time/DurationUnit.kt new file mode 100644 index 00000000000..1cfcd2f2d14 --- /dev/null +++ b/libraries/stdlib/src/kotlin/time/DurationUnit.kt @@ -0,0 +1,56 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license + * that can be found in the license/LICENSE.txt file. + */ + +@file:kotlin.jvm.JvmMultifileClass() +@file:kotlin.jvm.JvmName("DurationUnitKt") + +package kotlin.time + + +public expect enum class DurationUnit { + /** + * Time unit representing one nanosecond, which is 1/1000 of a microsecond. + */ + NANOSECONDS, + /** + * Time unit representing one microsecond, which is 1/1000 of a millisecond. + */ + MICROSECONDS, + /** + * Time unit representing one millisecond, which is 1/1000 of a second. + */ + MILLISECONDS, + /** + * Time unit representing one second. + */ + SECONDS, + /** + * Time unit representing one minute. + */ + MINUTES, + /** + * Time unit representing one hour. + */ + HOURS, + /** + * Time unit representing one day, which always equals 24 hours. + */ + DAYS; +} + + +public expect fun convertDurationUnit(value: Double, sourceUnit: DurationUnit, targetUnit: DurationUnit): Double + + + +internal fun DurationUnit.shortName(): String = when (this) { + DurationUnit.NANOSECONDS -> "ns" + DurationUnit.MICROSECONDS -> "us" + DurationUnit.MILLISECONDS -> "ms" + DurationUnit.SECONDS -> "s" + DurationUnit.MINUTES -> "m" + DurationUnit.HOURS -> "h" + DurationUnit.DAYS -> "d" +} \ No newline at end of file diff --git a/libraries/stdlib/src/kotlin/time/measureTime.kt b/libraries/stdlib/src/kotlin/time/measureTime.kt new file mode 100644 index 00000000000..4835cde7afe --- /dev/null +++ b/libraries/stdlib/src/kotlin/time/measureTime.kt @@ -0,0 +1,48 @@ +/* + * Copyright 2010-2019 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.contracts.* + +public inline fun measureTime(action: () -> Unit): Duration { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + return MonoClock.measureTime(action) +} + + +public inline fun Clock.measureTime(action: () -> Unit): Duration { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + val mark = mark() + action() + return mark.elapsedFrom +} + + + +public data class DurationMeasured(val value: T, val duration: Duration) + +public inline fun withMeasureTime(action: () -> T): DurationMeasured { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + return MonoClock.withMeasureTime(action) +} + +public inline fun Clock.withMeasureTime(action: () -> T): DurationMeasured { + contract { + callsInPlace(action, InvocationKind.EXACTLY_ONCE) + } + + val mark = mark() + val result = action() + return DurationMeasured(result, mark.elapsedFrom) +}