-
-
Notifications
You must be signed in to change notification settings - Fork 608
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: AsyncService for parallel EngineService processing
Co-authored-by: Jean-René Lavoie <>
- Loading branch information
1 parent
bb60463
commit 4c2b422
Showing
2 changed files
with
140 additions
and
0 deletions.
There are no files selected for viewing
41 changes: 41 additions & 0 deletions
41
fxgl-core/src/main/kotlin/com/almasb/fxgl/core/AsyncService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
/* | ||
* FXGL - JavaFX Game Library. The MIT License (MIT). | ||
* Copyright (c) AlmasB ([email protected]). | ||
* See LICENSE for details. | ||
*/ | ||
|
||
package com.almasb.fxgl.core | ||
|
||
import java.util.concurrent.CompletableFuture | ||
|
||
/** | ||
* | ||
* @author Jean-Rene Lavoie ([email protected]) | ||
*/ | ||
abstract class AsyncService<T> : EngineService() { | ||
|
||
private var asyncTask: CompletableFuture<T>? = null | ||
|
||
/** | ||
* Call the async game update. On next game update, wait for the task to be completed (if not already) and | ||
* call onPostGameUpdateAsync to allow JavaFX thread dependent task handling (e.g. updating the Nodes) | ||
*/ | ||
override fun onGameUpdate(tpf: Double) { | ||
asyncTask?.let { onPostGameUpdateAsync(it.get()) } | ||
asyncTask = CompletableFuture.supplyAsync(){ onGameUpdateAsync(tpf) } // Process until next onGameUpdate | ||
} | ||
|
||
/** | ||
* Async game update processing method. | ||
* Warning: This will not run on the main JavaFX thread. This means that any changes done on the Nodes will cause | ||
* an exception. | ||
*/ | ||
abstract fun onGameUpdateAsync(tpf: Double): T | ||
|
||
/** | ||
* Async processing Callback. This method is called on next onGameUpdate allowing synchronization between this | ||
* Service async processing and the main JavaFX thread. | ||
*/ | ||
open fun onPostGameUpdateAsync(result: T) { } | ||
|
||
} |
99 changes: 99 additions & 0 deletions
99
fxgl-core/src/test/kotlin/com/almasb/fxgl/core/AsyncServiceTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
/* | ||
* FXGL - JavaFX Game Library. The MIT License (MIT). | ||
* Copyright (c) AlmasB ([email protected]). | ||
* See LICENSE for details. | ||
*/ | ||
@file:Suppress("JAVA_MODULE_DOES_NOT_DEPEND_ON_MODULE") | ||
package com.almasb.fxgl.core | ||
|
||
import org.hamcrest.MatcherAssert.assertThat | ||
import org.hamcrest.Matchers | ||
import org.hamcrest.Matchers.`is` | ||
import org.hamcrest.Matchers.greaterThan | ||
import org.hamcrest.Matchers.lessThan | ||
import org.hamcrest.Matchers.both | ||
import org.junit.jupiter.api.Assertions.assertEquals | ||
import org.junit.jupiter.api.Test | ||
import kotlin.system.measureTimeMillis | ||
|
||
/** | ||
* | ||
* @author Jean-Rene Lavoie ([email protected]) | ||
*/ | ||
class AsyncServiceTest { | ||
|
||
@Test | ||
fun `Async Service with Unit (Kotlin Void)`() { | ||
val service = object : AsyncService<Unit>() { | ||
override fun onGameUpdateAsync(tpf: Double) { | ||
Thread.sleep(100) // Processing takes more time than a normal tick | ||
} | ||
} | ||
|
||
// On first call, it'll launch the async process and continue the game loop without affecting the tick | ||
// If it takes less than 5 millis, it's running async | ||
assertThat(measureTimeMillis { service.onGameUpdate(1.0) }.toDouble(), lessThan(7.0)) | ||
|
||
// On the second call, it must wait until the first call is resolved before calling it again (to prevent major desync) | ||
// We expect it to take more than 80 millis | ||
assertThat(measureTimeMillis { service.onGameUpdate(1.0) }.toDouble(), greaterThan(80.0)) | ||
} | ||
|
||
@Test | ||
fun `Async Service with T (String)`() { | ||
var postUpdateValue = "" | ||
val service = object : AsyncService<String>() { | ||
override fun onGameUpdateAsync(tpf: Double): String { | ||
return "Done" | ||
} | ||
|
||
override fun onPostGameUpdateAsync(result: String) { | ||
postUpdateValue = result | ||
} | ||
} | ||
|
||
// On first call, we don't have the postUpdateValue yet | ||
service.onGameUpdate(1.0) | ||
assertEquals(postUpdateValue, "") | ||
|
||
// On second update, we updated the postUpdateValue | ||
service.onGameUpdate(1.0) | ||
assertEquals(postUpdateValue, "Done") | ||
} | ||
|
||
@Test | ||
fun `Async Service parallel`() { | ||
val services = listOf( | ||
object : AsyncService<Unit>() { | ||
override fun onGameUpdateAsync(tpf: Double) { | ||
Thread.sleep(100) // Processing takes more time than a normal tick | ||
} | ||
}, | ||
object : AsyncService<Unit>() { | ||
override fun onGameUpdateAsync(tpf: Double) { | ||
Thread.sleep(100) // Processing takes more time than a normal tick | ||
} | ||
}, | ||
object : AsyncService<Unit>() { | ||
override fun onGameUpdateAsync(tpf: Double) { | ||
Thread.sleep(100) // Processing takes more time than a normal tick | ||
} | ||
} | ||
) | ||
|
||
// 3 services started in parallel without additional latency | ||
assertThat(measureTimeMillis { | ||
services.forEach { service -> | ||
service.onGameUpdate(1.0) | ||
} | ||
}.toDouble(), lessThan(7.0)) | ||
|
||
// 3 services resolved in approximately 1/3 of what it would take if they were sequentially resolved | ||
assertThat(measureTimeMillis { | ||
services.forEach { service -> | ||
service.onGameUpdate(1.0) | ||
} | ||
}.toDouble(), `is`(both(greaterThan(80.0)).and(lessThan(120.0)))) | ||
} | ||
|
||
} |