Skip to content

Commit e9f0456

Browse files
1247: Partial Tree Rerendering
Track whether or not the state (or state of a child) has changed in the WorkflowNode. Pass lastRendering if its not.
1 parent c7d7c15 commit e9f0456

File tree

3 files changed

+62
-33
lines changed

3 files changed

+62
-33
lines changed

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

Lines changed: 45 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import com.squareup.workflow1.NoopWorkflowInterceptor
66
import com.squareup.workflow1.RenderContext
77
import com.squareup.workflow1.RuntimeConfig
88
import com.squareup.workflow1.RuntimeConfigOptions
9+
import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES
910
import com.squareup.workflow1.StatefulWorkflow
1011
import com.squareup.workflow1.TreeSnapshot
1112
import com.squareup.workflow1.Workflow
1213
import com.squareup.workflow1.WorkflowAction
1314
import com.squareup.workflow1.WorkflowExperimentalApi
15+
import com.squareup.workflow1.WorkflowExperimentalRuntime
1416
import com.squareup.workflow1.WorkflowIdentifier
1517
import com.squareup.workflow1.WorkflowInterceptor
1618
import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession
@@ -44,7 +46,7 @@ import kotlin.coroutines.CoroutineContext
4446
* hard-coded values added to worker contexts. It must not contain a [Job] element (it would violate
4547
* structured concurrency).
4648
*/
47-
@OptIn(WorkflowExperimentalApi::class)
49+
@OptIn(WorkflowExperimentalApi::class, WorkflowExperimentalRuntime::class)
4850
internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
4951
val id: WorkflowNodeId,
5052
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
@@ -86,9 +88,11 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
8688
)
8789
private val sideEffects = ActiveStagingList<SideEffectNode>()
8890
private var lastProps: PropsT = initialProps
91+
private var lastRendering: RenderingT? = null
8992
private val eventActionsChannel =
9093
Channel<WorkflowAction<PropsT, StateT, OutputT>>(capacity = UNLIMITED)
9194
private var state: StateT
95+
private var subtreeStateDidChange: Boolean = true
9296

9397
private val baseRenderContext = RealRenderContext(
9498
renderer = subtreeManager,
@@ -205,16 +209,21 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
205209
coroutineContext.cancel(cause)
206210
}
207211

208-
// Call this after we have been passed any workflow instance, in [render] or [snapshot]. It may
209-
// have changed and we should check to see if we need to update our cached instances.
212+
/** Call this after we have been passed any workflow instance, in [render] or [snapshot]. It may
213+
* have changed and we should check to see if we need to update our cached instances.
214+
*
215+
* @return true if the instance has changed, otherwise false.
216+
*/
210217
private fun updateCachedWorkflowInstance(
211218
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>
212-
) {
219+
): Boolean {
213220
if (workflow !== cachedWorkflowInstance) {
214221
// instance has changed.
215222
interceptedWorkflowInstance = interceptor.intercept(workflow, this)
216223
cachedWorkflowInstance = workflow
224+
return true
217225
}
226+
return false
218227
}
219228

220229
/**
@@ -225,33 +234,48 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
225234
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
226235
props: PropsT
227236
): RenderingT {
228-
updateCachedWorkflowInstance(workflow)
229-
updatePropsAndState(props)
237+
val cachedWorkflowInstanceDidChange = updatePropsAndState(props, workflow)
230238

231-
baseRenderContext.unfreeze()
232-
val rendering = interceptedWorkflowInstance.render(props, state, context)
233-
baseRenderContext.freeze()
239+
if (!runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES) ||
240+
lastRendering == null ||
241+
subtreeStateDidChange) {
242+
if (!cachedWorkflowInstanceDidChange) {
243+
// If we haven't already updated the cached instance, better do it now!
244+
updateCachedWorkflowInstance(workflow)
245+
}
246+
baseRenderContext.unfreeze()
247+
lastRendering = interceptedWorkflowInstance.render(props, state, context)
248+
baseRenderContext.freeze()
234249

235-
workflowTracer.trace("UpdateRuntimeTree") {
236-
// Tear down workflows and workers that are obsolete.
237-
subtreeManager.commitRenderedChildren()
238-
// Side effect jobs are launched lazily, since they can send actions to the sink, and can only
239-
// be started after context is frozen.
240-
sideEffects.forEachStaging { it.job.start() }
241-
sideEffects.commitStaging { it.job.cancel() }
250+
workflowTracer.trace("UpdateRuntimeTree") {
251+
// Tear down workflows and workers that are obsolete.
252+
subtreeManager.commitRenderedChildren()
253+
// Side effect jobs are launched lazily, since they can send actions to the sink, and can only
254+
// be started after context is frozen.
255+
sideEffects.forEachStaging { it.job.start() }
256+
sideEffects.commitStaging { it.job.cancel() }
257+
}
242258
}
243259

244-
return rendering
260+
return lastRendering!!
245261
}
246262

263+
/**
264+
* @return true if the [interceptedWorkflowInstance] has been updated, false otherwise.
265+
*/
247266
private fun updatePropsAndState(
248-
newProps: PropsT
249-
) {
267+
newProps: PropsT,
268+
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
269+
): Boolean {
270+
var updatedCachedWorkflowInstance = false
250271
if (newProps != lastProps) {
272+
updatedCachedWorkflowInstance = updateCachedWorkflowInstance(workflow)
251273
val newState = interceptedWorkflowInstance.onPropsChanged(lastProps, newProps, state)
252274
state = newState
275+
subtreeStateDidChange = true
253276
}
254277
lastProps = newProps
278+
return updatedCachedWorkflowInstance
255279
}
256280

