Skip to content

Commit da58522

Browse files
CSR pass old rendering, do not render again if can skip
1 parent c0e9340 commit da58522

File tree

4 files changed

+119
-8
lines changed

4 files changed

+119
-8
lines changed

workflow-runtime/api/workflow-runtime.api

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,10 @@ public final class com/squareup/workflow1/WorkflowInterceptor$RenderContextInter
8888
}
8989

9090
public final class com/squareup/workflow1/WorkflowInterceptor$RenderPassSkipped : com/squareup/workflow1/WorkflowInterceptor$RuntimeLoopOutcome {
91-
public static final field INSTANCE Lcom/squareup/workflow1/WorkflowInterceptor$RenderPassSkipped;
91+
public fun <init> ()V
92+
public fun <init> (Z)V
93+
public synthetic fun <init> (ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
94+
public final fun getEndOfTick ()Z
9295
}
9396

9497
public final class com/squareup/workflow1/WorkflowInterceptor$RenderPassesComplete : com/squareup/workflow1/WorkflowInterceptor$RuntimeLoopOutcome {

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,10 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
170170
*/
171171
fun shouldShortCircuitForUnchangedState(
172172
actionResult: ActionProcessingResult,
173-
conflationHasChangedState: Boolean = false
174173
): Boolean {
175174
return runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES) &&
176175
actionResult is ActionApplied<*> &&
177-
!actionResult.stateChanged &&
178-
!conflationHasChangedState
176+
!actionResult.stateChanged
179177
}
180178

181179
scope.launch {
@@ -186,7 +184,7 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
186184
var actionResult: ActionProcessingResult = runner.processAction()
187185

188186
if (shouldShortCircuitForUnchangedState(actionResult)) {
189-
chainedInterceptor.onRuntimeLoopTick(RenderPassSkipped)
187+
chainedInterceptor.onRuntimeLoopTick(RenderPassSkipped())
190188
sendOutput(actionResult, onOutput)
191189
continue@outer
192190
}
@@ -211,10 +209,15 @@ public fun <PropsT, OutputT, RenderingT> renderWorkflowIn(
211209
// Skip rendering if we had unchanged state, keep draining actions.
212210
if (shouldShortCircuitForUnchangedState(
213211
actionResult = actionResult,
214-
conflationHasChangedState = conflationHasChangedState
215212
)
216213
) {
217-
chainedInterceptor.onRuntimeLoopTick(RenderPassSkipped)
214+
if (conflationHasChangedState) {
215+
chainedInterceptor.onRuntimeLoopTick(RenderPassSkipped(endOfTick = false))
216+
// An earlier render changed state, so we need to pass that to the UI then we
217+
// can skip this render.
218+
break@conflate
219+
}
220+
chainedInterceptor.onRuntimeLoopTick(RenderPassSkipped())
218221
sendOutput(actionResult, onOutput)
219222
continue@outer
220223
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,23 @@ public interface WorkflowInterceptor {
157157
): Unit = Unit
158158

159159
public sealed interface RuntimeLoopOutcome
160-
public object RenderPassSkipped : RuntimeLoopOutcome
160+
161+
/**
162+
* @param endOfTick This is true if this skip was the end of the loop iteration (i.e. no update
163+
* was passed to the UI. It is false otherwise. An example of when it can be false is if
164+
* we have an action that causes a state change, but then we can process more while the
165+
* `CONFLATE_STALE_RENDERINGS` optimization is enabled. We may skip rendering based on
166+
* the subsequent action not changing state, but we will need to finish the loop and update
167+
* the UI from the previous action that changed state.
168+
*/
169+
public class RenderPassSkipped(
170+
public val endOfTick: Boolean = true
171+
) : RuntimeLoopOutcome
172+
173+
/**
174+
* @param renderingAndSnapshot This is the rendering and snapshot that was passed out of the
175+
* Workflow runtime.
176+
*/
161177
public class RenderPassesComplete<R>(
162178
public val renderingAndSnapshot: RenderingAndSnapshot<R>
163179
) : RuntimeLoopOutcome

workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1887,6 +1887,95 @@ class RenderWorkflowInTest {
18871887
}
18881888
}
18891889

1890+
@Test
1891+
fun for_conflate_and_render_only_when_state_changed_we_do_not_render_again_if_only_previous_rendering_changed() {
1892+
runtimeTestRunner.runParametrizedTest(
1893+
paramSource = runtimeOptions
1894+
.filter {
1895+
it.first.contains(CONFLATE_STALE_RENDERINGS) &&
1896+
it.first.contains(RENDER_ONLY_WHEN_STATE_CHANGES)
1897+
},
1898+
before = ::setup,
1899+
) { (runtimeConfig: RuntimeConfig, workflowTracer: WorkflowTracer?, dispatcher: TestDispatcher) ->
1900+
runTest(dispatcher) {
1901+
check(runtimeConfig.contains(CONFLATE_STALE_RENDERINGS))
1902+
check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES))
1903+
1904+
var childHandlerActionExecuted = false
1905+
val trigger = MutableSharedFlow<String>()
1906+
val emitted = mutableListOf<String>()
1907+
var renderCount = 0
1908+
1909+
val childWorkflow = Workflow.stateful<String, String, String>(
1910+
initialState = "unchanging state",
1911+
render = { renderState ->
1912+
runningWorker(
1913+
trigger.asWorker()
1914+
) {
1915+
action("") {
1916+
state = it
1917+
setOutput(it)
1918+
}
1919+
}
1920+
renderState
1921+
}
1922+
)
1923+
val workflow = Workflow.stateful<String, String, String>(
1924+
initialState = "unchanging state",
1925+
render = { renderState ->
1926+
renderChild(childWorkflow) { childOutput ->
1927+
action("childHandler") {
1928+
childHandlerActionExecuted = true
1929+
state = "$childOutput+update"
1930+
}
1931+
}
1932+
runningWorker(
1933+
trigger.asWorker()
1934+
) {
1935+
action("") {
1936+
// no state change now!
1937+
}
1938+
}
1939+
renderState.also {
1940+
renderCount++
1941+
}
1942+
}
1943+
)
1944+
val props = MutableStateFlow(Unit)
1945+
val renderings = renderWorkflowIn(
1946+
workflow = workflow,
1947+
scope = backgroundScope,
1948+
props = props,
1949+
runtimeConfig = runtimeConfig,
1950+
workflowTracer = workflowTracer,
1951+
) { }
1952+
1953+
val collectionJob = launch {
1954+
// Collect this unconfined so we can get all the renderings faster than actions can
1955+
// be processed.
1956+
renderings.collect {
1957+
emitted += it.rendering
1958+
}
1959+
}
1960+
advanceIfStandard(dispatcher)
1961+
1962+
launch {
1963+
trigger.emit("changed state")
1964+
}
1965+
advanceIfStandard(dispatcher)
1966+
1967+
collectionJob.cancel()
1968+
1969+
// 2 renderings.
1970+
assertEquals(2, emitted.size)
1971+
assertEquals("changed state+update", emitted.last())
1972+
// Only 2 times rendered, the initial + the update (not 3).
1973+
assertEquals(2, renderCount)
1974+
assertTrue(childHandlerActionExecuted)
1975+
}
1976+
}
1977+
}
1978+
18901979
private class ExpectedException : RuntimeException()
18911980

18921981
private fun <T1, T2> cartesianProduct(

0 commit comments

Comments
 (0)