Stop Using System.currentTimeMillis() for Benchmarking: Kotlin’s measureTimedValue and Duration API

The Old Way: Manual Time Measurement

If you’ve ever benchmarked a function in Kotlin or Android, you’ve probably written something like this:

val start = System.currentTimeMillis()
val result = doExpensiveWork()
val elapsed = System.currentTimeMillis() - start
Log.d("Perf", "doExpensiveWork took ${elapsed}ms, result=$result")

It works, but it’s noisy. You need three lines just to time one call, and you have to name both the timing variable and the result variable separately. Kotlin 1.9 made this significantly cleaner with measureTimedValue and a mature, type-safe Duration API that deserves a lot more attention than it gets.

Laptop with code and a clock concept
Precise timing in Kotlin doesn’t need three variables and raw milliseconds anymore.

measureTimedValue: One Call, Two Results

measureTimedValue (from kotlin.time) executes a block, records how long it took, and returns both the result of the block and the elapsed time as a TimedValue<T> data class. You destructure it in one line:

import kotlin.time.measureTimedValue

val (result, duration) = measureTimedValue {
    doExpensiveWork()
}

println("Result: $result, took: $duration")
// Result: 42, took: 134ms

The duration is a kotlin.time.Duration object — not a raw Long. That matters a lot, as we’ll see in a moment. There’s also measureTime if you only care about the elapsed time and not the return value:

import kotlin.time.measureTime

val elapsed = measureTime {
    preloadImages()
}
println("Preload finished in $elapsed")

The Duration Type: Stop Guessing Units

The real gem here is kotlin.time.Duration. Raw millisecond Longs are a constant source of bugs — is this value in ms, seconds, or nanoseconds? Did you forget to multiply by 1000 somewhere? Duration eliminates all of that by making the unit part of the type.

You can create a Duration using extension properties on numeric types:

import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.hours

val timeout    = 30.seconds
val cacheLifetime = 5.minutes
val animDelay  = 250.milliseconds
val sessionMax = 2.hours

// Arithmetic is unit-safe
val totalTimeout = timeout + 10.seconds  // 40s
val half = cacheLifetime / 2             // 2m 30s

println(timeout.inWholeSeconds)          // 30
println(cacheLifetime.inWholeMilliseconds) // 300000

Durations are also naturally comparable and can be formatted for display:

val fast = 120.milliseconds
val slow = 3.seconds

println(fast < slow)        // true
println(slow.toString())    // 3s
println(fast.toString())    // 120ms

// Components — useful for countdown timers
val bigDuration = 1.hours + 23.minutes + 45.seconds
bigDuration.toComponents { hours, minutes, seconds, _ ->
    println("%02d:%02d:%02d".format(hours, minutes, seconds)) // 01:23:45
}

Practical Android Example: Logging Slow Operations

Here’s a wrapper you can drop into any Android project to log slow operations automatically, using both measureTimedValue and a Duration threshold:

import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.measureTimedValue

inline fun  logIfSlow(
    tag: String,
    threshold: Duration = 100.milliseconds,
    block: () -> T
): T {
    val (result, duration) = measureTimedValue(block)
    if (duration > threshold) {
        Log.w(tag, "Slow operation detected: ${duration} (threshold: $threshold)")
    }
    return result
}

// Usage:
val user = logIfSlow("UserRepo", threshold = 50.milliseconds) {
    userRepository.getUserById(id)
}

Clean, reusable, and entirely unit-safe. No chance of accidentally comparing milliseconds with seconds.

Green code running on a monitor
Wrapping slow-path code with measureTimedValue gives you timing data with zero ceremony.

Using Duration With Coroutines

The Kotlin coroutines library has been updated to accept Duration directly wherever timeouts and delays used to take Longs. This makes coroutine code far more readable:

import kotlin.time.Duration.Companion.seconds
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.*

suspend fun fetchWithTimeout(): Data = withTimeout(10.seconds) {
    apiService.fetchData()
}

suspend fun retryWithDelay(retries: Int) {
    repeat(retries) { attempt ->
        try {
            return apiService.fetchData()
        } catch (e: IOException) {
            delay(500.milliseconds * (attempt + 1)) // exponential-ish backoff
        }
    }
}

Compare withTimeout(10.seconds) to withTimeout(10_000L) — the former is immediately obvious; the latter requires you to know (or remember) that the parameter is in milliseconds.

Bonus: Duration in WorkManager and AlarmManager

If you set up WorkManager constraints or periodic work, you typically deal with raw Long + TimeUnit pairs. You can convert your Duration to whatever the API needs:

import kotlin.time.Duration.Companion.hours
import java.util.concurrent.TimeUnit

val syncInterval = 6.hours

val request = PeriodicWorkRequestBuilder(
    syncInterval.inWholeMinutes, TimeUnit.MINUTES
).build()

Summary

measureTimedValue and Kotlin’s Duration type are a small but meaningful quality-of-life upgrade for any Kotlin codebase. They replace ad-hoc Long arithmetic with a type-safe, self-documenting API that makes timing logic impossible to misread. If you work with performance monitoring, retry logic, coroutine timeouts, or periodic tasks, there’s almost no reason to use raw millisecond Longs anymore.


This post was written by a human with the help of Claude, an AI assistant by Anthropic.

Scroll to Top