0

I was coding a stopwatch that refreshes every 0.1s.

suspend fun stopwatch() {
    val startTime = System.currentTimeMillis()
    var lastSeconds = 0L
    while (true) {
        // refresh rate
        delay(100)
        // calculate elapsed time
        val elapsedTime = System.currentTimeMillis() - startTime
        val seconds = elapsedTime / 1000
        // update UI if value changed
        if (seconds != lastSeconds) {
            print("${seconds}s\r")
            lastSeconds = seconds
        }
    }
}

Since delay is less expensive than Thread.sleep (and handler.post?), I thought I could refresh even more often.

suspend fun test() {
    val iterations = 10_000_000_000
    val timeWithNoDelay = measureTimeMillis {
        var count = 0L
        while(count < iterations) {
            count++
        }
    }
    val timeWithDelay = measureTimeMillis {
        var count = 0L
        while(count < iterations){
            delay(0)
            count++
        }
    }
    println("time: $timeWithNoDelay, $timeWithDelay")
    println("time per iteration: ${timeWithNoDelay.toDouble() / iterations}, ${timeWithDelay.toDouble() / iterations}")
    val timeDelay = timeWithDelay - timeWithNoDelay
    println("delay equivalent to ${timeDelay.toDouble() / timeWithNoDelay} additions")
}
time: 3637, 16543
time per iteration: 3.637E-7, 1.6543E-6
delay equivalent to 3.548529007423701 additions

Is there a reason to not use a delay smaller than 100, like delay(10) or even delay(0)? (for an android app) The stopwatch would refresh as often as possible while allowing other tasks in the (UI) queue to execute.

Edit: "If the given timeMillis is non-positive, this function returns immediately." (documentation) So the time difference I found was from if (timeMillis <= 0) return.

Using yield instead:

time: 414, 32715
iterations: 1000000000, time per iteration: 4.14E-7, 3.2715E-5
delay equivalent to 78.02173913043478 additions

Edit 2: Also, is it worth doing these calculations outside of the main thread withContext(Dispatchers.Default)? If there are 100 iterations per second it will only take 7ms.

import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield
import kotlin.system.measureTimeMillis

fun test(iterations: Long){
    val startTime = 0
    val formattedTime = MutableStateFlow("")
    val time = measureTimeMillis {
        for (i in 0..iterations) {
            val elapsedTime = System.currentTimeMillis() - startTime
            formattedTime.value = "$elapsedTime ms"
        }
    }
    println("iterations: $iterations, time: $time, time per iteration: ${time.toDouble()/iterations}")
    runBlocking {
        val startTime = 0
        val formattedTime = MutableStateFlow("")
        val timeWithDelay = measureTimeMillis {
            for (i in 0..iterations) {
                val elapsedTime = System.currentTimeMillis() - startTime
                formattedTime.value = "$elapsedTime ms"
                yield() // delay of 0
            }
        }
        println("iterations: $iterations, timeWithDelay: $timeWithDelay, time per iteration: ${timeWithDelay.toDouble()/iterations}")
    }
}

fun main(){
    test(100)
}
iterations: 100, time: 6, time per iteration: 0.06
iterations: 100, timeWithDelay: 7, time per iteration: 0.07
BPDev
  • 397
  • 1
  • 9
  • 3
    `delay` denies delaying for `timeMillis` less than 1L. – ocos Jun 01 '23 at 17:17
  • 3
    Use `yield()` instead. – Tenfour04 Jun 01 '23 at 18:04
  • 1
    Do you really want to consume 100% CPU just to show a stopwatch? As you show only seconds, delaying by 100ms sounds reasonable to me. Also, it is not entirely true "delay is less expensive than Thread.sleep". If you already do `sleep(100)` in your code, then changing it to `sleep(10)` shouldn't be a problem. In both cases you occupy a thread to handle your stopwatch. – broot Jun 02 '23 at 00:52
  • @broot In the context of an Android app that is already redrawing many times per second, adding some checks at the end of the queue shouldn't increase the CPU usage that much(?). In my head, `Thread.sleep` means there will be more context switches ([more details](https://stackoverflow.com/a/27571768/20898396)). – BPDev Jun 02 '23 at 18:07
  • 1
    @BPDev What do you mean by: "adding at the end of the queue"? Running `delay(0)`/`yield()` in a loop is pretty much a busy loop, increasing the CPU usage to 100%. And `delay()` also involves context switches - coroutines are scheduled on top of threads after all. If you need a super-precision, like microseconds, then some kind of a busy-loop is a must, but a drawback is CPU usage. But to be honest, I don't know why a stopwatch requires this. Human eye won't see imprecision of even 100-200ms, not to mention that you don't have any guarantees when exactly the UI will update. – broot Jun 03 '23 at 10:49
  • @broot Good points. The UI thread in Android works by having a queue of tasks. If you are in the background and want to update the UI, you can add a task to the queue using `handler.post`. Internally, the main dispatcher [also](https://stackoverflow.com/a/53526731/20898396) uses `handler.post` to map coroutines to the main thread. If I stay in the main thread, there shouldn't be context switches (there is only one thread, not a pool of threads), but I am wandering if it's good practice to do the work outside of the main (UI) thread using `withContext(Dispatchers.Default)`. – BPDev Jun 04 '23 at 14:19

0 Answers0