System.currentTimeMillis()使うの止めよう
Java/Kotlinにおいて、処理の経過時間を計測する場面では、従来から System.currentTimeMillis() がよく利用されてきました。
fun main() {
// 処理開始前の時刻を記録
val startTime = System.currentTimeMillis()
// 計測対象の処理
Thread.sleep(500)
// 処理終了後の時刻を記録
val endTime = System.currentTimeMillis()
// 経過時間を計算して出力
val elapsedTime = endTime - startTime
println("経過時間: ${elapsedTime}ms") // 「経過時間: 501ms」のように出力されます
}
しかし、この方法には以下のような課題があります。
-
システム時計に依存する
OSやNTP同期、手動による時刻変更によって値が変化する可能性があります -
経過時間計測に不向き
現在の「カレンダー上の時刻」を返すだけであり、処理時間計測のための単調なクロックではありません
TimeSource.Monotonicとは
Kotlin 1.6 以降、kotlin.time パッケージに TimeSource.Monotonic が導入されました。
これは単調増加する時間ソースであり、システム時計の変更に影響されず、経過時間計測に特化しています。
基本的な使い方
import kotlin.time.*
fun main() {
val mark = TimeSource.Monotonic.markNow()
// 計測対象の処理
Thread.sleep(500)
println("経過時間: ${mark.elapsedNow()}") //「経過時間: 500.795840ms」と出力されます
}
markNow()で計測開始時刻を記録し、elapsedNow()で経過時間を取得できます。
時間経過に関するテストとの親和性が高い
TimeSourceの大きな利点のひとつは、仮想時間を利用できる点です。
TestTimeSourceを用いることで、実時間を待たずに時間経過をシミュレーションできます。
import kotlin.time.*
import kotlin.time.Duration.Companion.milliseconds
fun main() {
val testTime = TestTimeSource()
val mark = testTime.markNow()
doSomethingAfterDelay(mark) // まだ0秒のため、何も出力されない
testTime += 6.seconds // 仮想的に6秒進める
doSomethingAfterDelay(mark) // 一瞬で「5秒経過しました」が出力される
}
private fun doSomethingAfterDelay(mark: TimeMark) {
if (mark.elapsedNow() > 5.seconds) {
println("5秒経過しました")
}
}
この方法を利用することで、以下のようなメリットがあります。
-
待ち時間ゼロでテストできる
実時間を進める代わりに仮想的に時間を進められるので、Thread.sleep()などの待機が不要。本来1分かかる処理も、時間を仮想的に1分進めれば即座に結果を検証可能 -
非同期処理やタイマーもすぐ確認できる
遅延処理のテストで、意図したタイミングまで仮想時間を進められるため、「テストが終わらない」「タイミングが合わない」といったストレスがなくなる
補足:measureTime measureTimedValueについて
処理時間の計測に関して、TimeSource.Monotonic以外にも、measureTimeとmeasureTimedValueが便利そうだったのでついでに紹介です。
measureTime
渡したブロックの実行時間を返します。処理時間だけ欲しい時に使えそうです。
import kotlin.time.measureTime
fun main() {
println("処理を開始します...")
// ブロック内の処理時間を計測する
val duration = measureTime {
// 時間を計測したい処理
println("重い処理を実行中...")
Thread.sleep(1000) // 1秒待機
println("...完了")
}
println("計測された時間: $duration") // 例: 1.02s
}
measureTimeの内部ではTimeSource.Monotonicが使われているようです。
@SinceKotlin("1.9")
@WasExperimental(ExperimentalTime::class)
public inline fun measureTime(block: () -> Unit): Duration {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return TimeSource.Monotonic.measureTime(block)
}
measureTimedValue
処理の実行結果と実行時間を両方返します。処理時間と一緒に結果も欲しい時に使えそうです。
import kotlin.time.measureTimedValue
// 何らかの計算を行い、結果を返す関数
fun calculateData(): String {
Thread.sleep(500) // 0.5秒かかる重い計算をシミュレート
return "計算結果データ"
}
fun main() {
// 処理を実行し、その結果と所要時間を同時に受け取る
val (result, duration) = measureTimedValue {
calculateData()
}
println("処理が完了しました。")
println("取得した結果: $result") // -> 計算結果データ
println("処理にかかった時間: $duration") // -> 508.45ms のような値
}
こちらも内部ではTimeSource.Monotonicが使われていました。
@SinceKotlin("1.9")
@WasExperimental(ExperimentalTime::class)
public inline fun <T> measureTimedValue(block: () -> T): TimedValue<T> {
contract {
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
}
return TimeSource.Monotonic.measureTimedValue(block)
}
感想
TimeSource.Monotonicに関する記事が少なかったので、簡単にではありますが書いてみました。
業務で時間経過に関する処理を実装する機会があり、タイムアウト時のテストコードを書くときに初めてTimeSource.Monotonicの存在を知りました。
仮想時間を使えるのでタイムアウトまで処理を待つ必要がなくなり、動作確認もスムーズに終わらせることができました。
めでたしめでたし👏