Skip to content

Commit 1cee014

Browse files
Step 1/2: Introduce debug snapshots for emitting diagnostic information about the workflow tree.
Partial implementation of #343.
1 parent 3ae6053 commit 1cee014

File tree

20 files changed

+602
-82
lines changed

20 files changed

+602
-82
lines changed

kotlin/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import com.squareup.workflow.ui.ViewRegistry
2424
import com.squareup.workflow.ui.WorkflowRunner
2525
import com.squareup.workflow.ui.setContentWorkflow
2626
import com.squareup.workflow.ui.workflowOnBackPressed
27+
import io.reactivex.disposables.CompositeDisposable
2728
import io.reactivex.disposables.Disposables
2829
import timber.log.Timber
2930

@@ -48,7 +49,10 @@ class MainActivity : AppCompatActivity() {
4849
finish()
4950
}
5051

51-
loggingSub = workflowRunner.renderings.subscribe { Timber.d("rendering: %s", it) }
52+
loggingSub = CompositeDisposable(
53+
workflowRunner.renderings.subscribe { Timber.d("rendering: %s", it) },
54+
workflowRunner.debugSnapshots.subscribe { Timber.v("debug snapshot: %s", it) }
55+
)
5256
}
5357

5458
override fun onBackPressed() {

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/LaunchWorkflow.kt

+10-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.squareup.workflow
1717

18+
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot
1819
import com.squareup.workflow.internal.RealWorkflowLoop
1920
import com.squareup.workflow.internal.WorkflowLoop
2021
import com.squareup.workflow.internal.unwrapCancellationCause
@@ -151,10 +152,15 @@ internal fun <PropsT, StateT, OutputT : Any, RenderingT, RunnerT> launchWorkflow
151152
): RunnerT {
152153
val renderingsAndSnapshots = ConflatedBroadcastChannel<RenderingAndSnapshot<RenderingT>>()
153154
val outputs = BroadcastChannel<OutputT>(capacity = 1)
155+
val debugSnapshots = ConflatedBroadcastChannel<WorkflowHierarchyDebugSnapshot>()
154156
val workflowScope = scope + Job(parent = scope.coroutineContext[Job])
155157

156158
// Give the caller a chance to start collecting outputs.
157-
val session = WorkflowSession(renderingsAndSnapshots.asFlow(), outputs.asFlow())
159+
val session = WorkflowSession(
160+
renderingsAndSnapshots.asFlow(),
161+
outputs.asFlow(),
162+
debugSnapshots.asFlow()
163+
)
158164
val result = beforeStart(workflowScope, session)
159165

160166
val workflowJob = workflowScope.launch {
@@ -165,7 +171,8 @@ internal fun <PropsT, StateT, OutputT : Any, RenderingT, RunnerT> launchWorkflow
165171
initialSnapshot = initialSnapshot,
166172
initialState = initialState,
167173
onRendering = renderingsAndSnapshots::send,
168-
onOutput = outputs::send
174+
onOutput = outputs::send,
175+
onDebugSnapshot = debugSnapshots::send
169176
)
170177
}
171178

@@ -176,6 +183,7 @@ internal fun <PropsT, StateT, OutputT : Any, RenderingT, RunnerT> launchWorkflow
176183
val realCause = cause?.unwrapCancellationCause()
177184
renderingsAndSnapshots.close(realCause)
178185
outputs.close(realCause)
186+
debugSnapshots.close(realCause)
179187

180188
// If the cancellation came from inside the workflow loop, the outer runtime scope needs to be
181189
// explicitly cancelled. See https://github.com/square/workflow/issues/464.

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/WorkflowSession.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,18 @@
1515
*/
1616
package com.squareup.workflow
1717

18+
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot
1819
import kotlinx.coroutines.flow.Flow
1920

