Skip to content

Commit abd8f88

Browse files
DRAIN_EXCLUSIVE_ACTIONS implementation
If actions are not overlapping in their state changes, then we can process them all before a new render pass.
1 parent 10814b1 commit abd8f88

File tree

12 files changed

+609
-36
lines changed

12 files changed

+609
-36
lines changed

.github/workflows/kotlin.yml

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,27 @@ jobs :
180180
build-root-directory : samples/tutorial
181181
restore-cache-key : main-build-artifacts
182182

183+
jvm-drainExclusive-runtime-test :
184+
name : Conflate Stale Renderings Runtime JVM Tests
185+
runs-on : ubuntu-latest
186+
timeout-minutes : 20
187+
steps :
188+
- name: Checkout
189+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
190+
191+
- name : Check with Gradle
192+
uses : ./.github/actions/gradle-task
193+
with :
194+
task : jvmTest --continue -Pworkflow.runtime=drainExclusive
195+
restore-cache-key : main-build-artifacts
196+
197+
# Report as GitHub Pull Request Check.
198+
- name : Publish Test Report
199+
uses : mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
200+
if : always() # always run even if the previous step fails
201+
with :
202+
report_paths : '**/build/test-results/test/TEST-*.xml'
203+
183204
jvm-conflate-runtime-test :
184205
name : Conflate Stale Renderings Runtime JVM Tests
185206
runs-on : ubuntu-latest
@@ -306,6 +327,111 @@ jobs :
306327
with:
307328
report_paths: '**/build/test-results/test/TEST-*.xml'
308329

330+
jvm-conflate-drainExclusive-runtime-test:
331+
name: Conflate Stale Renderings Runtime JVM Tests
332+
runs-on: ubuntu-latest
333+
timeout-minutes: 20
334+
steps:
335+
- name: Checkout
336+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
337+
338+
- name: Check with Gradle
339+
uses: ./.github/actions/gradle-task
340+
with:
341+
task: jvmTest --continue -Pworkflow.runtime=conflate-drainExclusive
342+
restore-cache-key: main-build-artifacts
343+
344+
# Report as GitHub Pull Request Check.
345+
- name: Publish Test Report
346+
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
347+
if: always() # always run even if the previous step fails
348+
with:
349+
report_paths: '**/build/test-results/test/TEST-*.xml'
350+
351+
jvm-stateChange-drainExclusive-runtime-test:
352+
name: Render On State Change Only Runtime JVM Tests
353+
runs-on: ubuntu-latest
354+
timeout-minutes: 20
355+
steps:
356+
- name: Checkout
357+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
358+
359+
- name: Check with Gradle
360+
uses: ./.github/actions/gradle-task
361+
with:
362+
task: jvmTest --continue -Pworkflow.runtime=stateChange-drainExclusive
363+
restore-cache-key: main-build-artifacts
364+
365+
# Report as GitHub Pull Request Check.
366+
- name: Publish Test Report
367+
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
368+
if: always() # always run even if the previous step fails
369+
with:
370+
report_paths: '**/build/test-results/test/TEST-*.xml'
371+
372+
jvm-partial-drainExclusive-runtime-test:
373+
name: Partial Tree Rendering Only Runtime JVM Tests
374+
runs-on: ubuntu-latest
375+
timeout-minutes: 20
376+
steps:
377+
- name: Checkout
378+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
379+
380+
- name: Check with Gradle
381+
uses: ./.github/actions/gradle-task
382+
with:
383+
task: jvmTest --continue -Pworkflow.runtime=partial-drainExclusive
384+
restore-cache-key: main-build-artifacts
385+
386+
# Report as GitHub Pull Request Check.
387+
- name: Publish Test Report
388+
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
389+
if: always() # always run even if the previous step fails
390+
with:
391+
report_paths: '**/build/test-results/test/TEST-*.xml'
392+
393+
jvm-conflate-stateChange-drainExclusive-runtime-test:
394+
name: Render On State Change Only and Conflate Stale Runtime JVM Tests
395+
runs-on: ubuntu-latest
396+
timeout-minutes: 20
397+
steps:
398+
- name: Checkout
399+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
400+
401+
- name: Check with Gradle
402+
uses: ./.github/actions/gradle-task
403+
with:
404+
task: jvmTest --continue -Pworkflow.runtime=conflate-stateChange-drainExclusive
405+
restore-cache-key: main-build-artifacts
406+
407+
# Report as GitHub Pull Request Check.
408+
- name: Publish Test Report
409+
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
410+
if: always() # always run even if the previous step fails
411+
with:
412+
report_paths: '**/build/test-results/test/TEST-*.xml'
413+
414+
jvm-conflate-partial-drainExclusive-runtime-test:
415+
name: Render On State Change Only and Conflate Stale Runtime JVM Tests
416+
runs-on: ubuntu-latest
417+
timeout-minutes: 20
418+
steps:
419+
- name: Checkout
420+
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
421+
422+
- name: Check with Gradle
423+
uses: ./.github/actions/gradle-task
424+
with:
425+
task: jvmTest --continue -Pworkflow.runtime=conflate-partial-drainExclusive
426+
restore-cache-key: main-build-artifacts
427+
428+
# Report as GitHub Pull Request Check.
429+
- name: Publish Test Report
430+
uses: mikepenz/action-junit-report@5f47764eec0e1c1f19f40c8e60a5ba47e47015c5 # v4
431+
if: always() # always run even if the previous step fails
432+
with:
433+
report_paths: '**/build/test-results/test/TEST-*.xml'
434+
309435
ios-tests :
310436
name : iOS Tests
311437
runs-on : macos-latest
@@ -412,7 +538,7 @@ jobs :
412538
### <start-connected-check-shards>
413539
shardNum: [ 1, 2, 3 ]
414540
### <end-connected-check-shards>
415-
runtime : [ conflate, stateChange, conflate-stateChange, partial, conflate-partial, stable ]
541+
runtime : [ conflate, stateChange, drainExclusive, conflate-stateChange, partial, conflate-partial, stable, conflate-drainExclusive, stateChange-drainExclusive, partial-drainExclusive, conflate-partial-drainExclusive ]
416542
steps :
417543
- name: Checkout
418544
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

