ross_a
03/17/2025, 2:57 PMdelay
is significantly different to using a fixedRateTimer
when used for a few millis?ross_a
03/17/2025, 2:59 PMval context = newSingleThreadContext("tick")
val timeMillis = tickConfig.interval.toMillis()
return channelFlow<Long> {
var count = 0L
while (true) {
val element = count++
trySend(element)
kotlinx.coroutines.delay(timeMillis)
}
}.flowOn(context)
gets me 16-17/second and
return callbackFlow {
var count = 0L
val fixedRateTimer = fixedRateTimer(period = tickConfig.interval.toMillis()) {
trySendBlocking(count++)
}
awaitClose {
fixedRateTimer.cancel()
}
}
gets me to the expected 20/secondross_a
03/17/2025, 3:00 PMkevin.cianfarini
03/17/2025, 3:05 PMross_a
03/17/2025, 3:06 PMnewSingleThreadContext
is doing behind the sceneskevin.cianfarini
03/17/2025, 3:08 PMross_a
03/17/2025, 3:11 PMval timeMillis = tickConfig.interval.toMillis()
return GlobalScope.produce(newSingleThreadContext("tick"), capacity = UNLIMITED) {
var count = 0L
while (true) {
val element = count++
trySend(element)
kotlinx.coroutines.delay(timeMillis)
}
}.consumeAsFlow()
still 16-17 ticks/sec instead of 20
Going to fall back to not using delay for ticksDmitry Khalanskiy [JB]
03/17/2025, 3:20 PMcount++
and the subsequent trySend(element)
take 8-10 milliseconds. delay(50)
then waits for extra 50 milliseconds. In total, each iteration takes 58-60 milliseconds, corresponding to 16-17 iterations per second.
I don't know what fixedRateTimer
is (it's not part of our library), but from its name, I'd assume that it ensures that the average rate (that is, the number of iterations per unit of time) stays constant.
This can be implemented with delay
manually like this:
val iterationTime = measureTime {
val element = count++
trySend(element)
}
val toWait = iterationLength - iterationTime
delay(toWait)
ross_a
03/17/2025, 3:21 PMkevin.cianfarini
03/17/2025, 3:22 PMfixedRateTimer
will ensure that the time between the start of a previous task and the start of the subsequent task remains constant. Your code with delay doesn’t actually do that, so you’re building in some extra time to each interval periodkevin.cianfarini
03/17/2025, 3:23 PMfixedRateTimer
is doing, I think you’ll need to introduce some concurrency.kevin.cianfarini
03/17/2025, 3:24 PMdelay
. I had to introduce a mode that either cancels the previous job or runs them concurrently because of this problem.kevin.cianfarini
03/17/2025, 3:25 PMkevin.cianfarini
03/17/2025, 3:26 PMval context = newSingleThreadContext("tick")
val timeMillis = tickConfig.interval.toMillis()
return channelFlow<Long> {
var count = 0L
while (true) {
// Executes in, for example, 10ms ///
val element = count++
trySend(element)
////////////////////////////////////
// Delays for 50ms
kotlinx.coroutines.delay(timeMillis)
////////////////////////////////////
}
}.flowOn(context)
kevin.cianfarini
03/17/2025, 3:27 PMfixedRateTimer
will ensure that the time it takes to execute your task is done within the period that it’s specified to execute, in this case the 50msross_a
03/17/2025, 3:28 PMDmitry Khalanskiy [JB]
03/17/2025, 3:28 PMross_a
03/17/2025, 3:29 PMross_a
03/17/2025, 3:31 PMval toMillis = tickConfig.interval.toMillis()
return flow {
var count = 0L
var next = System.currentTimeMillis()
while (true) {
val start = System.currentTimeMillis()
println("Emit ${measureTimeMillis {
emit(count++)
}}")
next = start + toMillis
val toDelay = next - System.currentTimeMillis()
println("To delay: $toDelay")
println("Delay ${measureTimeMillis {
delay(toDelay)
}}")
}
}.flowOn(newSingleThreadContext("tick"))
Emit 0
To delay: 50
Delay 62
Delay 62
Delay 62
Emit 0
To delay: 50
Delay 62
Emit 0
Emit 0
To delay: 50
Emit 0
To delay: 50
To delay: 50
Delay 62
Delay 62
Emit 0
To delay: 50
ross_a
03/17/2025, 3:31 PMDmitry Khalanskiy [JB]
03/17/2025, 3:40 PMdelay
seems wildly inaccurate, but at the same time, surprisingly consistent. I'll need to take a look at this, but I'm knee-deep in kotlinx-datetime
at the moment. Could you file an issue to kotlinx.coroutines
, preferably with a self-contained reproducer?ross_a
03/17/2025, 3:40 PMross_a
03/17/2025, 3:41 PMkevin.cianfarini
03/17/2025, 3:54 PMimport kotlin.time.measureTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.newSingleThreadContext
fun main() = runBlocking(newSingleThreadContext("foo")) {
repeat(20) {
val elapsed = measureTime {
delay(50)
}
println("Supposed to delay for 50ms, delayed for $elapsed.")
}
}
It prints the following output:
Supposed to delay for 50ms, delayed for 60.950070ms.
Supposed to delay for 50ms, delayed for 50.307421ms.
Supposed to delay for 50ms, delayed for 50.305745ms.
Supposed to delay for 50ms, delayed for 50.352972ms.
Supposed to delay for 50ms, delayed for 50.336299ms.
Supposed to delay for 50ms, delayed for 50.306166ms.
Supposed to delay for 50ms, delayed for 50.322560ms.
Supposed to delay for 50ms, delayed for 50.307023ms.
Supposed to delay for 50ms, delayed for 50.352728ms.
Supposed to delay for 50ms, delayed for 50.386073ms.
Supposed to delay for 50ms, delayed for 50.342678ms.
Supposed to delay for 50ms, delayed for 50.378550ms.
Supposed to delay for 50ms, delayed for 50.311117ms.
Supposed to delay for 50ms, delayed for 50.314146ms.
Supposed to delay for 50ms, delayed for 50.571076ms.
Supposed to delay for 50ms, delayed for 50.340461ms.
Supposed to delay for 50ms, delayed for 50.326914ms.
Supposed to delay for 50ms, delayed for 50.338576ms.
Supposed to delay for 50ms, delayed for 50.314560ms.
Supposed to delay for 50ms, delayed for 50.352552ms.
ross_a
03/17/2025, 4:09 PMSupposed to delay for 50ms, delayed for 55.788200ms.
Supposed to delay for 50ms, delayed for 50.378400ms.
Supposed to delay for 50ms, delayed for 61.915500ms.
Supposed to delay for 50ms, delayed for 60.764900ms.
Supposed to delay for 50ms, delayed for 61.419900ms.
Supposed to delay for 50ms, delayed for 61.910500ms.
Supposed to delay for 50ms, delayed for 61.554600ms.
Supposed to delay for 50ms, delayed for 62.495300ms.
Supposed to delay for 50ms, delayed for 61.706800ms.
kevin.cianfarini
03/17/2025, 4:10 PMkevin.cianfarini
03/17/2025, 4:11 PMimport kotlin.time.measureTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
repeat(20) {
val elapsed = measureTime {
delay(50)
}
println("Supposed to delay for 50ms, delayed for $elapsed.")
}
}
ross_a
03/17/2025, 4:12 PMkevin.cianfarini
03/17/2025, 4:13 PMross_a
03/17/2025, 4:13 PMross_a
03/17/2025, 4:20 PMlast = System.currentTimeMillis()
var x = 0
val fixedRateTimer = executor.scheduleAtFixedRate(
{
val now = System.currentTimeMillis()
println("Fixed2 ${now - last}")
last = now
if (x++ == 20) System.exit(0)
},
delay,
delay,
TimeUnit.MILLISECONDS
)
Even this is giving me
Fixed2 59
Fixed2 46
Fixed2 47
Fixed2 62
Fixed2 47
Fixed2 47
Fixed2 46
Fixed2 47
Fixed2 62
Fixed2 47
Fixed2 45
Fixed2 47
Fixed2 62
Fixed2 45
Fixed2 47
Fixed2 46
Fixed2 62
Fixed2 46
Fixed2 48
Fixed2 46
Fixed2 46
kevin.cianfarini
03/17/2025, 4:25 PMdelay
in the above example?ross_a
03/17/2025, 4:26 PMkevin.cianfarini
03/17/2025, 4:26 PMross_a
03/17/2025, 4:27 PMkevin.cianfarini
03/17/2025, 4:27 PMross_a
03/17/2025, 4:28 PMkevin.cianfarini
03/17/2025, 4:28 PMkevin.cianfarini
03/17/2025, 4:28 PMross_a
03/17/2025, 4:37 PMscheduleAtFixedRate
runs fast sometimes and slow other times so averages out.
Also confused why I don't see this issue manifest as stuttering in other applications.
Some sort of issue with nanoTime vs millis?kevin.cianfarini
03/17/2025, 7:43 PMkevin.cianfarini
03/17/2025, 7:44 PMimport kotlin.time.measureTime
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
fun main() = runBlocking {
repeat(20) {
val elapsed = measureTime {
delay(50)
}
println("Supposed to delay for 50ms, delayed for $elapsed.")
}
}
ross_a
03/17/2025, 8:37 PMArsen
03/18/2025, 9:08 AMReal-time OS
so you never guaranteed to get time slices at accurate intervals as well as accurate duration. There can be interruptions
from hardware, slice duration adjustments due to internal heuristics e.g. thread priorities, other processes "usage patterns" e.g. high frequency of user inputs (aka games or GUI app) so OS may give them "special treatment", etc.
There are special Real-time OS
that are used in specific domains e.g. medical or aircraft hardware.
Though you might be overengineering your task and slight inaccuracy might be unnoticed by user.ross_a
03/18/2025, 9:12 AMdelay
.Arsen
03/18/2025, 9:14 AMross_a
03/18/2025, 9:15 AMArsen
03/18/2025, 9:16 AMross_a
03/18/2025, 9:16 AMross_a
03/18/2025, 9:18 AMross_a
03/18/2025, 9:20 AMArsen
03/18/2025, 9:21 AMross_a
03/18/2025, 9:23 AMross_a
03/18/2025, 9:24 AMscheduleAtFixedRate
seems to average it out (sometimes less than 50ms) threw me off even furtherDmitry Khalanskiy [JB]
03/18/2025, 10:21 AMfixedRateTimer
ensures the correct timing if even native code fails to do that. Maybe we could utilize the same technique in our delay
implementation.ross_a
03/18/2025, 10:24 AMdelay
Fixed2 47
Fixed2 46
Fixed2 47
Fixed2 62
Fixed2 47
Fixed2 45
However I surfaced https://github.com/Kotlin/kotlinx.coroutines/issues/3680 - I think it would be a useful addition (or option) if there is any new API addedDmitry Khalanskiy [JB]
03/18/2025, 10:25 AMDmitry Khalanskiy [JB]
03/18/2025, 10:28 AMIt seemed to just have some below and some aboveOh, that's neat. So, they must be keeping track of the rate of the previous tasks and compensate for too slow (too fast) rates by adjusting the later ones.
Arsen
03/18/2025, 10:28 AMSystem.currentTimeMillis()
checksArsen
03/18/2025, 10:30 AM// java/util/Timer.java
private void mainLoop() {
Arsen
03/18/2025, 10:35 AM.wait(executionTime - currentTime)
is the key, though it still depends on the OS scheduler to awake thread accurately 🤔