Network/test/dorkboxTest/network/app/Stopwatch.kt
2023-07-21 00:20:23 +02:00

268 lines
8.4 KiB
Kotlin

/*
* Copyright 2023 dorkbox, llc
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright (C) 2008 The Guava Authors
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package dorkboxTest.network.app
import java.time.Duration
import java.util.concurrent.*
/**
* An object that measures elapsed time in nanoseconds. It is useful to measure elapsed time using
* this class instead of direct calls to [System.nanoTime] for a few reasons:
*
*
* * An alternate time source can be substituted, for testing or performance reasons.
* * As documented by `nanoTime`, the value returned has no absolute meaning, and can only
* be interpreted as relative to another timestamp returned by `nanoTime` at a different
* time. `Stopwatch` is a more effective abstraction because it exposes only these
* relative values, not the absolute ones.
*
*
*
* Basic usage:
*
* <pre>`Stopwatch stopwatch = Stopwatch.createStarted();
* doSomething();
* stopwatch.stop(); // optional
*
* Duration duration = stopwatch.elapsed();
*
* log.info("time: " + stopwatch); // formatted string like "12.3 ms"
`</pre> *
*
*
* Stopwatch methods are not idempotent; it is an error to start or stop a stopwatch that is
* already in the desired state.
*
*
* When testing code that uses this class, use [.createUnstarted] or [ ][.createStarted] to supply a fake or mock ticker. This allows you to simulate any valid
* behavior of the stopwatch.
*
*
* **Note:** This class is not thread-safe.
*
*
* **Warning for Android users:** a stopwatch with default behavior may not continue to keep
* time while the device is asleep. Instead, create one like this:
*
* <pre>`Stopwatch.createStarted(
* new Ticker() {
* public long read() {
* return android.os.SystemClock.elapsedRealtimeNanos();
* }
* });
`</pre> *
*
* @author Kevin Bourrillion
* @since 10.0
*/
class Stopwatch {
private val ticker: Ticker
/**
* Returns `true` if [.start] has been called on this stopwatch, and [.stop]
* has not been called since the last call to `start()`.
*/
var isRunning = false
private set
private var elapsedNanos: Long = 0
private var startTick: Long = 0
internal constructor() {
ticker = Ticker.systemTicker()
}
internal constructor(ticker: Ticker?) {
if (ticker == null) {
throw NullPointerException("ticker")
}
this.ticker = ticker
}
/**
* Starts the stopwatch.
*
* @return this `Stopwatch` instance
*
* @throws IllegalStateException if the stopwatch is already running.
*/
fun start(): Stopwatch {
check(!isRunning) { "This stopwatch is already running." }
isRunning = true
startTick = ticker.read()
return this
}
/**
* Stops the stopwatch. Future reads will return the fixed duration that had elapsed up to this
* point.
*
* @return this `Stopwatch` instance
*
* @throws IllegalStateException if the stopwatch is already stopped.
*/
fun stop(): Stopwatch {
val tick = ticker.read()
check(isRunning) { "This stopwatch is already stopped." }
isRunning = false
elapsedNanos += tick - startTick
return this
}
/**
* Sets the elapsed time for this stopwatch to zero, and places it in a stopped state.
*
* @return this `Stopwatch` instance
*/
fun reset(): Stopwatch {
elapsedNanos = 0
isRunning = false
return this
}
fun elapsedNanos(): Long {
return if (isRunning) ticker.read() - startTick + elapsedNanos else elapsedNanos
}
/**
* Returns the current elapsed time shown on this stopwatch, expressed in the desired time unit,
* with any fraction rounded down.
*
*
* **Note:** the overhead of measurement can be more than a microsecond, so it is generally
* not useful to specify [TimeUnit.NANOSECONDS] precision here.
*
*
* It is generally not a good idea to use an ambiguous, unitless `long` to represent
* elapsed time. Therefore, we recommend using [.elapsed] instead, which returns a
* strongly-typed [Duration] instance.
*
* @since 14.0 (since 10.0 as `elapsedTime()`)
*/
fun elapsed(desiredUnit: TimeUnit): Long {
return desiredUnit.convert(elapsedNanos(), TimeUnit.NANOSECONDS)
}
/**
* Returns the current elapsed time shown on this stopwatch as a [Duration]. Unlike [ ][.elapsed], this method does not lose any precision due to rounding.
*
* @since 22.0
*/
fun elapsed(): Duration {
return Duration.ofNanos(elapsedNanos())
}
/**
* Returns a string representation of the current elapsed time.
*/
override fun toString(): String {
return toString(elapsedNanos())
}
companion object {
/**
* Creates (but does not start) a new stopwatch using [System.nanoTime] as its time source.
*
* @since 15.0
*/
fun createUnstarted(): Stopwatch {
return Stopwatch()
}
/**
* Creates (but does not start) a new stopwatch, using the specified time source.
*
* @since 15.0
*/
fun createUnstarted(ticker: Ticker?): Stopwatch {
return Stopwatch(ticker)
}
/**
* Creates (and starts) a new stopwatch using [System.nanoTime] as its time source.
*
* @since 15.0
*/
fun createStarted(): Stopwatch {
return Stopwatch().start()
}
/**
* Creates (and starts) a new stopwatch, using the specified time source.
*
* @since 15.0
*/
fun createStarted(ticker: Ticker?): Stopwatch {
return Stopwatch(ticker).start()
}
fun toString(nanos: Long): String {
val unit = chooseUnit(nanos)
val value = nanos.toDouble() / TimeUnit.NANOSECONDS.convert(1, unit)
// Too bad this functionality is not exposed as a regular method call
return String.format("%.4g %s", value, abbreviate(unit))
}
fun chooseUnit(nanos: Long): TimeUnit {
if (TimeUnit.DAYS.convert(nanos, TimeUnit.NANOSECONDS) > 0) {
return TimeUnit.DAYS
}
if (TimeUnit.HOURS.convert(nanos, TimeUnit.NANOSECONDS) > 0) {
return TimeUnit.HOURS
}
if (TimeUnit.MINUTES.convert(nanos, TimeUnit.NANOSECONDS) > 0) {
return TimeUnit.MINUTES
}
if (TimeUnit.SECONDS.convert(nanos, TimeUnit.NANOSECONDS) > 0) {
return TimeUnit.SECONDS
}
if (TimeUnit.MILLISECONDS.convert(nanos, TimeUnit.NANOSECONDS) > 0) {
return TimeUnit.MILLISECONDS
}
return if (TimeUnit.MICROSECONDS.convert(nanos, TimeUnit.NANOSECONDS) > 0) {
TimeUnit.MICROSECONDS
} else TimeUnit.NANOSECONDS
}
private fun abbreviate(unit: TimeUnit): String {
return when (unit) {
TimeUnit.NANOSECONDS -> "ns"
TimeUnit.MICROSECONDS -> "\u03bcs" // μs
TimeUnit.MILLISECONDS -> "ms"
TimeUnit.SECONDS -> "s"
TimeUnit.MINUTES -> "min"
TimeUnit.HOURS -> "h"
TimeUnit.DAYS -> "d"
else -> throw AssertionError()
}
}
}
}