2021
/**
2122
* A tuple of [Flow]s representing all the emissions from the workflow runtime.
2223
*
2324
* Passed to the function taken by [launchWorkflowIn].
25+
*
26+
* @param debugSnapshots A stream of diagnostic information about the runtime.
2427
*/
2528
class WorkflowSession<out OutputT : Any, out RenderingT>(
2629
val renderingsAndSnapshots: Flow<RenderingAndSnapshot<RenderingT>>,
27-
val outputs: Flow<OutputT>
30+
val outputs: Flow<OutputT>,
31+
val debugSnapshots: Flow<WorkflowHierarchyDebugSnapshot>
2832
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Copyright 2019 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.workflow.debugging
17+
18+
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot.Child
19+
import com.squareup.workflow.internal.WorkflowId
20+
import kotlin.LazyThreadSafetyMode.PUBLICATION
21+
22+
/**
23+
* Describes a tree of snapshots of the states of the entire workflow tree after a particular render
24+
* pass.
25+
*
26+
* Emitted from [com.squareup.workflow.WorkflowSession.debugInfo].
27+
*
28+
* @param workflowType A string representation of the type of this workflow.
29+
* @param state The actual state value of the workflow.
30+
* @param children All the child workflows that were rendered by this workflow in the last render
31+
* pass. See [Child].
32+
*/
33+
data class WorkflowHierarchyDebugSnapshot(
34+
val workflowType: String,
35+
val props: Any?,
36+
val state: Any?,
37+
val rendering: Any?,
38+
val children: List<Child>
39+
) {
40+
/** Convenience constructor to get the workflow type string the right way. */
41+
constructor(
42+
workflowId: WorkflowId<*, *, *>,
43+
props: Any?,
44+
state: Any?,
45+
rendering: Any?,
46+
children: List<Child>
47+
) : this(workflowId.typeDebugString, props, state, rendering, children)
48+
49+
/**
50+
* Represents a workflow that was rendered by the workflow represented by a particular
51+
* [WorkflowHierarchyDebugSnapshot].
52+
*
53+
* @param key The string key that was used to render this child.
54+
* @param snapshot The [WorkflowHierarchyDebugSnapshot] that describes the state of this child.
55+
*/
56+
data class Child(
57+
val key: String,
58+
val snapshot: WorkflowHierarchyDebugSnapshot
59+
)
60+
61+
/**
62+
* This formatted string is expensive to generate, so cache it.
63+
*/
64+
private val lazyDescription by lazy(PUBLICATION) {
65+
buildString {
66+
writeSnapshot(this@WorkflowHierarchyDebugSnapshot)
67+
}
68+
}
69+
70+
override fun toString(): String = """
71+
|WorkflowHierarchyDebugSnapshot(
72+
${toDescriptionString().trimEnd().prependIndent("| ")}
73+
|)
74+
""".trimMargin("|")
75+
76+
/**
77+
* Generates a multi-line, recursive string describing the update.
78+
*/
79+
fun toDescriptionString(): String = lazyDescription
80+
}
81+
82+
private fun StringBuilder.writeSnapshot(snapshot: WorkflowHierarchyDebugSnapshot) {
83+
append("workflowType: ")
84+
appendln(snapshot.workflowType)
85+
86+
append("props: ")
87+
appendln(snapshot.props)
88+
89+
append("state: ")
90+
appendln(snapshot.state)
91+
92+
append("rendering: ")
93+
append(snapshot.rendering)
94+
95+
if (snapshot.children.isNotEmpty()) {
96+
appendln()
97+
appendln("children (${snapshot.children.size}):")
98+
99+
val childIndent = "| "
100+
for ((index, child) in snapshot.children.withIndex()) {
101+
append("| key: ")
102+
appendln(child.key.ifEmpty { "{no key}" })
103+
append(
104+
child.snapshot.toDescriptionString()
105+
.prependIndent(childIndent)
106+
)
107+
if (index < snapshot.children.size - 1) appendln()
108+
}
109+
}
110+
}

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/Behavior.kt

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.squareup.workflow.internal
1818
import com.squareup.workflow.Worker
1919
import com.squareup.workflow.Workflow
2020
import com.squareup.workflow.WorkflowAction
21+
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot.Child
2122
import kotlinx.coroutines.Deferred
2223

2324
/**
@@ -29,6 +30,7 @@ import kotlinx.coroutines.Deferred
2930
*/
3031
data class Behavior<StateT, out OutputT : Any> internal constructor(
3132
val childCases: List<WorkflowOutputCase<*, *, StateT, OutputT>>,
33+
val childDebugSnapshots: List<Child>,
3234
val workerCases: List<WorkerCase<*, StateT, OutputT>>,
3335
val nextActionFromEvent: Deferred<WorkflowAction<StateT, OutputT>>
3436
) {

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/RealRenderContext.kt

+9-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import com.squareup.workflow.Sink
2323
import com.squareup.workflow.Worker
2424
import com.squareup.workflow.Workflow
2525
import com.squareup.workflow.WorkflowAction
26+
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot.Child
2627
import com.squareup.workflow.internal.Behavior.WorkerCase
2728
import com.squareup.workflow.internal.Behavior.WorkflowOutputCase
2829
import kotlinx.coroutines.CompletableDeferred
@@ -42,12 +43,13 @@ class RealRenderContext<StateT, OutputT : Any>(
4243
child: Workflow<ChildPropsT, ChildOutputT, ChildRenderingT>,
4344
id: WorkflowId<ChildPropsT, ChildOutputT, ChildRenderingT>,
4445
props: ChildPropsT
45-
): ChildRenderingT
46+
): RenderingEnvelope<ChildRenderingT>
4647
}
4748