workflow-config/config-android/src/main/java/com/squareup/workflow1/config/AndroidRuntimeConfigTools.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.squareup.workflow1.config
33
import com.squareup.workflow1.RuntimeConfig
44
import com.squareup.workflow1.RuntimeConfigOptions
55
import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
6+
import com.squareup.workflow1.RuntimeConfigOptions.DRAIN_EXCLUSIVE_ACTIONS
67
import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
78
import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
89
import com.squareup.workflow1.RuntimeConfigOptions.STABLE_EVENT_HANDLERS
@@ -33,6 +34,10 @@ public class AndroidRuntimeConfigTools {
3334
*
3435
* - `stable` Enables stable event handlers (changes the default value of the `remember`
3536
* parameter of `RenderContext.eventHandler` functions from `false` to `true`)
37+
*
38+
* - `drainExclusive` Enables draining exclusive actions. If we have more actions to process
39+
* that are queued on nodes not affected by the last action application, then we will
40+
* continue to process those actions before another render pass.
3641
*/
3742
@WorkflowExperimentalRuntime
3843
public fun getAppWorkflowRuntimeConfig(): RuntimeConfig {
@@ -48,6 +53,7 @@ public class AndroidRuntimeConfigTools {
4853
"stateChange" -> config.add(RENDER_ONLY_WHEN_STATE_CHANGES)
4954
"partial" -> config.addAll(setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING))
5055
"stable" -> config.add(STABLE_EVENT_HANDLERS)
56+
"drainExclusive" -> config.add(DRAIN_EXCLUSIVE_ACTIONS)
5157
else -> throw IllegalArgumentException("Unrecognized runtime config option \"$it\"")
5258
}
5359
}

