Skip to content

Commit

Permalink
Async API improvements. #182
Browse files Browse the repository at this point in the history
  • Loading branch information
czyzby committed Mar 29, 2020
1 parent 2e77ffe commit 710c4be
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 18 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
- **[CHANGE]** (`ktx-app`) `LetterboxingViewport` moved from `ktx-app` to `ktx-graphics`.
- **[FEATURE]** (`ktx-ashley`) Added `Entity.contains` (`in` operator) that checks if an `Entity` has a `Component`.
- **[FEATURE]** (`ktx-async`) Added `RenderingScope` factory function for custom scopes using rendering thread dispatcher.
- **[FEATURE]** (`ktx-async`) `newAsyncContext` and `newSingleThreadAsyncContext` now support `threadName` parameter
that allows to set thread name pattern of `AsyncExecutor` threads.
- **[FIX]** (`ktx-async`) `isOnRenderingThread` now behaves consistently regardless of launching coroutine context.
- **[FEATURE]** (`ktx-graphics`) Added `LetterboxingViewport` from `ktx-app`.
- **[FEATURE]** (`ktx-graphics`) Added `takeScreenshot` utility function that allows to save a screenshot of the application.
- **[FEATURE]** (`ktx-graphics`) Added `BitmapFont.center` extension method that allows to center text on an object.
Expand Down
6 changes: 6 additions & 0 deletions async/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -245,10 +245,16 @@ import ktx.async.AsyncExecutorDispatcher
import ktx.async.newAsyncContext
import ktx.async.newSingleThreadAsyncContext

// Context with a single thread:
val singleThreaded = newSingleThreadAsyncContext()

// Context with multiple threads:
val multiThreaded = newAsyncContext(threads = 4)

// Context with a custom thread name pattern:
val multiThreadedWithNamedThreads = newAsyncContext(threads = 4, threadName = "MyThread")

