Skip to content

Commit b6e70a0

Browse files
authored
Merge pull request #1374 from square/wenli/visualizer-uds
Stream emulator data into workflow visualizer app
2 parents f1c2dca + 3a93534 commit b6e70a0

File tree

10 files changed

+399
-102
lines changed

10 files changed

+399
-102
lines changed

workflow-trace-viewer/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@ It can be run via Gradle using:
1010
./gradlew :workflow-trace-viewer:run
1111
```
1212

13+
By Default, the app will be in file parsing mode, where you are able to select a previously recorded workflow trace file for it to visualize the data.
14+
15+
By hitting the bottom switch, you are able to toggle to live stream mode, where data is directly pulled from the emulator into the visualizer. A connection can only happen once. If there needs to be rerecording of the trace, the emulator must first be restarted, and then the app must be restarted as well. This is due to the fact that any open socket will consume all render pass data, meaning there is nothing to read from the emulator.
16+
17+
It is ***important*** to run the emulator first before toggling to live mode.
18+
1319
### Terminology
1420

1521
**Trace**: A trace is a file — made up of frames — that contains the execution history of a Workflow. It includes information about render passes, how states have changed within workflows, and the specific props being passed through. The data collected to generate these should be in chronological order, and allows developers to step through the process easily.

workflow-trace-viewer/api/workflow-trace-viewer.api

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
public final class com/squareup/workflow1/traceviewer/AppKt {
2-
public static final fun App (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
3-
}
4-
51
public final class com/squareup/workflow1/traceviewer/ComposableSingletons$MainKt {
62
public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ComposableSingletons$MainKt;
73
public static field lambda-1 Lkotlin/jvm/functions/Function3;

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/App.kt

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.compose.runtime.LaunchedEffect
66
import androidx.compose.runtime.getValue
77
import androidx.compose.runtime.mutableFloatStateOf
88
import androidx.compose.runtime.mutableIntStateOf
9+
import androidx.compose.runtime.mutableStateListOf
910
import androidx.compose.runtime.mutableStateOf
1011
import androidx.compose.runtime.remember
1112
import androidx.compose.runtime.setValue
@@ -16,8 +17,9 @@ import androidx.compose.ui.geometry.Offset
1617
import com.squareup.workflow1.traceviewer.model.Node
1718
import com.squareup.workflow1.traceviewer.model.NodeUpdate
1819
import com.squareup.workflow1.traceviewer.ui.FrameSelectTab
19-
import com.squareup.workflow1.traceviewer.ui.RenderDiagram
2020
import com.squareup.workflow1.traceviewer.ui.RightInfoPanel
21+
import com.squareup.workflow1.traceviewer.ui.TraceModeToggleSwitch
22+
import com.squareup.workflow1.traceviewer.util.RenderTrace
2123
import com.squareup.workflow1.traceviewer.util.SandboxBackground
2224
import com.squareup.workflow1.traceviewer.util.UploadFile
2325
import io.github.vinceglb.filekit.PlatformFile
@@ -26,15 +28,18 @@ import io.github.vinceglb.filekit.PlatformFile
2628
* Main composable that provides the different layers of UI.
2729
*/
2830
@Composable
29-
public fun App(
31+
internal fun App(
3032
modifier: Modifier = Modifier
3133
) {
32-
var selectedTraceFile by remember { mutableStateOf<PlatformFile?>(null) }
3334
var selectedNode by remember { mutableStateOf<NodeUpdate?>(null) }
34-
var workflowFrames by remember { mutableStateOf<List<Node>>(emptyList()) }
35+
val workflowFrames = remember { mutableStateListOf<Node>() }
3536
var frameIndex by remember { mutableIntStateOf(0) }
3637
val sandboxState = remember { SandboxState() }
3738

39+
// Default to File mode, and can be toggled to be in Live mode.
40+
var traceMode by remember { mutableStateOf<TraceMode>(TraceMode.File(null)) }
41+
var selectedTraceFile by remember { mutableStateOf<PlatformFile?>(null) }
42+
3843
LaunchedEffect(sandboxState) {
3944
snapshotFlow { frameIndex }.collect {
4045
sandboxState.reset()
@@ -44,18 +49,29 @@ public fun App(
4449
Box(
4550
modifier = modifier
4651
) {
52+
fun resetStates() {
53+
selectedTraceFile = null
54+
selectedNode = null
55+
frameIndex = 0
56+
workflowFrames.clear()
57+
}
58+
4759
// Main content
48-
if (selectedTraceFile != null) {
49-
SandboxBackground(
50-
sandboxState = sandboxState,
51-
) {
52-
RenderDiagram(
53-
traceFile = selectedTraceFile!!,
60+
SandboxBackground(
61+
sandboxState = sandboxState,
62+
) {
63+
// if there is not a file selected and trace mode is live, then don't render anything.
64+
val readyForFileTrace = traceMode is TraceMode.File && selectedTraceFile != null
65+
val readyForLiveTrace = traceMode is TraceMode.Live
66+
if (readyForFileTrace || readyForLiveTrace) {
67+
RenderTrace(
68+
traceSource = traceMode,
5469
frameInd = frameIndex,
55-
onFileParse = { workflowFrames = it },
70+
onFileParse = { workflowFrames.addAll(it) },
5671
onNodeSelect = { node, prevNode ->
5772
selectedNode = NodeUpdate(node, prevNode)
58-
}
73+
},
74+
onNewFrame = { frameIndex += 1 }
5975
)
6076
}
6177
}
@@ -73,16 +89,37 @@ public fun App(
7389
.align(Alignment.TopEnd)
7490
)
7591

76-
// The states are reset when a new file is selected.
77-
UploadFile(
78-
resetOnFileSelect = {
79-
selectedTraceFile = it
80-
selectedNode = null
81-
frameIndex = 0
82-
workflowFrames = emptyList()
92+
TraceModeToggleSwitch(
93+
onToggle = {
94+
resetStates()
95+
traceMode = if (traceMode is TraceMode.Live) {
96+
frameIndex = 0
97+
TraceMode.File(null)
98+
} else {
99+
// TODO: TraceRecorder needs to be able to take in multiple clients if this is the case
100+
/*
101+
We set the frame to -1 here since we always increment it during Live mode as the list of
102+
frames get populated, so we avoid off by one when indexing into the frames.
103+
*/
104+
frameIndex = -1
105+
TraceMode.Live
106+
}
83107
},
84-
modifier = Modifier.align(Alignment.BottomStart)
108+
traceMode = traceMode,
109+
modifier = Modifier.align(Alignment.BottomCenter)
85110
)
111+
112+
// The states are reset when a new file is selected.
113+
if (traceMode is TraceMode.File) {
114+
UploadFile(
115+
resetOnFileSelect = {
116+
resetStates()
117+
selectedTraceFile = it
118+
traceMode = TraceMode.File(it)
119+
},
120+
modifier = Modifier.align(Alignment.BottomStart)
121+
)
122+
}
86123
}
87124
}
88125

@@ -92,6 +129,10 @@ internal class SandboxState {
92129

93130
fun reset() {
94131
offset = Offset.Zero
95-
scale = 1f
96132
}
97133
}
134+
135+
internal sealed interface TraceMode {
136+
data class File(val file: PlatformFile?) : TraceMode
137+
data object Live : TraceMode
138+
}

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/Main.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import androidx.compose.ui.window.singleWindowApplication
88
* Main entry point for the desktop application, see [README.md] for more details.
99
*/
1010
fun main() {
11-
singleWindowApplication(title = "Workflow Trace Viewer") {
11+
singleWindowApplication(title = "Workflow Trace Viewer", exitProcessOnExit = false) {
1212
App(Modifier.fillMaxSize())
1313
}
1414
}

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/FrameSelectTab.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
1010
import androidx.compose.material.Surface
1111
import androidx.compose.material.Text
1212
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.LaunchedEffect
1314
import androidx.compose.ui.Modifier
1415
import androidx.compose.ui.draw.clip
1516
import androidx.compose.ui.graphics.Color
@@ -31,7 +32,12 @@ internal fun FrameSelectTab(
3132
modifier: Modifier = Modifier
3233
) {
3334
val lazyListState = rememberLazyListState()
34-
35+
if (currentIndex >= 0) {
36+
LaunchedEffect(currentIndex) {
37+
lazyListState.animateScrollToItem(currentIndex)
38+
}
39+
}
40+
3541
Surface(
3642
modifier = modifier,
3743
color = Color.White,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.squareup.workflow1.traceviewer.ui
2+
3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.padding
5+
import androidx.compose.material.Switch
6+
import androidx.compose.material.SwitchDefaults
7+
import androidx.compose.material.Text
8+
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Alignment
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.graphics.Color
12+
import androidx.compose.ui.text.font.FontStyle
13+
import androidx.compose.ui.unit.dp
14+
import androidx.compose.ui.unit.sp
15+
import com.squareup.workflow1.traceviewer.TraceMode
16+
17+
@Composable
18+
internal fun TraceModeToggleSwitch(
19+
onToggle: () -> Unit,
20+
traceMode: TraceMode,
21+
modifier: Modifier = Modifier
22+
) {
23+
Column(
24+
modifier = modifier.padding(16.dp),
25+
horizontalAlignment = Alignment.CenterHorizontally
26+
) {
27+
Switch(
28+
checked = traceMode is TraceMode.Live,
29+
onCheckedChange = {
30+
onToggle()
31+
},
32+
colors = SwitchDefaults.colors(
33+
checkedThumbColor = Color.Black,
34+
checkedTrackColor = Color.Black,
35+
)
36+
)
37+
38+
Text(
39+
text = if (traceMode is TraceMode.Live) {
40+
"Live Mode"
41+
} else {
42+
"File Mode"
43+
},
44+
fontSize = 12.sp,
45+
fontStyle = FontStyle.Italic
46+
)
47+
}
48+
}

workflow-trace-viewer/src/jvmMain/kotlin/com/squareup/workflow1/traceviewer/ui/WorkflowTree.kt

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@ import androidx.compose.foundation.layout.padding
1111
import androidx.compose.material.Text
1212
import androidx.compose.runtime.Composable
1313
import androidx.compose.runtime.LaunchedEffect
14-
import androidx.compose.runtime.getValue
15-
import androidx.compose.runtime.mutableStateListOf
16-
import androidx.compose.runtime.mutableStateMapOf
17-
import androidx.compose.runtime.mutableStateOf
18-
import androidx.compose.runtime.remember
19-
import androidx.compose.runtime.setValue
2014
import androidx.compose.ui.Alignment
2115
import androidx.compose.ui.ExperimentalComposeUiApi
2216
import androidx.compose.ui.Modifier
@@ -27,62 +21,6 @@ import androidx.compose.ui.input.pointer.isSecondaryPressed
2721
import androidx.compose.ui.input.pointer.onPointerEvent
2822
import androidx.compose.ui.unit.dp
2923
import com.squareup.workflow1.traceviewer.model.Node
30-
import com.squareup.workflow1.traceviewer.util.ParseResult
31-
import com.squareup.workflow1.traceviewer.util.parseTrace
32-
import io.github.vinceglb.filekit.PlatformFile
33-
34-
/**
35-
* Access point for drawing the main content of the app. It will load the trace for given files and
36-
* tabs. This will also all errors related to errors parsing a given trace JSON file.
37-
*/
38-
@Composable
39-
internal fun RenderDiagram(
40-
traceFile: PlatformFile,
41-
frameInd: Int,
42-
onFileParse: (List<Node>) -> Unit,
43-
onNodeSelect: (Node, Node?) -> Unit,
44-
modifier: Modifier = Modifier
45-
) {
46-
var isLoading by remember(traceFile) { mutableStateOf(true) }
47-
var error by remember(traceFile) { mutableStateOf<Throwable?>(null) }
48-
var frames = remember { mutableStateListOf<Node>() }
49-
var fullTree = remember { mutableStateListOf<Node>() }
50-
var affectedNodes = remember { mutableStateListOf<Set<Node>>() }
51-
52-
LaunchedEffect(traceFile) {
53-
val parseResult = parseTrace(traceFile)
54-
55-
when (parseResult) {
56-
is ParseResult.Failure -> {
57-
error = parseResult.error
58-
}
59-
is ParseResult.Success -> {
60-
val parsedFrames = parseResult.trace ?: emptyList()
61-
frames.addAll(parsedFrames)
62-
fullTree.addAll(parseResult.trees)
63-
affectedNodes.addAll(parseResult.affectedNodes)
64-
onFileParse(parsedFrames)
65-
isLoading = false
66-
}
67-
}
68-
}
69-
70-
if (error != null) {
71-
Text("Error parsing file: ${error?.message}")
72-
return
73-
}
74-
75-
if (!isLoading) {
76-
val previousFrame = if (frameInd > 0) fullTree[frameInd - 1] else null
77-
DrawTree(
78-
node = fullTree[frameInd],
79-
previousNode = previousFrame,
80-
affectedNodes = affectedNodes[frameInd],
81-
expandedNodes = remember(frameInd) { mutableStateMapOf() },
82-
onNodeSelect = onNodeSelect,
83-
)
84-
}
85-
}
8624

8725
/**
8826
* Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree
@@ -92,7 +30,7 @@ internal fun RenderDiagram(
9230
* closed from user clicks.
9331
*/
9432
@Composable
95-
private fun DrawTree(
33+
internal fun DrawTree(
9634
node: Node,
9735
previousNode: Node?,
9836
affectedNodes: Set<Node>,

0 commit comments

Comments
 (0)