1. Overview

In this tutorial, we’re going to get familiar with a few different approaches to calculating elapsed time in Kotlin.

First, we’ll start by defining different ways of measuring time in modern computing. Then, we’ll evaluate a few different ways to calculate durations in Kotlin.

2. Definition of Clocks

In modern computing, there are at least two types of clocks. One such clock is known as the time-of-day or wall clock time. The time-of-day clock reports the actual date and time according to a calendar.

For instance, the System.currentTimeMillis() returns the number of seconds since the epoch time. So it’s a time-of-day clock in Java.

Time-of-day clocks are not guaranteed to move forward always or even to move forward reasonably. That might sound counterintuitive at first. However, such clocks can be inaccurate for a variety of reasons such as drifting from NTP servers, misconfigured NTP servers, sudden resets, VM pauses, leap seconds, and so on. Because of these sorts of oddities, a machine may experience a sudden jump forward or backward in time.

Therefore, these clocks are not good tools for measuring elapsed time or duration, as negative elapsed durations are a possibility.

Monotonic clocks are the second type of clock available in modern computers. Such clocks are usually measuring the number of nanoseconds since the computer has started or any other arbitrary event. Because of this, they are guaranteed to move forward always and consequently are good for measuring duration. The System.nanoTime() in Java is a monotonic clock.

Over the course of this tutorial, we’ll categorize each solution in terms of one of these two clocks.

3. The measureTimeMillis Function

To measure the elapsed time in milliseconds, we can use the measureTimeMillis(block: () -> Unit) function. All we have to do is to pass a block of code:

val elapsed = measureTimeMillis {
    Thread.sleep(100)
    println("Measuring time via measureTimeMillis")
}

assertThat(elapsed).isGreaterThanOrEqualTo(100)

The measureTimeMillis() function will execute the passed lambda expression and returns the elapsed time in milliseconds. With this approach, the lambda itself can’t return any value to the outer context.

It’s worth mentioning that the measureTimeMillis() function is using the System.currentTimeMillis() method under the hood to calculate the elapsed duration. Therefore, it’s based on a time-of-day clock and is not recommended to calculate durations, even though we can!

4. The measureNanoTime Function

In order to measure the elapsed time in nanoseconds, we can use the measureNanoTime(block: () -> Unit) function:

val elapsed = measureNanoTime {
    Thread.sleep(100)
    println("Measuring time via measureNanoTime")
}

assertThat(elapsed).isGreaterThanOrEqualTo(100 * 1_000_000)

Since each millisecond is 1,000,000 nanoseconds, we’re performing that multiplication here. Similar to measureTimeMillis(), this function only returns the elapsed time, so the lambda itself can’t return anything to the enclosing scope.

Moreover, the measureNanoTime() is using the System.nanoTime() method internally. Therefore, it’s based on a monotonic clock.

5. The TimeSource API

Kotlin 1.3 introduced the TimeSource and Duration APIs to measure time intervals.

For instance, here’s how we can measure the elapsed time with this new API:

@Test
@ExperimentalTime
fun `Given a block of code, When using measureTime, Then reports the elapsed time as a Duration`() {
    val elapsed: Duration = measureTime {
        Thread.sleep(100)
        println("Measuring time via measureTime")
    }

    assertThat(elapsed).isGreaterThanOrEqualTo(100.milliseconds) // deprecated since 1.6, removed from 1.8
    assertThat(elapsed).isGreaterThanOrEqualTo(100.toDuration(DurationUnit.MILLISECONDS))
}

Since this still is an experimental API, we should opt-in using the kotlin.time.ExperimentalTime annotation. The kotlin.time.measureTime(block: () -> Unit) function accepts a block of code as a lambda expression and calculates the elapsed time while executing it.

As opposed to measureTimeMillis() and measureNanoTime(), this function returns a kotlin.time.Duration instance instead of a primitive number. Moreover, to create Duration instances we used two approaches:

It’s worth mentioning that the first approach, using the Int.milliseconds extension property, has been removed since Kotlin 1.8. Therefore, we’ll use the second approach for verification in the rest of the tutorial.

The measureTime() is using the TimeSource.Monotonic implementation under the hood, so it’s based on a monotonic clock. Also, this function only returns the elapsed time, so the lambda itself can’t return anything to the enclosing context.

As opposed to the measureTime() function, the measureTimedValue(block: () -> T) experimental function can return a value in addition to the elapsed duration:

val (value, elapsed) = measureTimedValue {
    Thread.sleep(100)
    42
}

assertThat(value).isEqualTo(42)
assertThat(elapsed).isGreaterThanOrEqualTo(100.toDuration(DurationUnit.MILLISECONDS))

As shown above, the measureTimedValue() function returns a value to the outer context. Here we used a destructing pattern to decompose the returned TimedValue instance into a (value, elapsed) pair. Of course, we can also use the TimedValue itself as the return type without destructing the result:

val timedValue: TimedValue = measureTimedValue {
    Thread.sleep(100)
    42
}

assertThat(timedValue.value).isEqualTo(42)
assertThat(timedValue.duration).isGreaterThanOrEqualTo(100.toDuration(DurationUnit.MILLISECONDS))

6. Classic Java

Kotlin provides many nice and concise abstractions over System utilities of Java for measuring duration. However, it’s still possible to use a plain Java approach for this task.

For instance, here’s how we can achieve the same thing using the System.nanoTime():

val start = System.nanoTime()
Thread.sleep(100)

assertThat(System.nanoTime() - start).isGreaterThanOrEqualTo(100 * 1_000_000)

We can also achieve the same using the System.currentTimeMillis() static method. However, using a monotonic clock is always preferred when calculating elapsed times.

7. Conclusion

In this article, we saw different clocks available on most modern computers. Then, we evaluated different ways of calculating the elapsed time in Kotlin, including the current stable API, an experimental API, and a plain old Java API.

As usual, all the examples are available over on GitHub.


« 上一篇: Kotlin中的弃用