257281
/**
@@ -270,6 +294,8 @@ internal class WorkflowNode<PropsT, StateT, OutputT, RenderingT>(
270294
// Changing state is sticky, we pass it up if it ever changed.
271295
stateChanged = actionApplied.stateChanged || (childResult?.stateChanged ?: false)
272296
)
297+
// Our state changed or one of our children's state changed.
298+
subtreeStateDidChange = aggregateActionApplied.stateChanged
273299
return if (actionApplied.output != null) {
274300
emitAppliedActionToParent(aggregateActionApplied)
275301
} else {

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

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,23 +207,37 @@ class WorkflowOperatorsTest {
207207
private abstract class StateFlowWorkflow<T>(
208208
val name: String,
209209
val flow: StateFlow<T>
210-
) : StatelessWorkflow<Unit, Nothing, T>() {
210+
) : StatefulWorkflow<Unit, T, Nothing, T>() {
211211
var starts: Int = 0
212212
private set
213213

214+
override fun initialState(
215+
props: Unit,
216+
snapshot: Snapshot?
217+
): T {
218+
return flow.value
219+
}
220+
214221
private val rerenderWorker = object : Worker<T> {
215222
override fun run(): Flow<T> = flow.onStart { starts++ }
216223
}
217224

218225
override fun render(
219226
renderProps: Unit,
227+
renderState: T,
220228
context: RenderContext
221229
): T {
222230
// Listen to the flow to trigger a re-render when it updates.
223-
context.runningWorker(rerenderWorker as Worker<Any?>) { WorkflowAction.noAction() }
224-
return flow.value
231+
context.runningWorker(rerenderWorker) { output: T ->
232+
action("rerenderUpdate") {
233+
state = output
234+
}
235+
}
236+
return renderState
225237
}
226238

239+
override fun snapshotState(state: T): Snapshot? = null
240+
227241
override fun toString(): String = "StateFlowWorkflow($name)"
228242
}
229243
}

workflow-tracing/src/test/resources/com/squareup/workflow1/diagnostic/tracing/expected_trace_file.txt

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,6 @@
6363
{"name":"Sink received: Worker<String> (2)","cat":"update","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"action":"action(EmitWorkerOutputAction(worker=TypedWorker(java.lang.String (Kotlin reflection is not available)), key=))"}},
6464
{"name":"WorkflowAction: Worker<String> (2)","cat":"update","ph":"i","ts":0,"pid":0,"tid":0,"s":"p","args":{"action":"action(EmitWorkerOutputAction(worker=TypedWorker(java.lang.String (Kotlin reflection is not available)), key=))","oldState":"0","newState":"{no change}","output":"fired!"}},
6565
{"name":"Worker<String> (2)","ph":"O","ts":0,"pid":0,"tid":0,"id":"2","args":{"snapshot":"0"}},
66-
{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"3"}},
67-
{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}},
68-
{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"3","state":"changed state"}},
69-
{"name":"TestWorkflow (1)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"1","props":"0","state":"initial"}},
70-
{"name":"TestWorkflow (1)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}},
71-
{"name":"Props changed: Worker<String> (2)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"TypedWorker(java.lang.String (Kotlin reflection is not available))","newProps":"TypedWorker(java.lang.String (Kotlin reflection is not available))","oldState":"0","newState":"{no change}"}},
72-
{"name":"Worker<String> (2)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"2","props":"TypedWorker(java.lang.String (Kotlin reflection is not available))","state":"0"}},
73-
{"name":"Worker<String> (2)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"kotlin.Unit"}},
74-
{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}},
75-
{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}},
76-
{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}},
7766
{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}},
7867
{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}},
7968
{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"3","newProps":"4","oldState":"changed state","newState":"{no change}"}},

0 commit comments

Comments
 (0)