Skip to content

Commit 25c3697

Browse files
Rewrite the tracing infrastructure to use a listener-based approach.
The workflow runtime supports registering a single `WorkflowDiagnosticListener` at initialization that will receive events for all kinds of significant events in the runtime. This still supports the debug snapshotting and update information that Swift currently has, but also supports reporting much finer-grained profiling data. This supersedes #612, and closes #343 again.
1 parent cbeae8a commit 25c3697

File tree

37 files changed

+2084
-128
lines changed

37 files changed

+2084
-128
lines changed

kotlin/build.gradle

+2-3
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,8 @@ subprojects {
149149
project.tasks.findByName('check')?.dependsOn 'detekt'
150150

151151
project.configurations.configureEach {
152-
// No module depends on on Kotlin Reflect directly, but there could be transitive dependencies
153-
// in tests with a lower version. This could cause problems with a newer Kotlin version that
154-
// we use.
152+
// There could be transitive dependencies in tests with a lower version. This could cause
153+
// problems with a newer Kotlin version that we use.
155154
resolutionStrategy.force "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}"
156155
}
157156
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package com.squareup.sample.dungeon
1717

1818
import android.os.Bundle
1919
import androidx.appcompat.app.AppCompatActivity
20+
import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener
2021
import com.squareup.workflow.ui.WorkflowRunner
2122
import com.squareup.workflow.ui.setContentWorkflow
2223

@@ -34,7 +35,8 @@ class MainActivity : AppCompatActivity() {
3435
WorkflowRunner.Config(
3536
workflow = component.appWorkflow,
3637
viewRegistry = component.viewRegistry,
37-
props = "simple_maze.txt"
38+
props = "simple_maze.txt",
39+
diagnosticListener = SimpleLoggingDiagnosticListener()
3840
)
3941
}
4042
}

kotlin/samples/dungeon/common/src/main/java/com/squareup/sample/dungeon/GameWorkflow.kt

+2
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ private class TickerWorker(private val ticksPerSecond: Int) : Worker<Long> {
223223
delay(periodMs)
224224
}
225225
}
226+
227+
override fun toString(): String = "TickerWorker(ticksPerSecond=$ticksPerSecond)"
226228
}
227229

