Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit eadb4ff8 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add suspend utils that will be necessary for loading assets safely." into main

parents ae2ba8a6 4645afb7
Loading
Loading
Loading
Loading
+97 −14
Original line number Diff line number Diff line
@@ -18,11 +18,15 @@ package com.android.systemui.util.kotlin

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import java.lang.System.currentTimeMillis
import kotlin.time.Duration.Companion.milliseconds
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.Test
import org.junit.runner.RunWith
@@ -33,24 +37,103 @@ class RaceSuspendTest : SysuiTestCase() {
    @Test
    fun raceSimple() = runBlocking {
        val winner = CompletableDeferred<Int>()
        val result = async {
            race(
                { winner.await() },
                { awaitCancellation() },
            )
        }
        val result = async { race({ winner.await() }, { awaitCancellation() }) }
        winner.complete(1)
        assertThat(result.await()).isEqualTo(1)
    }

    @Test fun raceImmediate() = runBlocking { assertThat(race<Int>({ 1 }, { 2 })).isEqualTo(1) }
}

@SmallTest
@RunWith(AndroidJUnit4::class)
class TimeoutSuspendTest : SysuiTestCase() {
    @Test
    fun launchWithTimeout() = runBlocking {
        val result = launchWithTimeout(500.milliseconds) { 1 }
        assertThat(result.isSuccess).isTrue()
    }

    @Test
    fun raceImmediate() = runBlocking {
        assertThat(
                race<Int>(
                    { 1 },
                    { 2 },
                )
            )
            .isEqualTo(1)
    fun launchWithTimeout_timesOutWhenDelay() = runBlocking {
        val launchStart = currentTimeMillis()
        val result =
            launchWithTimeout(100.milliseconds) {
                println("Starting delay")
                delay(1000.milliseconds)
                println("Finished delay")
            }
        val launchDurationMs = currentTimeMillis() - launchStart
        // VERIFY that block timed out
        assertThat(result.isSuccess).isFalse()
        // VERIFY that the result was returned faster than the delay's minimum duration
        assertThat(launchDurationMs).isAtMost(999)
    }

    @Test
    fun awaitAndCancelOnTimeout_coroutine() = runBlocking {
        val deferred = CompletableDeferred<Int>()
        launch {
            delay(100.milliseconds)
            deferred.complete(1)
        }
        val result = deferred.awaitAndCancelOnTimeout(500.milliseconds)
        assertThat(result.isSuccess).isTrue()
        assertThat(result.getOrNull()).isEqualTo(1)
    }

    @Test
    fun awaitAndCancelOnTimeout_thread_sleep() = runBlocking {
        val deferred = CompletableDeferred<Int>()
        Thread {
                Thread.sleep(100)
                deferred.complete(1)
            }
            .start()
        val result = deferred.awaitAndCancelOnTimeout(500.milliseconds)
        assertThat(result.isSuccess).isTrue()
        assertThat(result.getOrNull()).isEqualTo(1)
    }

    @Test
    fun awaitAndCancelOnTimeout_thread_sleepForeverUntilCancelled() = runBlocking {
        val deferred = CompletableDeferred<Int>()
        val thread = Thread {
            while (!deferred.isCancelled) {
                Thread.sleep(100)
            }
        }
        thread.start()
        val result = deferred.awaitAndCancelOnTimeout(500.milliseconds)
        assertThat(result.isFailure).isTrue()
        delay(200.milliseconds)
        assertThat(thread.isAlive).isFalse()
    }

    @Test
    fun awaitAndCancelOnTimeout_thread_busyLoop() = runBlocking {
        val deferred = CompletableDeferred<Int>()
        val thread = Thread {
            val busyLoopStart = currentTimeMillis()
            println("Starting busy loop")
            while (currentTimeMillis() - busyLoopStart < 1000) {
                // busy loop
            }
            println("Finished busy loop")
        }
        val operationStart = currentTimeMillis()
        thread.start()
        val result = deferred.awaitAndCancelOnTimeout(500.milliseconds)
        val operationDurationMs = currentTimeMillis() - operationStart
        // VERIFY that the thread is still running
        assertThat(thread.isAlive).isTrue()
        // VERIFY that block timed out
        assertThat(result.isSuccess).isFalse()
        // VERIFY that the result was returned faster than the busy loop's minimum duration
        assertThat(operationDurationMs).isAtMost(999)
        // CLEANUP: let the busy loop end
        delay(1000.milliseconds)
        // VERIFY that the thread is stopped
        assertThat(thread.isAlive).isFalse()
    }
}
+50 −1
Original line number Diff line number Diff line
@@ -16,9 +16,14 @@

package com.android.systemui.util.kotlin

import com.android.app.tracing.coroutines.launchTraced as launch
import java.util.concurrent.CancellationException
import java.util.concurrent.TimeoutException
import kotlin.time.Duration
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.coroutineScope
import com.android.app.tracing.coroutines.launchTraced as launch
import kotlinx.coroutines.delay

/**
 * Runs the given [blocks] in parallel, returning the result of the first one to complete, and
@@ -33,3 +38,47 @@ suspend fun <R> race(vararg blocks: suspend () -> R): R = coroutineScope {
    }
    completion.await().also { raceJob.cancel() }
}

/**
 * Runs the given [block] in parallel with a timeout, returning failure if the timeout completes
 * first.
 */
suspend fun <R> launchWithTimeout(timeout: Duration, block: suspend () -> R): Result<R> =
    race(
        { Result.success(block()) },
        {
            delay(timeout)
            Result.failure(TimeoutException("$block timed out after $timeout"))
        },
    )

/**
 * Awaits this [Deferred] until the given [timeout] has elapsed.
 * * If the timeout is reached before the deferred completes, the returned [Result] will contain
 *   [TimeoutException], and the deferred will be cancelled.
 * * If the deferred completes normally and in time, the returned Result will contain the value.
 * * If the deferred completes exceptionally, the returned Result will contain the exception.
 *
 * This is designed to solve situations that arise when the deferred work is not cooperative, for
 * example when it is loading bitmap data from another process and we want to have a limit on how
 * long we will wait, even if we cannot cancel that operation.
 */
suspend fun <R> Deferred<R>.awaitAndCancelOnTimeout(timeout: Duration): Result<R> {
    val deferred = this
    return race(
        {
            try {
                Result.success(deferred.await())
            } catch (ex: CancellationException) {
                deferred.cancel()
                throw ex
            } catch (ex: Exception) {
                Result.failure(ex)
            }
        },
        {
            delay(timeout)
            Result.failure(TimeoutException("$deferred timed out after $timeout"))
        },
    )
}