workflow-config/config-jvm/src/main/java/com/squareup/workflow1/config/JvmTestRuntimeConfigTools.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.squareup.workflow1.config
33
import com.squareup.workflow1.RuntimeConfig
44
import com.squareup.workflow1.RuntimeConfigOptions
55
import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
6+
import com.squareup.workflow1.RuntimeConfigOptions.DRAIN_EXCLUSIVE_ACTIONS
67
import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING
78
import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
89
import com.squareup.workflow1.RuntimeConfigOptions.STABLE_EVENT_HANDLERS
@@ -35,6 +36,10 @@ public class JvmTestRuntimeConfigTools {
3536
*
3637
* - `stable` Enables stable event handlers (changes the default value of the `remember`
3738
* parameter of `RenderContext.eventHandler` functions from `false` to `true`)
39+
*
40+
* - `drainExclusive` Enables draining exclusive actions. If we have more actions to process
41+
* that are queued on nodes not affected by the last action application, then we will
42+
* continue to process those actions before another render pass.
3843
*/
3944
@OptIn(WorkflowExperimentalRuntime::class)
4045
public fun getTestRuntimeConfig(): RuntimeConfig {
@@ -50,6 +55,7 @@ public class JvmTestRuntimeConfigTools {
5055
"stateChange" -> config.add(RENDER_ONLY_WHEN_STATE_CHANGES)
5156
"partial" -> config.addAll(setOf(RENDER_ONLY_WHEN_STATE_CHANGES, PARTIAL_TREE_RENDERING))
5257
"stable" -> config.add(STABLE_EVENT_HANDLERS)
58+
"drainExclusive" -> config.add(DRAIN_EXCLUSIVE_ACTIONS)
5359
else -> throw IllegalArgumentException("Unrecognized runtime config option \"$it\"")
5460
}
5561
}

workflow-core/api/workflow-core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ public final class com/squareup/workflow1/PropsUpdated : com/squareup/workflow1/
164164
public final class com/squareup/workflow1/RuntimeConfigOptions : java/lang/Enum {
165165
public static final field CONFLATE_STALE_RENDERINGS Lcom/squareup/workflow1/RuntimeConfigOptions;
166166
public static final field Companion Lcom/squareup/workflow1/RuntimeConfigOptions$Companion;
167+
public static final field DRAIN_EXCLUSIVE_ACTIONS Lcom/squareup/workflow1/RuntimeConfigOptions;
167168
public static final field PARTIAL_TREE_RENDERING Lcom/squareup/workflow1/RuntimeConfigOptions;
168169
public static final field RENDER_ONLY_WHEN_STATE_CHANGES Lcom/squareup/workflow1/RuntimeConfigOptions;
169170
public static final field STABLE_EVENT_HANDLERS Lcom/squareup/workflow1/RuntimeConfigOptions;

workflow-core/src/commonMain/kotlin/com/squareup/workflow1/RuntimeConfig.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ public enum class RuntimeConfigOptions {
6666
*/
6767
@WorkflowExperimentalRuntime
6868
STABLE_EVENT_HANDLERS,
69+
70+
/**
71+
* If we have more actions to process that are queued on nodes not affected by the last
72+
* action application, then we will continue to process those actions before another render
73+
* pass.
74+
*/
75+
@WorkflowExperimentalRuntime
76+
DRAIN_EXCLUSIVE_ACTIONS,
6977
;
7078

7179
public companion object {

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.squareup.workflow1
22

33
import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
4+
import com.squareup.workflow1.RuntimeConfigOptions.DRAIN_EXCLUSIVE_ACTIONS
45
import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
56
import com.squareup.workflow1.internal.WorkflowRunner
67
import com.squareup.workflow1.internal.chained
@@ -189,12 +190,49 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
189190
// we don't surprise anyone with an unexpected rendering pass. Show's over, go home.
190191
if (!isActive) return@launch
191192

193+
var drainingActionResult = actionResult
194+
if (runtimeConfig.contains(DRAIN_EXCLUSIVE_ACTIONS)) {
195+
var drainingHasChangedState = false
196+
drain@ while (isActive &&
197+
drainingActionResult is ActionApplied<*> &&
198+
drainingActionResult.output == null
199+
) {
200+
drainingHasChangedState = drainingHasChangedState || drainingActionResult.stateChanged
201+
// We start by yielding, because if we are on an Unconfined dispatcher, we want to give
202+
// other signals (like Workers listening to the same result) a chance to get dispatched
203+
// and queue their actions.
204+
yield()
205+
// We may have more mutually exclusive actions we can apply before a render pass.
206+
drainingActionResult =
207+
runner.processAction(waitForAnAction = false, skipChangedNodes = true)
208+
209+
// If no mutually exclusive actions processed, then go ahead and do the render pass.
210+
if (drainingActionResult == ActionsExhausted) break@drain
211+
212+
// Now save last result if its not ActionsExhausted
213+
actionResult = drainingActionResult
214+
215+
// If no state changed, send any output and start outer loop again.
216+
if (shouldShortCircuitForUnchangedState(
217+
actionResult = drainingActionResult,
218+
conflationHasChangedState = drainingHasChangedState
219+
)
220+
) {
221+
sendOutput(drainingActionResult, onOutput)
222+
continue@outer
223+
}
224+
}
225+
}
226+
192227
// Next Render Pass.
193228
var nextRenderAndSnapshot: RenderingAndSnapshot<RenderingT> = runner.nextRendering()
194229

195230
if (runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) {
196231
var conflationHasChangedState = false
197-
conflate@ while (isActive && actionResult is ActionApplied<*> && actionResult.output == null) {
232+
conflate@ while (isActive &&
233+
actionResult is ActionApplied<*> &&
234+
actionResult.output == null
235+
) {
198236
conflationHasChangedState = conflationHasChangedState || actionResult.stateChanged
199237
// We start by yielding, because if we are on an Unconfined dispatcher, we want to give
200238
// other signals (like Workers listening to the same result) a chance to get dispatched

workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/SubtreeManager.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,11 +150,14 @@ internal class SubtreeManager<PropsT, StateT, OutputT>(
150150
*
151151
* @return [Boolean] whether or not the children action queues are empty.
152152
*/
153-
fun onNextChildAction(selector: SelectBuilder<ActionProcessingResult>): Boolean {
153+
fun onNextChildAction(
154+
selector: SelectBuilder<ActionProcessingResult>,
155+
skipChangedNodes: Boolean = false
156+
): Boolean {
154157
var empty = true
155158
children.forEachActive { child ->
156159
// Do this separately so the compiler doesn't avoid it if empty is already false.
157-
val childEmpty = child.workflowNode.onNextAction(selector)
160+
val childEmpty = child.workflowNode.onNextAction(selector, skipChangedNodes)
158161
empty = childEmpty && empty
159162
}
160163
return empty

0 commit comments

Comments
 (0)