Skip to content

Commit 29bae34

Browse files
Step 2/2: Introduce diagnostics for workflow outputs.
Closes #343.
1 parent 1cee014 commit 29bae34

File tree

16 files changed

+663
-72
lines changed

16 files changed

+663
-72
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ class MainActivity : AppCompatActivity() {
5151

5252
loggingSub = CompositeDisposable(
5353
workflowRunner.renderings.subscribe { Timber.d("rendering: %s", it) },
54-
workflowRunner.debugSnapshots.subscribe { Timber.v("debug snapshot: %s", it) }
54+
workflowRunner.debugInfo.subscribe { Timber.v("debug snapshot: %s", it) }
5555
)
5656
}
5757

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

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

18-
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot
18+
import com.squareup.workflow.debugging.WorkflowDebugInfo
1919
import com.squareup.workflow.internal.RealWorkflowLoop
2020
import com.squareup.workflow.internal.WorkflowLoop
2121
import com.squareup.workflow.internal.unwrapCancellationCause
@@ -152,7 +152,7 @@ internal fun <PropsT, StateT, OutputT : Any, RenderingT, RunnerT> launchWorkflow
152152
): RunnerT {
153153
val renderingsAndSnapshots = ConflatedBroadcastChannel<RenderingAndSnapshot<RenderingT>>()
154154
val outputs = BroadcastChannel<OutputT>(capacity = 1)
155-
val debugSnapshots = ConflatedBroadcastChannel<WorkflowHierarchyDebugSnapshot>()
155+
val debugSnapshots = ConflatedBroadcastChannel<WorkflowDebugInfo>()
156156
val workflowScope = scope + Job(parent = scope.coroutineContext[Job])
157157

158158
// Give the caller a chance to start collecting outputs.

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

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

18-
import com.squareup.workflow.debugging.WorkflowHierarchyDebugSnapshot
18+
import com.squareup.workflow.debugging.WorkflowDebugInfo
1919
import kotlinx.coroutines.flow.Flow
2020

2121
/**
2222
* A tuple of [Flow]s representing all the emissions from the workflow runtime.
2323
*
2424
* Passed to the function taken by [launchWorkflowIn].
2525
*
26-
* @param debugSnapshots A stream of diagnostic information about the runtime.
26+
* @param debugInfo A stream of [diagnostic information][WorkflowDebugInfo] about the runtime.
2727
*/
2828
class WorkflowSession<out OutputT : Any, out RenderingT>(
2929
val renderingsAndSnapshots: Flow<RenderingAndSnapshot<RenderingT>>,
3030
val outputs: Flow<OutputT>,
31-
val debugSnapshots: Flow<WorkflowHierarchyDebugSnapshot>
31+
val debugInfo: Flow<WorkflowDebugInfo>
3232
)
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.debugging
17+
18+
/**
19+
* A pair of [WorkflowHierarchyDebugSnapshot] and optional [WorkflowUpdateDebugInfo].
20+
*
21+
* The [WorkflowUpdateDebugInfo] will be null on the first render pass and non-null on every
22+
* subsequent pass, since the first pass is the only one that is not triggered by some kind of
23+
* update.
24+
*/
25+
data class WorkflowDebugInfo(
26+
val hierarchySnapshot: WorkflowHierarchyDebugSnapshot,
27+
val updateInfo: WorkflowUpdateDebugInfo?
28+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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.WorkflowUpdateDebugInfo.Kind
19+
import com.squareup.workflow.debugging.WorkflowUpdateDebugInfo.Kind.Passthrough
20+
import com.squareup.workflow.debugging.WorkflowUpdateDebugInfo.Kind.Updated
21+
import com.squareup.workflow.debugging.WorkflowUpdateDebugInfo.Source
22+
import com.squareup.workflow.debugging.WorkflowUpdateDebugInfo.Source.Sink
23+
import com.squareup.workflow.debugging.WorkflowUpdateDebugInfo.Source.Subtree
24+
import com.squareup.workflow.debugging.WorkflowUpdateDebugInfo.Source.Worker
25+
import com.squareup.workflow.internal.WorkflowId
26+
import kotlin.LazyThreadSafetyMode.PUBLICATION
27+
28+
/**
29+
* A description of a workflow update triggered by a [Source] (worker, event, etc).
30+
*
31+
* This is a simple linked list that represents a traversal down the workflow tree that starts at
32+
* the root and indicates, for each workflow, if its child output something that it handled
33+
* (see [Kind]), or if it was just a parent of a workflow that didn't output anything.
34+
*
35+
* When a workflow handles an update, the type of update is indicated by [Source].
36+
*/
37+
data class WorkflowUpdateDebugInfo(
38+
val workflowType: String,
39+
val kind: Kind
40+
) {
41+
constructor(
42+
workflowId: WorkflowId<*, *, *>,
43+
kind: Kind
44+
) : this(workflowId.typeDebugString, kind)
45+
46+
/**
47+
* A sealed class that indicates whether a workflow actually executed a `WorkflowAction`, or
48+
* was just the ancestor of a workflow that did.
49+
*
50+
* Contains two subclasses, see their documentation for details:
51+
* - [Updated]
52+
* - [Passthrough]
53+
*/
54+
sealed class Kind {
55+
/**
56+
* Indicates that this workflow actually executed a `WorkflowAction`.
57+
* [Updated.source] is a [Source] that indicates if the action was triggered by:
58+
* - An event ([Source.Sink])
59+
* - A worker ([Source.Worker])
60+
* - A child workflow emitting an output ([Source.Subtree])
61+
*/
62+
data class Updated(val source: Source) : Kind()
63+
64+
/**
65+
* Indicates that one of this workflow's descendants executed a
66+
* `WorkflowAction`, but none of its immediate children emitted an output, so this workflow
67+
* didn't get directly notified about it.
68+
*/
69+
data class Passthrough(
70+
val key: String,
71+
val childInfo: WorkflowUpdateDebugInfo
72+
) : Kind()
73+
}
74+
75+
/**
76+
* A sealed class that indicates what triggered the update.
77+
*
78+
* Contains three subclasses, see their documentation for details:
79+
* - [Sink]
80+
* - [Worker]
81+
* - [Subtree]
82+
*/
83+
sealed class Source {
84+
/**
85+
* Indicates that the update was triggered by an event being received by a
86+
* [Sink][com.squareup.workflow.Sink] (see
87+
* [makeActionSink][com.squareup.workflow.RenderContext.makeActionSink]).
88+
*/
89+
object Sink : Source()
90+
91+
/**
92+
* Indicates that the update was triggered by an output emitted from a
93+
* [Worker][com.squareup.workflow.Worker] being run by this workflow.
94+
*
95+
* @param key The string key of the worker that emitted the output.
96+
* @param output The value emitted from the worker.
97+
*/
98+
data class Worker(
99+
val key: String,
100+
val output: Any
101+
) : Source()
102+
103+
/**
104+
* Indicates that the update was triggered by an output emitted from a child workflow that was
105+
* rendered by this workflow.
106+
*
107+
* @param key The string key of the child that emitted the output.
108+
* @param output The value emitted from the child.
109+
* @param childInfo The [WorkflowUpdateDebugInfo] that describes the child's update.
110+
*/
111+
data class Subtree(
112+
val key: String,
113+
val output: Any,
114+
val childInfo: WorkflowUpdateDebugInfo
115+
) : Source()
116+
}
117+
118+
/**
119+
* This string is expensive to generate, so cache it.
120+
*/
121+
private val lazyDescription by lazy(PUBLICATION) {
122+
buildString {
123+
writeUpdate(this@WorkflowUpdateDebugInfo)
124+
}
125+
}
126+
127+
override fun toString(): String = """
128+
|WorkflowUpdateDebugInfo(
129+
|${toDescriptionString().trimEnd().prependIndent(" ")}
130+
|)
131+
""".trimMargin("|")
132+
133+
/**
134+
* Generates a multi-line, recursive string describing the update.
135+
*/
136+
fun toDescriptionString(): String = lazyDescription
137+
}
138+
139+
private fun StringBuilder.writeUpdate(update: WorkflowUpdateDebugInfo) {
140+
append(update.workflowType)
141+
append(' ')
142+
143+
when (val kind = update.kind) {
144+
is Updated -> {
145+
append("updated from ")
146+
when (val source = kind.source) {
147+
Sink -> append("sink")
148+
is Worker -> {
149+
append("worker[key=")
150+
append(source.key)
151+
append("]: ")
152+
append(source.output)
153+
}
154+
is Subtree -> {
155+
append("workflow[key=")
156+
append(source.key)
157+
append("]: ")
158+
appendln(source.output)
159+
append("")
160+
append(source.childInfo.toDescriptionString())
161+
}
162+
}
163+
}
164+
is Passthrough -> {
165+
append("passing through from workflow[key=")
166+
append(kind.key)
167+
appendln("]")
168+
append("")
169+
append(kind.childInfo.toDescriptionString())
170+
}
171+
}
172+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.WorkflowUpdateDebugInfo
19+
20+
/**
21+
* Simple holder for the output generated by a tick pass, and the
22+
* [WorkflowUpdateDebugInfo] that contains diagnostic information about the update.
23+
*/
24+
internal class OutputEnvelope<out O : Any>(
25+
val output: O?,
26+
val debugInfo: WorkflowUpdateDebugInfo
27+
)

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

+28-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import com.squareup.workflow.Snapshot
1919
import com.squareup.workflow.StatefulWorkflow
2020
import com.squareup.workflow.Workflow
2121
import com.squareup.workflow.WorkflowAction
22+
import com.squareup.workflow.WorkflowAction.Companion.noAction
23+
import com.squareup.workflow.debugging.WorkflowUpdateDebugInfo.Kind
24+
import com.squareup.workflow.debugging.WorkflowUpdateDebugInfo.Source
2225
import com.squareup.workflow.internal.Behavior.WorkflowOutputCase
2326
import com.squareup.workflow.parse
2427
import com.squareup.workflow.readByteStringWithLength
@@ -79,15 +82,36 @@ internal class SubtreeManager<StateT, OutputT : Any>(
7982
/**
8083
* Uses [selector] to invoke [WorkflowNode.tick] for every running child workflow this instance
8184
* is managing.
85+
*
86+
* If one of this workflow's children emits an output, [handler] will be called with the action
87+
* assigned to that workflow and a [Source.Subtree] describing the child that triggered the
88+
* update.
89+
*
90+
* Note that if [handler] is called with [Kind.Passthrough], the workflow action that gets
91+
* passed in will always be [noAction]. This is the only logical value because
92+
* [Kind.Passthrough] specifically means that this workflow did not actually get updated
93+
* itself, but one of its descendants did and this one is just part of the path down the tree.
8294
*/
8395
fun <T : Any> tickChildren(
84-
selector: SelectBuilder<T?>,
85-
handler: (WorkflowAction<StateT, OutputT>) -> T?
96+
selector: SelectBuilder<OutputEnvelope<T>>,
97+
handler: (WorkflowAction<StateT, OutputT>, Kind) -> OutputEnvelope<T>
8698
) {
8799
for ((case, host) in hostLifetimeTracker.lifetimes) {
88100
host.tick(selector) { output ->
89-
val componentUpdate = case.acceptChildOutput(output)
90-
return@tick handler(componentUpdate)
101+
return@tick if (output.output != null) {
102+
val componentUpdate = case.acceptChildOutput(output.output)
103+
// This workflow's child emitted a non-null output: the WorkflowNode will execute the
104+
// WorkflowAction, and we need to pass DidUpdate to indicate that this workflow actually
105+
// got updated.
106+
handler(
107+
componentUpdate,
108+
Kind.Updated(Source.Subtree(case.id.name, output.output, output.debugInfo))
109+
)
110+
} else {
111+
// The child didn't actually emit an output, which means this workflow has nothing to do,
112+
// which means we will only report our child updating.
113+
handler(noAction(), Kind.Passthrough(case.id.name, output.debugInfo))
114+
}
91115
}
92116
}
93117
}

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

+6-6
Original file line numberDiff line numberDiff line change
@@ -18,7 +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
21+
import com.squareup.workflow.debugging.WorkflowDebugInfo
2222
import kotlinx.coroutines.ExperimentalCoroutinesApi
2323
import kotlinx.coroutines.FlowPreview
2424
import kotlinx.coroutines.channels.consume
@@ -45,7 +45,7 @@ internal interface WorkflowLoop {
4545
initialState: StateT? = null,
4646
onRendering: suspend (RenderingAndSnapshot<RenderingT>) -> Unit,
4747
onOutput: suspend (OutputT) -> Unit,
48-
onDebugSnapshot: suspend (WorkflowHierarchyDebugSnapshot) -> Unit
48+
onDebugSnapshot: suspend (WorkflowDebugInfo) -> Unit
4949
): Nothing
5050
}
5151

@@ -59,11 +59,11 @@ internal object RealWorkflowLoop : WorkflowLoop {
5959
initialState: StateT?,
6060
onRendering: suspend (RenderingAndSnapshot<RenderingT>) -> Unit,
6161
onOutput: suspend (OutputT) -> Unit,
62-
onDebugSnapshot: suspend (WorkflowHierarchyDebugSnapshot) -> Unit
62+
onDebugSnapshot: suspend (WorkflowDebugInfo) -> Unit
6363
): Nothing = coroutineScope {
6464
val inputsChannel = props.produceIn(this)
6565
inputsChannel.consume {
66-
var output: OutputT? = null
66+
var output: OutputEnvelope<OutputT>? = null
6767
var input: PropsT = inputsChannel.receive()
6868
var inputsClosed = false
6969
val rootNode = WorkflowNode(
@@ -83,8 +83,8 @@ internal object RealWorkflowLoop : WorkflowLoop {
8383
val snapshot = rootNode.snapshot(workflow)
8484

8585
onRendering(RenderingAndSnapshot(rendering, snapshot))
86-
onDebugSnapshot(debugSnapshot)
87-
output?.let { onOutput(it) }
86+
onDebugSnapshot(WorkflowDebugInfo(debugSnapshot, output?.debugInfo))
87+
output?.output?.let { onOutput(it) }
8888

8989
// Tick _might_ return an output, but if it returns null, it means the state or a child
9090
// probably changed, so we should re-render/snapshot and emit again.

0 commit comments

Comments
 (0)