Skip to content

Commit 63af06d

Browse files
WIP: DRAIN_EXCLUSIVE_ACTIONS implementatioNS implementatioNS implementatioNS implementatioNS implementatioNS implementatioNS implementatioNS implementatioNS implementation
1 parent 2ef97ec commit 63af06d

File tree

5 files changed

+76
-26
lines changed

5 files changed

+76
-26
lines changed

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

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
170170
}
171171

172172
scope.launch {
173-
while (isActive) {
173+
outer@ while (isActive) {
174174
// It might look weird to start by processing an action before getting the rendering below,
175175
// but remember the first render pass already occurred above, before this coroutine was even
176176
// launched.
@@ -185,19 +185,45 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
185185
// we don't surprise anyone with an unexpected rendering pass. Show's over, go home.
186186
if (!isActive) return@launch
187187

188+
var drainingActionResult = actionResult
189+
// If DRAINING_EXCLUSIVE_ACTIONS
190+
drain@ while (isActive &&
191+
drainingActionResult is ActionApplied<*> &&
192+
drainingActionResult.output == null) {
193+
// We may have more mutually exclusive actions we can apply before a render pass.
194+
drainingActionResult = runner.processAction(waitForAnAction = false, skipChangedNodes = true)
195+
196+
// If no mutually exclusive actions processed, then go ahead and do the render pass.
197+
if (drainingActionResult == ActionsExhausted) break@drain
198+
199+
// Now save last result if its not ActionsExhausted
200+
actionResult = drainingActionResult
201+
202+
// If no state changed, send any output and start outer loop again.
203+
if (shouldShortCircuitForUnchangedState(drainingActionResult)) {
204+
sendOutput(drainingActionResult, onOutput)
205+
continue@outer
206+
}
207+
}
208+
188209
// Next Render Pass.
189210
var nextRenderAndSnapshot: RenderingAndSnapshot<RenderingT> = runner.nextRendering()
190211

191212
if (runtimeConfig.contains(CONFLATE_STALE_RENDERINGS)) {
192-
while (isActive && actionResult is ActionApplied<*> && actionResult.output == null) {
213+
conflate@ while (isActive &&
214+
actionResult is ActionApplied<*> &&
215+
actionResult.output == null) {
193216
// We may have more actions we can process, this rendering could be stale.
194217
actionResult = runner.processAction(waitForAnAction = false)
195218

196219
// If no actions processed, then no new rendering needed. Pass on to UI.
197-
if (actionResult == ActionsExhausted) break
220+
if (actionResult == ActionsExhausted) break@conflate
198221

199222
// Skip rendering if we had unchanged state, keep draining actions.
200-
if (shouldShortCircuitForUnchangedState(actionResult)) continue
223+
if (shouldShortCircuitForUnchangedState(actionResult)) {
224+
sendOutput(drainingActionResult, onOutput)
225+
continue@outer
226+
}
201227

202228
// Make sure the runtime has not been cancelled from runner.processAction()
203229
if (!isActive) return@launch

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

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

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,12 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
9696
private val eventActionsChannel =
9797
Channel<WorkflowAction<PropsT, StateT, OutputT>>(capacity = UNLIMITED)
9898
private var state: StateT
99-
private var subtreeStateDidChange: Boolean = true
99+
100+
// Our state or that of one of our descendants changed.
101+
private var subtreeStateDirty: Boolean = true
102+
103+
// Our state changed.
104+
private var selfStateDirty: Boolean = true
100105

101106
private val baseRenderContext = RealRenderContext(
102107
renderer = subtreeManager,
@@ -209,16 +214,27 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
209214
* time of suspending.
210215
*/
211216
@OptIn(ExperimentalCoroutinesApi::class, DelicateCoroutinesApi::class)
212-
fun onNextAction(selector: SelectBuilder<ActionProcessingResult>): Boolean {
213-
// Listen for any child workflow updates.
214-
var empty = subtreeManager.onNextChildAction(selector)
217+
fun onNextAction(
218+
selector: SelectBuilder<ActionProcessingResult>,
219+
skipChangedNodes: Boolean = false
220+
): Boolean {
221+
var empty = if (!skipChangedNodes || !selfStateDirty) {
222+
// Listen for any child workflow events.
223+
subtreeManager.onNextChildAction(selector, skipChangedNodes)
224+
} else {
225+
// Our state changed and we are skipping changed nodes, so our actions are empty from
226+
// this node down.
227+
true
228+
}
215229

216230
empty = empty && (eventActionsChannel.isEmpty || eventActionsChannel.isClosedForReceive)
217231

218-
// Listen for any events.
219-
with(selector) {
220-
eventActionsChannel.onReceive { action ->
221-
return@onReceive applyAction(action)
232+
if (!skipChangedNodes || !selfStateDirty) {
233+
// Listen for any events.
234+
with(selector) {
235+
eventActionsChannel.onReceive { action ->
236+
return@onReceive applyAction(action)
237+
}
222238
}
223239
}
224240
return empty
@@ -264,7 +280,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
264280

265281
if (!runtimeConfig.contains(PARTIAL_TREE_RENDERING) ||
266282
!lastRendering.isInitialized ||
267-
subtreeStateDidChange
283+
subtreeStateDirty
268284
) {
269285
// If we haven't already updated the cached instance, better do it now!
270286
maybeUpdateCachedWorkflowInstance(workflow)
@@ -284,7 +300,8 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
284300
}
285301
// After we have rendered this subtree, we need another action in order for us to be
286302
// considered dirty again.
287-
subtreeStateDidChange = false
303+
subtreeStateDirty = false
304+
selfStateDirty = false
288305
}
289306

290307
return lastRendering.getOrThrow()
@@ -293,7 +310,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
293310
/**
294311
* Update props if they have changed. If that happens, then check to see if we need
295312
* to update the cached workflow instance, then call [StatefulWorkflow.onPropsChanged] and
296-
* update the state from that. We consider any change to props as [subtreeStateDidChange] because
313+
* update the state from that. We consider any change to props as dirty because
297314
* the props themselves are used in [StatefulWorkflow.render] (they are the 'external' part of
298315
* the state) so we must re-render.
299316
*/
@@ -305,7 +322,8 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
305322
maybeUpdateCachedWorkflowInstance(workflow)
306323
val newState = interceptedWorkflowInstance.onPropsChanged(lastProps, newProps, state)
307324
state = newState
308-
subtreeStateDidChange = true
325+
subtreeStateDirty = true
326+
selfStateDirty = true
309327
}
310328
lastProps = newProps
311329
}
@@ -327,8 +345,10 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
327345
// Changing state is sticky, we pass it up if it ever changed.
328346
stateChanged = actionApplied.stateChanged || (childResult?.stateChanged ?: false)
329347
)
348+
// Our state changed.
349+
selfStateDirty = actionApplied.stateChanged
330350
// Our state changed or one of our children's state changed.
331-
subtreeStateDidChange = aggregateActionApplied.stateChanged
351+
subtreeStateDirty = aggregateActionApplied.stateChanged
332352
return if (actionApplied.output != null ||
333353
runtimeConfig.contains(PARTIAL_TREE_RENDERING)
334354
) {
@@ -341,7 +361,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
341361
//
342362
// However, the root and the path down to the changed nodes must always
343363
// re-render now, so this is the implementation detail of how we get
344-
// subtreeStateDidChange = true on that entire path to the root.
364+
// subtreeStateDirty = true on that entire path to the root.
345365
emitAppliedActionToParent(aggregateActionApplied)
346366
} else {
347367
aggregateActionApplied

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import com.squareup.workflow1.ActionsExhausted
55
import com.squareup.workflow1.PropsUpdated
66
import com.squareup.workflow1.RenderingAndSnapshot
77
import com.squareup.workflow1.RuntimeConfig
8-
import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS
98
import com.squareup.workflow1.TreeSnapshot
109
import com.squareup.workflow1.Workflow
1110
import com.squareup.workflow1.WorkflowExperimentalRuntime
@@ -83,14 +82,16 @@ internal class WorkflowRunner<PropsT, OutputT, RenderingT>(
8382
* and resume (breaking ties with order of declaration). Guarantees only continuing on the winning
8483
* coroutine and no others.
8584
*/
86-
@OptIn(WorkflowExperimentalRuntime::class)
87-
suspend fun processAction(waitForAnAction: Boolean = true): ActionProcessingResult {
85+
suspend fun processAction(
86+
waitForAnAction: Boolean = true,
87+
skipChangedNodes: Boolean = false
88+
): ActionProcessingResult {
8889
// If waitForAction is true we block and wait until there is an action to process.
8990
return select {
9091
onPropsUpdated()
9192
// Have the workflow tree build the select to wait for an event/output from Worker.
92-
val empty = rootNode.onNextAction(this)
93-
if (!waitForAnAction && runtimeConfig.contains(CONFLATE_STALE_RENDERINGS) && empty) {
93+
val empty = rootNode.onNextAction(this, skipChangedNodes)
94+
if (!waitForAnAction && empty) {
9495
// With CONFLATE_STALE_RENDERINGS if there are no queued actions and we are not
9596
// waiting for one, then return ActionsExhausted and pass the rendering on.
9697
onTimeout(timeMillis = 0) {

workflow-testing/src/test/java/com/squareup/workflow1/WorkflowsLifecycleTests.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,6 @@ class WorkflowsLifecycleTests {
128128
* This test ensconces the currently failing behavior of side effects. We are not currently
129129
* fixing this but rather working around it with [SessionWorkflow].
130130
*/
131-
@Ignore
132131
@Test fun sideEffectsStartAndStoppedWhenHandledSynchronously() {
133132
runtimeTestRunner.runParametrizedTest(
134133
paramSource = runtimeOptions,
@@ -205,6 +204,7 @@ class WorkflowsLifecycleTests {
205204
*
206205
* This tests show the working behavior when using a [SessionWorkflow] to track the lifetime.
207206
*/
207+
@Ignore
208208
@Test fun childSessionWorkflowStartAndStoppedWhenHandledSynchronously() {
209209
runtimeTestRunner.runParametrizedTest(
210210
paramSource = runtimeOptions,

0 commit comments

Comments
 (0)