228230
private data class MoveResult(

kotlin/samples/hello-workflow-fragment/src/main/java/com/squareup/sample/helloworkflowfragment/HelloWorkflowFragment.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.squareup.sample.helloworkflowfragment
1717

18+
import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener
1819
import com.squareup.workflow.ui.ViewRegistry
1920
import com.squareup.workflow.ui.WorkflowFragment
2021
import com.squareup.workflow.ui.WorkflowRunner
@@ -23,6 +24,8 @@ private val viewRegistry = ViewRegistry(HelloFragmentLayoutRunner)
2324

2425
class HelloWorkflowFragment : WorkflowFragment<Unit, Unit>() {
2526
override fun onCreateWorkflow(): WorkflowRunner.Config<Unit, Unit> {
26-
return WorkflowRunner.Config(HelloWorkflow, viewRegistry)
27+
return WorkflowRunner.Config(HelloWorkflow, viewRegistry,
28+
diagnosticListener = SimpleLoggingDiagnosticListener()
29+
)
2730
}
2831
}

kotlin/samples/hello-workflow/src/main/java/com/squareup/sample/helloworkflow/HelloWorkflowActivity.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package com.squareup.sample.helloworkflow
1717

1818
import android.os.Bundle
1919
import androidx.appcompat.app.AppCompatActivity
20+
import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener
2021
import com.squareup.workflow.ui.ViewRegistry
2122
import com.squareup.workflow.ui.WorkflowRunner
2223
import com.squareup.workflow.ui.setContentWorkflow
@@ -29,7 +30,10 @@ class HelloWorkflowActivity : AppCompatActivity() {
2930
override fun onCreate(savedInstanceState: Bundle?) {
3031
super.onCreate(savedInstanceState)
3132
runner = setContentWorkflow(savedInstanceState) {
32-
WorkflowRunner.Config(HelloWorkflow, viewRegistry)
33+
WorkflowRunner.Config(
34+
HelloWorkflow, viewRegistry,
35+
diagnosticListener = SimpleLoggingDiagnosticListener()
36+
)
3337
}
3438
}
3539

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

+9-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import androidx.test.espresso.IdlingResource
2121
import com.squareup.sample.authworkflow.AuthViewBindings
2222
import com.squareup.sample.gameworkflow.TicTacToeViewBindings
2323
import com.squareup.sample.panel.PanelContainer
24+
import com.squareup.workflow.VeryExperimentalWorkflow
25+
import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener
2426
import com.squareup.workflow.ui.ViewRegistry
2527
import com.squareup.workflow.ui.WorkflowRunner
2628
import com.squareup.workflow.ui.setContentWorkflow
@@ -35,6 +37,7 @@ class MainActivity : AppCompatActivity() {
3537

3638
lateinit var idlingResource: IdlingResource
3739

40+
@UseExperimental(VeryExperimentalWorkflow::class)
3841
override fun onCreate(savedInstanceState: Bundle?) {
3942
super.onCreate(savedInstanceState)
4043

@@ -47,7 +50,12 @@ class MainActivity : AppCompatActivity() {
4750

4851
workflowRunner = setContentWorkflow(
4952
savedInstanceState,
50-
{ WorkflowRunner.Config(component.mainWorkflow, viewRegistry) }
53+
{
54+
WorkflowRunner.Config(
55+
component.mainWorkflow, viewRegistry,
56+
diagnosticListener = SimpleLoggingDiagnosticListener()
57+
)
58+
}
5159
) {
5260
finish()
5361
}

kotlin/samples/todo-android/app/src/main/java/com/squareup/sample/mainactivity/MainActivity.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package com.squareup.sample.mainactivity
1818
import android.os.Bundle
1919
import androidx.appcompat.app.AppCompatActivity
2020
import com.squareup.sample.todo.TodoListsAppWorkflow
21+
import com.squareup.workflow.diagnostic.SimpleLoggingDiagnosticListener
2122
import com.squareup.workflow.ui.ViewRegistry
2223
import com.squareup.workflow.ui.WorkflowRunner
2324
import com.squareup.workflow.ui.setContentWorkflow
@@ -36,7 +37,10 @@ class MainActivity : AppCompatActivity() {
3637
?: TodoListsAppWorkflow()
3738

3839
workflowRunner = setContentWorkflow(savedInstanceState) {
39-
WorkflowRunner.Config(rootWorkflow, viewRegistry)
40+
WorkflowRunner.Config(
41+
rootWorkflow, viewRegistry,
42+
diagnosticListener = SimpleLoggingDiagnosticListener()
43+
)
4044
}
4145
}
4246

kotlin/workflow-core/src/main/java/com/squareup/workflow/Worker.kt

+5
Original file line numberDiff line numberDiff line change
@@ -359,11 +359,14 @@ private class TimerWorker(
359359

360360
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean =
361361
otherWorker is TimerWorker && otherWorker.key == key
362+
363+
override fun toString(): String = "TimerWorker(delayMs=$delayMs)"
362364
}
363365

364366
private object FinishedWorker : Worker<Nothing> {
365367
override fun run(): Flow<Nothing> = emptyFlow()
366368
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean = otherWorker === FinishedWorker
369+
override fun toString(): String = "FinishedWorker"
367370
}
368371

369372
private class WorkerWrapper<T, R>(
@@ -374,4 +377,6 @@ private class WorkerWrapper<T, R>(
374377
override fun doesSameWorkAs(otherWorker: Worker<*>): Boolean =
375378
otherWorker is WorkerWrapper<*, *> &&
376379
wrapped.doesSameWorkAs(otherWorker.wrapped)
380+
381+
override fun toString(): String = "WorkerWrapper($wrapped)"
377382
}

kotlin/workflow-runtime/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,5 @@ dependencies {
3131
api deps.kotlin.coroutines.core
3232

3333
testImplementation deps.kotlin.test.jdk
34+
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${versions.kotlin}"
3435
}

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

+24-10
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,12 @@ fun <PropsT, StateT, OutputT : Any, RenderingT, RunnerT> launchWorkflowForTestFr
139139
beforeStart = beforeStart
140140
)
141141

142-
@UseExperimental(ExperimentalCoroutinesApi::class, FlowPreview::class)
142+
@UseExperimental(
143+
ExperimentalCoroutinesApi::class,
144+
FlowPreview::class,
145+
VeryExperimentalWorkflow::class
146+
)
147+
@Suppress("LongParameterList")
143148
internal fun <PropsT, StateT, OutputT : Any, RenderingT, RunnerT> launchWorkflowImpl(
144149
scope: CoroutineScope,
145150
workflowLoop: WorkflowLoop,
@@ -156,17 +161,26 @@ internal fun <PropsT, StateT, OutputT : Any, RenderingT, RunnerT> launchWorkflow
156161
// Give the caller a chance to start collecting outputs.
157162
val session = WorkflowSession(renderingsAndSnapshots.asFlow(), outputs.asFlow())
158163
val result = beforeStart(workflowScope, session)
164+
val visitor = session.diagnosticListener
159165

160166
val workflowJob = workflowScope.launch {
161-
// Run the workflow processing loop forever, or until it fails or is cancelled.
162-
workflowLoop.runWorkflowLoop(
163-
workflow,
164-
props,
165-
initialSnapshot = initialSnapshot,
166-
initialState = initialState,
167-
onRendering = renderingsAndSnapshots::send,
168-
onOutput = outputs::send
169-
)
167+
visitor?.onRuntimeStarted(this)
168+
try {
169+
// Run the workflow processing loop forever, or until it fails or is cancelled.
170+
workflowLoop.runWorkflowLoop(
171+
workflow,
172+
props,
173+
initialSnapshot = initialSnapshot,
174+
initialState = initialState,
175+
onRendering = renderingsAndSnapshots::send,
176+
onOutput = outputs::send,
177+
diagnosticListener = visitor
178+
)
179+
} finally {
180+
// Only emit the runtime stopped debug event after all child coroutines have completed.
181+
// coroutineScope does an implicit join on all its children.
182+
visitor?.onRuntimeStopped()
183+
}
170184
}
171185

172186
// Ensure we close the channels when we're done, so that they propagate errors.

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

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

18+
import com.squareup.workflow.diagnostic.WorkflowDiagnosticListener
1819
import kotlinx.coroutines.flow.Flow
1920

2021
/**
2122
* A tuple of [Flow]s representing all the emissions from the workflow runtime.
2223
*
23-
* Passed to the function taken by [launchWorkflowIn].
24+
* Passed to [launchWorkflowIn]'s `beforeStart` function.
25+
*
26+
* @param diagnosticListener Null by default. If set to a non-null value before `beforeStart`
27+
* returns, that [WorkflowDiagnosticListener] will receive all diagnostic events from the runtime.
28+
* Setting this property after `beforeStart` returns will have no effect.
2429
*/
30+
@UseExperimental(VeryExperimentalWorkflow::class)
2531
class WorkflowSession<out OutputT : Any, out RenderingT>(
2632
val renderingsAndSnapshots: Flow<RenderingAndSnapshot<RenderingT>>,
27-
val outputs: Flow<OutputT>
33+
val outputs: Flow<OutputT>,
34+
var diagnosticListener: WorkflowDiagnosticListener? = null
2835
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
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.diagnostic
17+
18+
import com.squareup.workflow.VeryExperimentalWorkflow
19+
import com.squareup.workflow.WorkflowAction
20+
import kotlinx.coroutines.CoroutineScope
21+
22+
/**
23+
* Returns a [WorkflowDiagnosticListener] that will delegate all method calls first to this
24+
* instance, and then to [next].
25+
*/
26+
fun WorkflowDiagnosticListener.andThen(
27+
next: WorkflowDiagnosticListener
28+
): WorkflowDiagnosticListener {
29+
return (this as? ChainedDiagnosticListener
30+
?: ChainedDiagnosticListener(this))
31+
.apply { addVisitor(next) }
32+
}
33+
34+
@UseExperimental(VeryExperimentalWorkflow::class)
35+
@Suppress("TooManyFunctions")
36+
internal class ChainedDiagnosticListener(
37+
listener: WorkflowDiagnosticListener
38+
) : WorkflowDiagnosticListener {
39+
40+
private val visitors = mutableListOf(listener)
41+
42+
fun addVisitor(listener: WorkflowDiagnosticListener) {
43+
if (listener is ChainedDiagnosticListener) {
44+
visitors.addAll(listener.visitors)
45+
} else {
46+
visitors += listener
47+
}
48+
}
49+
50+
override fun onBeforeRenderPass(props: Any?) {
51+
visitors.forEach { it.onBeforeRenderPass(props) }
52+
}
53+
54+
override fun onPropsChanged(
55+
workflowId: Long?,
56+
oldProps: Any?,
57+
newProps: Any?,
58+
oldState: Any?,
59+
newState: Any?
60+
) {
61+
visitors.forEach { it.onPropsChanged(workflowId, oldProps, newProps, oldState, newState) }
62+
}
63+
64+
override fun onBeforeWorkflowRendered(
65+
workflowId: Long,
66+
props: Any?,
67+
state: Any?
68+
) {
69+
visitors.forEach { it.onBeforeWorkflowRendered(workflowId, props, state) }
70+
}
71+
72+
override fun onAfterWorkflowRendered(
73+
workflowId: Long,
74+
rendering: Any?
75+
) {
76+
visitors.forEach { it.onAfterWorkflowRendered(workflowId, rendering) }
77+
}
78+
79+
override fun onAfterRenderPass(rendering: Any?) {
80+
visitors.forEach { it.onAfterRenderPass(rendering) }
81+
}
82+
83+
override fun onBeforeSnapshotPass() {
84+
visitors.forEach { it.onBeforeSnapshotPass() }
85+
}
86+
87+
override fun onAfterSnapshotPass() {
88+
visitors.forEach { it.onAfterSnapshotPass() }
89+
}
90+
91+
override fun onRuntimeStarted(workflowScope: CoroutineScope) {
92+
visitors.forEach { it.onRuntimeStarted(workflowScope) }
93+
}
94+
95+
override fun onRuntimeStopped() {
96+
visitors.forEach { it.onRuntimeStopped() }
97+
}
98+
99+
override fun onWorkflowStarted(
100+
workflowId: Long,
101+
parentId: Long?,
102+
workflowType: String,
103+
key: String,
104+
initialProps: Any?,
105+
initialState: Any?,
106+
restoredFromSnapshot: Boolean
107+
) {
108+
visitors.forEach {
109+
it.onWorkflowStarted(
110+
workflowId, parentId, workflowType, key, initialProps, initialState, restoredFromSnapshot
111+
)
112+
}
113+
}
114+
115+
override fun onWorkflowStopped(workflowId: Long) {
116+
visitors.forEach { it.onWorkflowStopped(workflowId) }
117+
}
118+
119+
override fun onWorkerStarted(
120+
workerId: Long,
121+
parentWorkflowId: Long,
122+
key: String,
123+
description: String
124+
) {
125+
visitors.forEach { it.onWorkerStarted(workerId, parentWorkflowId, key, description) }
126+
}
127+
128+
override fun onWorkerStopped(
129+
workerId: Long,
130+
parentWorkflowId: Long
131+
) {
132+
visitors.forEach { it.onWorkerStopped(workerId, parentWorkflowId) }
133+
}
134+
135+
override fun onWorkerOutput(
136+
workerId: Long,
137+
parentWorkflowId: Long,
138+
output: Any
139+
) {
140+
visitors.forEach { it.onWorkerOutput(workerId, parentWorkflowId, output) }
141+
}
142+
143+
override fun onSinkReceived(
144+
workflowId: Long,
145+
action: WorkflowAction<*, *>
146+
) {
147+
visitors.forEach { it.onSinkReceived(workflowId, action) }
148+
}
149+
150+
override fun onWorkflowAction(
151+
workflowId: Long,
152+
action: WorkflowAction<*, *>,
153+
oldState: Any?,
154+
newState: Any?,
155+
output: Any?
156+
) {
157+
visitors.forEach { it.onWorkflowAction(workflowId, action, oldState, newState, output) }
158+
}
159+
}

0 commit comments

Comments
 (0)