Reputation: 123
I am working on a video editing tool and I have a function that renders a scene. Depending on the complexity of that frame, it might take a while for this to happen. For the video editor to run smoothly, I have to allocate a certain time for rendering each frame. Within that time, if the frame was rendered, we show it to the user, if not, that frame gets skipped and we move on to the next one. The problem is that when I run a suspending function and ask coroutine to allocate certain milliseconds to it, it is in fact taking longer to happen. The actual code is very complex to include here, but I made a proxy code and that also has the same problem I am facing. Here is my proxy:
I have a render function that can take a long time. So, I have given it a maximum time (delay) of 10 seconds:
suspend fun MainActivity.render()
{
// in each frame, we print the time (in seconds) to the screen
// frame rate is 25
findViewById<TextView>(R.id.testTextView).text =
String.format("%.2f", currentFrame.toFloat() / 25f)
delay(10000)
}
Now, I have my play function that calls the render once in each frame. In this example, I have assumed that each second is made of 25 frames. So each frame has a length of 40 milliseconds (1000 / 25). So, the code for playback is as follows. In this code, each frame should either be rendered in 40 milliseconds or we should move on to the next cycle:
suspend fun MainActivity.play()
{
val elapsedTime = measureTimeMillis {
// I assume the total is 200 frames or 8 seconds.
while (currentFrame < 200)
{
withTimeoutOrNull(40) // make or break in 40 millies
{
withContext(Dispatchers.Main)
{
currentFrame += 1
render()
}
}
}
}
findViewById<TextView>(R.id.totalTextView).text =
String.format("It took: %.2f", elapsedTime.toFloat() / 1000f)
}
And finally, I call this function in a button:
class MainActivity : AppCompatActivity()
{
var currentFrame = 0
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.testButton).setOnClickListener {
CoroutineScope(Dispatchers.Main).launch {
play()
}
}
}
}
You can check out this image to see that when I run this code, what's meant to happen in 8 seconds, in fact, takes 8.37. Any suggestions for fixing this code, or generally achieving the same result?
Upvotes: 1
Views: 1157
Reputation: 28452
When thinking about cancelling some background coroutines/threads, we need to understand one thing: it is technically impossible (?) to stop/cancel/interrupt a running code. That means cancellations are always cooperative. This is the same for cancelling a coroutine in Kotlin and for interrupting a background thread with Thread.interrupt()
- code running in the interrupted thread need to intentionally check if it was interrupted, otherwise it will be still running normally.
We can see this clearly by executing this code:
val time = measureTimeMillis {
runBlocking {
val job = async {
Thread.sleep(3000)
}
delay(1000)
job.cancel()
}
}
println("time: $time")
Despite the fact our async task was cancelled after 1000ms, it is running for a full 3000ms. It was cancelled, but it can't stop working, because it is doing something (sleeping).
Now, change this code to:
val time = measureTimeMillis {
runBlocking {
val job = async {
repeat(6) {
Thread.sleep(500)
yield()
}
}
delay(1000)
job.cancel()
}
}
println("time: $time")
This time it takes about 1.5s - after being cancelled, it finishes in the next yield()
window.
To make your code more responsive to cancelling, you need to regularly check if a coroutine is still active or just call a suspending function that does this (yield()
is one option). Also, you should not really assume you can control timings with such precision. It will always take a little more time than you expected.
You can read more about this topic here
Upvotes: 3