4849
private val nextUpdateFromEvent = CompletableDeferred<WorkflowAction<StateT, OutputT>>()
4950
private val workerCases = mutableListOf<WorkerCase<*, StateT, OutputT>>()
5051
private val childCases = mutableListOf<WorkflowOutputCase<*, *, StateT, OutputT>>()
52+
private val childDebugSnapshots = mutableListOf<Child>()
5153

5254
/** Used to prevent modifications to this object after [buildBehavior] is called. */
5355
private var frozen = false
@@ -93,7 +95,11 @@ class RealRenderContext<StateT, OutputT : Any>(
9395
val case: WorkflowOutputCase<ChildPropsT, ChildOutputT, StateT, OutputT> =
9496
WorkflowOutputCase(child, id, props, handler)
9597
childCases += case
96-
return renderer.render(case, child, id, props)
98+
val (rendering, debugSnapshot) = renderer.render(case, child, id, props)
99+
// Hold onto the description of this child's state for later, so we can include it in the
100+
// parent's debug snapshot tree.
101+
childDebugSnapshots += Child(key, debugSnapshot)
102+
return rendering
97103
}
98104

99105
override fun <T> runningWorker(
@@ -113,6 +119,7 @@ class RealRenderContext<StateT, OutputT : Any>(
113119
frozen = true
114120
return Behavior(
115121
childCases = childCases.toList(),
122+
childDebugSnapshots = childDebugSnapshots.toList(),
116123
workerCases = workerCases.toList(),
117124
nextActionFromEvent = nextUpdateFromEvent
118125
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright 2019 Square Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.squareup.workflow.internal
17+
18+
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot
19+
20+
/**
21+
* Simple holder for a rendering generated by a render pass, and the
22+
* [WorkflowHierarchyDebugSnapshot] that contains diagnostic information about the workflows
23+
* rendered in that pass.
24+
*/
25+
data class RenderingEnvelope<R>(
26+
val rendering: R,
27+
val debugSnapshot: WorkflowHierarchyDebugSnapshot
28+
)

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/SubtreeManager.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ internal class SubtreeManager<StateT, OutputT : Any>(
5858
child: Workflow<ChildPropsT, ChildOutputT, ChildRenderingT>,
5959
id: WorkflowId<ChildPropsT, ChildOutputT, ChildRenderingT>,
6060
props: ChildPropsT
61-
): ChildRenderingT {
61+
): RenderingEnvelope<ChildRenderingT> {
6262
// @formatter:on
6363
// Start tracking this case so we can be ready to render it.
6464
@Suppress("UNCHECKED_CAST")

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowId.kt

+7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ internal constructor(
3939
workflow: Workflow<PropsT, OutputT, RenderingT>,
4040
name: String = ""
4141
) : this(workflow::class, name)
42+
43+
/**
44+
* String representation of this workflow's type (i.e. class name), suitable for use in
45+
* diagnostic output (see [WorkflowHierarchyDebugSnapshot][com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot]
46+
* ).
47+
*/
48+
val typeDebugString: String = type.java.name
4249
}
4350

4451
@Suppress("unused")

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowLoop.kt

+7-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.squareup.workflow.internal
1818
import com.squareup.workflow.RenderingAndSnapshot
1919
import com.squareup.workflow.Snapshot
2020
import com.squareup.workflow.StatefulWorkflow
21+
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot
2122
import kotlinx.coroutines.ExperimentalCoroutinesApi
2223
import kotlinx.coroutines.FlowPreview
2324
import kotlinx.coroutines.channels.consume
@@ -43,7 +44,8 @@ internal interface WorkflowLoop {
4344
initialSnapshot: Snapshot?,
4445
initialState: StateT? = null,
4546
onRendering: suspend (RenderingAndSnapshot<RenderingT>) -> Unit,
46-
onOutput: suspend (OutputT) -> Unit
47+
onOutput: suspend (OutputT) -> Unit,
48+
onDebugSnapshot: suspend (WorkflowHierarchyDebugSnapshot) -> Unit
4749
): Nothing
4850
}
4951

@@ -56,7 +58,8 @@ internal object RealWorkflowLoop : WorkflowLoop {
5658
initialSnapshot: Snapshot?,
5759
initialState: StateT?,
5860
onRendering: suspend (RenderingAndSnapshot<RenderingT>) -> Unit,
59-
onOutput: suspend (OutputT) -> Unit
61+
onOutput: suspend (OutputT) -> Unit,
62+
onDebugSnapshot: suspend (WorkflowHierarchyDebugSnapshot) -> Unit
6063
): Nothing = coroutineScope {
6164
val inputsChannel = props.produceIn(this)
6265
inputsChannel.consume {
@@ -76,10 +79,11 @@ internal object RealWorkflowLoop : WorkflowLoop {
7679
while (true) {
7780
coroutineContext.ensureActive()
7881

79-
val rendering = rootNode.render(workflow, input)
82+
val (rendering, debugSnapshot) = rootNode.render(workflow, input)
8083
val snapshot = rootNode.snapshot(workflow)
8184

8285
onRendering(RenderingAndSnapshot(rendering, snapshot))
86+
onDebugSnapshot(debugSnapshot)
8387
output?.let { onOutput(it) }
8488

8589
// Tick _might_ return an output, but if it returns null, it means the state or a child

kotlin/workflow-runtime/src/main/java/com/squareup/workflow/internal/WorkflowNode.kt

+12-3
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.squareup.workflow.StatefulWorkflow
2020
import com.squareup.workflow.Workflow
2121
import com.squareup.workflow.WorkflowAction
2222
import com.squareup.workflow.applyTo
23+
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot
2324
import com.squareup.workflow.internal.Behavior.WorkerCase
2425
import com.squareup.workflow.parse
2526
import com.squareup.workflow.readByteStringWithLength
@@ -90,7 +91,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
9091
fun render(
9192
workflow: StatefulWorkflow<PropsT, *, OutputT, RenderingT>,
9293
input: PropsT
93-
): RenderingT =
94+
): RenderingEnvelope<RenderingT> =
9495
renderWithStateType(workflow as StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>, input)
9596

9697
/**
@@ -176,7 +177,7 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
176177
private fun renderWithStateType(
177178
workflow: StatefulWorkflow<PropsT, StateT, OutputT, RenderingT>,
178179
input: PropsT
179-
): RenderingT {
180+
): RenderingEnvelope<RenderingT> {
180181
updatePropsAndState(workflow, input)
181182

182183
val context = RealRenderContext(subtreeManager)
@@ -189,7 +190,15 @@ internal class WorkflowNode<PropsT, StateT, OutputT : Any, RenderingT>(
189190
workerTracker.track(workerCases)
190191
}
191192

192-
return rendering
193+
val debugSnapshot = WorkflowHierarchyDebugSnapshot(
194+
workflowId = id,
195+
props = input,
196+
state = state,
197+
rendering = rendering,
198+
children = behavior!!.childDebugSnapshots
199+
)
200+
201+
return RenderingEnvelope(rendering, debugSnapshot)
193202
}
194203

195204
private fun updatePropsAndState(

0 commit comments

Comments
 (0)