// Context with an existing executor:
val executor = AsyncExecutor(2)
val fromExistingExecutor = AsyncExecutorDispatcher(executor, threads = 2)
```
Expand Down
20 changes: 14 additions & 6 deletions async/src/main/kotlin/ktx/async/async.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ object KtxAsync : CoroutineScope {
override val coroutineContext = MainDispatcher

/**
* Should be invoked on the main rendering thread before using KTX coroutines. Might slightly affect performance
* otherwise.
* Should be invoked on the main rendering thread before using KTX coroutines.
* Failing to do so will cause some parts of the API to throw exceptions.
*/
fun initiate() {
ImmediateDispatcher.initiate()
MainDispatcher.initiate()
}
}

Expand All @@ -48,14 +48,19 @@ fun RenderingScope() = CoroutineScope(SupervisorJob() + MainDispatcher)
/**
* Creates a new [AsyncExecutorDispatcher] wrapping around an [AsyncExecutor] with a single thread to execute tasks
* asynchronously outside of the main rendering thread.
*
* [AsyncExecutor] thread will be named according to the [threadName] pattern.
*/
fun newSingleThreadAsyncContext() = newAsyncContext(1)
fun newSingleThreadAsyncContext(threadName: String = "AsyncExecutor-Thread") = newAsyncContext(1, threadName)

/**
* Creates a new [AsyncExecutorDispatcher] wrapping around an [AsyncExecutor] with the chosen amount of [threads]
* to execute tasks asynchronously outside of the main rendering thread.
*
* [AsyncExecutor] threads will be named according to the [threadName] pattern.
*/
fun newAsyncContext(threads: Int) = AsyncExecutorDispatcher(AsyncExecutor(threads), threads)
fun newAsyncContext(threads: Int, threadName: String = "AsyncExecutor-Thread") =
AsyncExecutorDispatcher(AsyncExecutor(threads, threadName), threads)

/**
* Suspends the coroutine to execute the defined [block] on the main rendering thread and return its result.
Expand All @@ -65,7 +70,10 @@ suspend fun <T> onRenderingThread(block: suspend CoroutineScope.() -> T) = withC
/**
* Returns true if the coroutine was launched from a rendering thread dispatcher.
*/
fun CoroutineScope.isOnRenderingThread() = coroutineContext[ContinuationInterceptor.Key] is RenderingThreadDispatcher
fun CoroutineScope.isOnRenderingThread() =
coroutineContext[ContinuationInterceptor.Key] is RenderingThreadDispatcher
&& Thread.currentThread() === MainDispatcher.mainThread


/**
* Attempts to skip the current frame. Resumes the execution using a task scheduled with [Application.postRunnable].
Expand Down
14 changes: 2 additions & 12 deletions async/src/main/kotlin/ktx/async/dispatchers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -114,23 +114,13 @@ class RenderingThreadDispatcherFactory : MainDispatcherFactory {
*/
object MainDispatcher : RenderingThreadDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher = ImmediateDispatcher
}

/**
* Supports immediate tasks execution in the main rendering thread context.
*/
object ImmediateDispatcher : RenderingThreadDispatcher() {
@ExperimentalCoroutinesApi
override val immediate: MainCoroutineDispatcher = this
override val immediate = this
lateinit var mainThread: Thread

/** Must be called **on the rendering thread** before using KTX coroutines. */
fun initiate() {
mainThread = Thread.currentThread()
}

override fun isDispatchNeeded(context: CoroutineContext): Boolean = Thread.currentThread() != mainThread

override fun toString(): String = super.toString() + "(immediate)"
override fun isDispatchNeeded(context: CoroutineContext): Boolean = !KtxAsync.isOnRenderingThread()
}
95 changes: 95 additions & 0 deletions async/src/test/kotlin/ktx/async/asyncTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ class KtxAsyncTest : AsyncTest() {
assertEquals(1, dispatcher.threads)
}

@Test
fun `should create a single-threaded AsyncExecutorDispatcher with selected name`() {
// When:
val dispatcher: AsyncExecutorDispatcher = newSingleThreadAsyncContext(threadName = "MyThreadName")

// Then:
assertEquals(1, dispatcher.threads)
assertTrue("MyThreadName" in getExecutionThread(dispatcher.executor).name)
}

@Test
fun `should create a multi-threaded AsyncExecutorDispatcher`() {
// When:
Expand All @@ -53,6 +63,16 @@ class KtxAsyncTest : AsyncTest() {
assertEquals(4, dispatcher.threads)
}

@Test
fun `should create a multi-threaded AsyncExecutorDispatcher with selected name`() {
// When:
val dispatcher: AsyncExecutorDispatcher = newAsyncContext(threads = 4, threadName = "MyThreadName")

// Then:
assertEquals(4, dispatcher.threads)
assertTrue("MyThreadName" in getExecutionThread(dispatcher.executor).name)
}

@Test
fun `should execute on the main rendering thread`() {
// Given:
Expand Down Expand Up @@ -134,6 +154,45 @@ class KtxAsyncTest : AsyncTest() {
assertTrue(isOnRenderingThread.get())
}

@Test
fun `should detect rendering thread from coroutine with different context`() {
// Given:
val executionThread = CompletableFuture<Thread>()
val isOnRenderingThread = AtomicBoolean(true)
val dispatcher: AsyncExecutorDispatcher = newSingleThreadAsyncContext()

// When:
KtxAsync.launch(dispatcher) {
onRenderingThread {
isOnRenderingThread.set(isOnRenderingThread())
executionThread.complete(Thread.currentThread())
}
}

// Then:
assertSame(getMainRenderingThread(), executionThread.join())
assertTrue(isOnRenderingThread.get())
}

@Test
fun `should detect rendering thread from coroutine launched from global context`() {
// Given:
val executionThread = CompletableFuture<Thread>()
val isOnRenderingThread = AtomicBoolean(false)

// When:
GlobalScope.launch {
onRenderingThread {
isOnRenderingThread.set(isOnRenderingThread())
executionThread.complete(Thread.currentThread())
}
}

// Then:
assertSame(getMainRenderingThread(), executionThread.join())
assertTrue(isOnRenderingThread.get())
}

@Test
fun `should detect non-rendering threads`() {
// Given:
Expand All @@ -152,6 +211,23 @@ class KtxAsyncTest : AsyncTest() {
assertFalse(isOnRenderingThread.get())
}

@Test
fun `should detect non-rendering thread launched from global scope`() {
// Given:
val executionThread = CompletableFuture<Thread>()
val isOnRenderingThread = AtomicBoolean(true)

// When:
GlobalScope.launch {
isOnRenderingThread.set(isOnRenderingThread())
executionThread.complete(Thread.currentThread())
}

// Then:
assertNotSame(getMainRenderingThread(), executionThread.join())
assertFalse(isOnRenderingThread.get())
}

@Test
fun `should detect nested non-rendering threads`() {
// Given:
Expand All @@ -171,6 +247,25 @@ class KtxAsyncTest : AsyncTest() {
assertNotSame(getMainRenderingThread(), executionThread.join())
assertFalse(isOnRenderingThread.get())
}
@Test
fun `should detect nested non-rendering threads with separate context`() {
// Given:
val executionThread = CompletableFuture<Thread>()
val isOnRenderingThread = AtomicBoolean(true)
val dispatcher: AsyncExecutorDispatcher = newSingleThreadAsyncContext()

// When:
KtxAsync.launch(Dispatchers.KTX) {
withContext(dispatcher) {
isOnRenderingThread.set(isOnRenderingThread())
executionThread.complete(Thread.currentThread())
}
}

// Then:
assertNotSame(getMainRenderingThread(), executionThread.join())
assertFalse(isOnRenderingThread.get())
}

@Test
fun `should detect non-rendering threads with context switch`() {
Expand Down

0 comments on commit 710c4be

Please sign in to comment.