Skip to content

Stream emulator data into workflow visualizer app #1374

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 24 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e9579fc
Log data retrieved from socket
wenli-cai Jul 16, 2025
a121bf4
Change moshi adapter to use generics
wenli-cai Jul 17, 2025
ccdd35c
Create new toggle to switch between tracing modes
wenli-cai Jul 17, 2025
f678ab7
Fix moshi adapter
wenli-cai Jul 17, 2025
c63c43a
Extract socket setup and reading logic.
wenli-cai Jul 17, 2025
d326385
WIP
wenli-cai Jul 17, 2025
6e71ae9
Consolidate logic to take in TraceMode rather than specific params
wenli-cai Jul 17, 2025
f5fdb6e
Apply changes from apiDump
wenli-cai Jul 18, 2025
8c1b2dd
Stream data from emulator directly into visualizer
wenli-cai Jul 18, 2025
9d50d60
Return to using generics for Moshi adapter
wenli-cai Jul 18, 2025
70c9b45
Allow for auto scrolling during Live Mode
wenli-cai Jul 18, 2025
e233921
Fix SocketException
wenli-cai Jul 18, 2025
f414e75
Fix lint violations
wenli-cai Jul 18, 2025
fff11db
Consolidate live and trace mode render on result logic
wenli-cai Jul 18, 2025
cd8b4ab
Clean up compose violations
wenli-cai Jul 18, 2025
a5d20ac
Change function visibility for test
wenli-cai Jul 18, 2025
b8c62ab
Merge branch 'main' into wenli/visualizer-uds
wenli-cai Jul 18, 2025
febd6d5
Fix merge bug
wenli-cai Jul 18, 2025
764f991
Fix PR comments
wenli-cai Jul 23, 2025
a0f22da
Extract parsing logic from tree rendering logic
wenli-cai Jul 23, 2025
2ae9051
Refactor SocketClient
wenli-cai Jul 24, 2025
a03fcc9
Apply changes from apiDump
wenli-cai Jul 24, 2025
be3fbd1
More socket refactoring
wenli-cai Jul 24, 2025
9319e1d
Apply changes from apiDump
wenli-cai Jul 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions workflow-trace-viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ It can be run via Gradle using:
./gradlew :workflow-trace-viewer:run
```

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.

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.

It is ***important*** to run the emulator first before toggling to live mode.

### Terminology

**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.
Expand Down
4 changes: 0 additions & 4 deletions workflow-trace-viewer/api/workflow-trace-viewer.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
public final class com/squareup/workflow1/traceviewer/AppKt {
public static final fun App (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}

public final class com/squareup/workflow1/traceviewer/ComposableSingletons$MainKt {
public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ComposableSingletons$MainKt;
public static field lambda-1 Lkotlin/jvm/functions/Function3;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
Expand All @@ -16,8 +17,9 @@ import androidx.compose.ui.geometry.Offset
import com.squareup.workflow1.traceviewer.model.Node
import com.squareup.workflow1.traceviewer.model.NodeUpdate
import com.squareup.workflow1.traceviewer.ui.FrameSelectTab
import com.squareup.workflow1.traceviewer.ui.RenderDiagram
import com.squareup.workflow1.traceviewer.ui.RightInfoPanel
import com.squareup.workflow1.traceviewer.ui.TraceModeToggleSwitch
import com.squareup.workflow1.traceviewer.util.RenderTrace
import com.squareup.workflow1.traceviewer.util.SandboxBackground
import com.squareup.workflow1.traceviewer.util.UploadFile
import io.github.vinceglb.filekit.PlatformFile
Expand All @@ -26,15 +28,18 @@ import io.github.vinceglb.filekit.PlatformFile
* Main composable that provides the different layers of UI.
*/
@Composable
public fun App(
internal fun App(
modifier: Modifier = Modifier
) {
var selectedTraceFile by remember { mutableStateOf<PlatformFile?>(null) }
var selectedNode by remember { mutableStateOf<NodeUpdate?>(null) }
var workflowFrames by remember { mutableStateOf<List<Node>>(emptyList()) }
val workflowFrames = remember { mutableStateListOf<Node>() }
var frameIndex by remember { mutableIntStateOf(0) }
val sandboxState = remember { SandboxState() }

// Default to File mode, and can be toggled to be in Live mode.
var traceMode by remember { mutableStateOf<TraceMode>(TraceMode.File(null)) }
var selectedTraceFile by remember { mutableStateOf<PlatformFile?>(null) }

LaunchedEffect(sandboxState) {
snapshotFlow { frameIndex }.collect {
sandboxState.reset()
Expand All @@ -44,18 +49,29 @@ public fun App(
Box(
modifier = modifier
) {
fun resetStates() {
selectedTraceFile = null
selectedNode = null
frameIndex = 0
workflowFrames.clear()
}

// Main content
if (selectedTraceFile != null) {
SandboxBackground(
sandboxState = sandboxState,
) {
RenderDiagram(
traceFile = selectedTraceFile!!,
SandboxBackground(
sandboxState = sandboxState,
) {
// if there is not a file selected and trace mode is live, then don't render anything.
val readyForFileTrace = traceMode is TraceMode.File && selectedTraceFile != null
val readyForLiveTrace = traceMode is TraceMode.Live
if (readyForFileTrace || readyForLiveTrace) {
RenderTrace(
traceSource = traceMode,
frameInd = frameIndex,
onFileParse = { workflowFrames = it },
onFileParse = { workflowFrames.addAll(it) },
onNodeSelect = { node, prevNode ->
selectedNode = NodeUpdate(node, prevNode)
}
},
onNewFrame = { frameIndex += 1 }
)
}
}
Expand All @@ -73,16 +89,37 @@ public fun App(
.align(Alignment.TopEnd)
)

// The states are reset when a new file is selected.
UploadFile(
resetOnFileSelect = {
selectedTraceFile = it
selectedNode = null
frameIndex = 0
workflowFrames = emptyList()
TraceModeToggleSwitch(
onToggle = {
resetStates()
traceMode = if (traceMode is TraceMode.Live) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, consider calling the traceMode instance under evaluation as "currentTraceMode"

if(currentTraceMode is TraceMode.Live {

} else {

}

IMO, it's a bit clearer that the left side is the new state and the right side is existing state.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed

frameIndex = 0
TraceMode.File(null)
} else {
// TODO: TraceRecorder needs to be able to take in multiple clients if this is the case
/*
We set the frame to -1 here since we always increment it during Live mode as the list of
frames get populated, so we avoid off by one when indexing into the frames.
*/
frameIndex = -1
TraceMode.Live
}
},
modifier = Modifier.align(Alignment.BottomStart)
traceMode = traceMode,
modifier = Modifier.align(Alignment.BottomCenter)
)

// The states are reset when a new file is selected.
if (traceMode is TraceMode.File) {
UploadFile(
resetOnFileSelect = {
resetStates()
selectedTraceFile = it
traceMode = TraceMode.File(it)
},
modifier = Modifier.align(Alignment.BottomStart)
)
}
}
}

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

fun reset() {
offset = Offset.Zero
scale = 1f
}
}

internal sealed interface TraceMode {
data class File(val file: PlatformFile?) : TraceMode
data object Live : TraceMode
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import androidx.compose.ui.window.singleWindowApplication
* Main entry point for the desktop application, see [README.md] for more details.
*/
fun main() {
singleWindowApplication(title = "Workflow Trace Viewer") {
singleWindowApplication(title = "Workflow Trace Viewer", exitProcessOnExit = false) {
App(Modifier.fillMaxSize())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
Expand All @@ -31,7 +32,12 @@ internal fun FrameSelectTab(
modifier: Modifier = Modifier
) {
val lazyListState = rememberLazyListState()

if (currentIndex >= 0) {
LaunchedEffect(currentIndex) {
lazyListState.animateScrollToItem(currentIndex)
}
}

Surface(
modifier = modifier,
color = Color.White,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.squareup.workflow1.traceviewer.ui

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.squareup.workflow1.traceviewer.TraceMode

@Composable
internal fun TraceModeToggleSwitch(
onToggle: () -> Unit,
traceMode: TraceMode,
modifier: Modifier = Modifier
) {
// File mode is unchecked by default, and live mode is checked.
var checked by remember {
mutableStateOf(traceMode is TraceMode.Live)
}

Column(
modifier = modifier.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Switch(
checked = checked,
onCheckedChange = {
checked = it
onToggle()
},
colors = SwitchDefaults.colors(
checkedThumbColor = Color.Black,
checkedTrackColor = Color.Black,
)
)

Text(
text = if (traceMode is TraceMode.Live) {
"Live Mode"
} else {
"File Mode"
},
fontSize = 12.sp,
fontStyle = FontStyle.Italic
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
Expand All @@ -27,62 +21,6 @@ import androidx.compose.ui.input.pointer.isSecondaryPressed
import androidx.compose.ui.input.pointer.onPointerEvent
import androidx.compose.ui.unit.dp
import com.squareup.workflow1.traceviewer.model.Node
import com.squareup.workflow1.traceviewer.util.ParseResult
import com.squareup.workflow1.traceviewer.util.parseTrace
import io.github.vinceglb.filekit.PlatformFile

/**
* Access point for drawing the main content of the app. It will load the trace for given files and
* tabs. This will also all errors related to errors parsing a given trace JSON file.
*/
@Composable
internal fun RenderDiagram(
traceFile: PlatformFile,
frameInd: Int,
onFileParse: (List<Node>) -> Unit,
onNodeSelect: (Node, Node?) -> Unit,
modifier: Modifier = Modifier
) {
var isLoading by remember(traceFile) { mutableStateOf(true) }
var error by remember(traceFile) { mutableStateOf<Throwable?>(null) }
var frames = remember { mutableStateListOf<Node>() }
var fullTree = remember { mutableStateListOf<Node>() }
var affectedNodes = remember { mutableStateListOf<Set<Node>>() }

LaunchedEffect(traceFile) {
val parseResult = parseTrace(traceFile)

when (parseResult) {
is ParseResult.Failure -> {
error = parseResult.error
}
is ParseResult.Success -> {
val parsedFrames = parseResult.trace ?: emptyList()
frames.addAll(parsedFrames)
fullTree.addAll(parseResult.trees)
affectedNodes.addAll(parseResult.affectedNodes)
onFileParse(parsedFrames)
isLoading = false
}
}
}

if (error != null) {
Text("Error parsing file: ${error?.message}")
return
}

if (!isLoading) {
val previousFrame = if (frameInd > 0) fullTree[frameInd - 1] else null
DrawTree(
node = fullTree[frameInd],
previousNode = previousFrame,
affectedNodes = affectedNodes[frameInd],
expandedNodes = remember(frameInd) { mutableStateMapOf() },
onNodeSelect = onNodeSelect,
)
}
}

/**
* Since the workflow nodes present a tree structure, we utilize a recursive function to draw the tree
Expand All @@ -92,7 +30,7 @@ internal fun RenderDiagram(
* closed from user clicks.
*/
@Composable
private fun DrawTree(
internal fun DrawTree(
node: Node,
previousNode: Node?,
affectedNodes: Set<Node>,
Expand Down
Loading
Loading