Skip to content

WIP improve visualizer usability #1382

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

Draft
wants to merge 34 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e3b81f3
Complete reset when changing trace files
wenli-cai Jul 21, 2025
cbcea20
Fix naming for node current and past state
wenli-cai Jul 22, 2025
9c1fc6a
Enumerate the specific changes for each node and color them accordingly
wenli-cai Jul 22, 2025
25d04a4
Move away from using reflection for accessing node fields
wenli-cai Jul 22, 2025
1ce9943
Change node data being visualized
wenli-cai Jul 22, 2025
b5d3b16
Remove unnecessary frame tracking in main composable
wenli-cai Jul 22, 2025
6dddf94
Add legend for the colors used in the frame
wenli-cai Jul 22, 2025
6d2d31b
Group unaffected and affected children separately
wenli-cai Jul 24, 2025
e71a870
Separate composable components and change workflow UI pattern
wenli-cai Jul 24, 2025
18c3b6f
Don't apply simple/nested pattern for unaffected children group
wenli-cai Jul 24, 2025
5646d10
Add file dump functionality from live tracing mode
wenli-cai Jul 25, 2025
18f82cd
Fix NPE when enabling live view without a connected device
japplin Jul 24, 2025
07ab5fd
Enable compose hot reload
japplin Jul 25, 2025
58c58a9
Change sample/tutorial module's agp version
wenli-cai Jul 25, 2025
2aaa120
WIP text diff
wenli-cai Jul 25, 2025
5ec8a18
Add text-diff functionality
wenli-cai Jul 26, 2025
82eae8b
Fix compose violations
wenli-cai Jul 26, 2025
1633f4c
Merge branch 'main' into wenli/improve-visualizer
wenli-cai Jul 26, 2025
1e1da90
Apply changes from dependencyGuardBaseline --refresh-dependencies
wenli-cai Jul 26, 2025
8fbeab4
Apply changes from artifactsDump
workflow-pr-fixer[bot] Jul 26, 2025
a50f9a5
Fix merge bugs
wenli-cai Jul 26, 2025
c4fe05e
Allow device selection
wenli-cai Jul 26, 2025
6687f04
Manual api dump
wenli-cai Jul 26, 2025
cdfbc82
Remove compose hot reload changes
wenli-cai Jul 28, 2025
5d3e3f4
Apply changes from dependencyGuardBaseline --refresh-dependencies
wenli-cai Jul 28, 2025
fdf2253
Apply changes from apiDump
workflow-pr-fixer[bot] Jul 28, 2025
5280f84
Apply changes from artifactsDump
workflow-pr-fixer[bot] Jul 28, 2025
a2c98ae
Clean up
wenli-cai Jul 28, 2025
325b239
Add search box for available nodes in the specified frame
wenli-cai Jul 29, 2025
688688e
Refactor directories
wenli-cai Jul 29, 2025
24fae61
Improve error handling
wenli-cai Jul 29, 2025
cc40120
Make app window snap to the specific node when it is being searched.
wenli-cai Jul 30, 2025
a9a44e9
Fix zooming for sandbox
wenli-cai Jul 30, 2025
4f9f92b
Replace color legend with tooltip display on node hover
wenli-cai Aug 1, 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
4 changes: 2 additions & 2 deletions dependencies/classpath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.21
org.jetbrains.kotlin:kotlin-gradle-plugins-bom:2.0.21
org.jetbrains.kotlin:kotlin-klib-commonizer-api:2.0.21
org.jetbrains.kotlin:kotlin-native-utils:2.0.21
org.jetbrains.kotlin:kotlin-reflect:2.0.20
org.jetbrains.kotlin:kotlin-reflect:2.0.21
org.jetbrains.kotlin:kotlin-serialization:2.0.21
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.0.21
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.0.21
org.jetbrains.kotlin:kotlin-stdlib:2.0.20
org.jetbrains.kotlin:kotlin-stdlib:2.0.21
org.jetbrains.kotlin:kotlin-tooling-core:2.0.21
org.jetbrains.kotlin:kotlin-util-io:2.0.21
org.jetbrains.kotlin:kotlin-util-klib:2.0.21
Expand Down
1 change: 0 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ turbine = "1.0.0"
vanniktech-publish = "0.32.0"

[plugins]

kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
16 changes: 8 additions & 8 deletions workflow-core/dependencies/jvmMainRuntimeClasspath.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
com.squareup.okio:okio-jvm:3.3.0
com.squareup.okio:okio:3.3.0
org.jetbrains.kotlin:kotlin-bom:2.1.21
org.jetbrains.kotlin:kotlin-stdlib-common:2.1.21
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.21
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.21
org.jetbrains.kotlin:kotlin-stdlib:2.1.21
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
org.jetbrains.kotlin:kotlin-bom:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.2.0
org.jetbrains.kotlin:kotlin-stdlib:2.2.0
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0
org.jetbrains:annotations:23.0.0
31 changes: 21 additions & 10 deletions workflow-runtime/dependencies/jvmMainRuntimeClasspath.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
com.squareup.okio:okio-jvm:3.3.0
com.squareup.okio:okio:3.3.0
org.jetbrains.kotlin:kotlin-bom:2.1.21
org.jetbrains.kotlin:kotlin-stdlib-common:2.1.21
org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.21
org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.21
org.jetbrains.kotlin:kotlin-stdlib:2.1.21
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.7.3
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3
com.fasterxml.jackson.core:jackson-annotations:2.12.7
com.fasterxml.jackson.core:jackson-core:2.12.7
com.fasterxml.jackson.core:jackson-databind:2.12.7.1
com.fasterxml.jackson.module:jackson-module-kotlin:2.12.7
com.fasterxml.jackson:jackson-bom:2.12.7
org.freemarker:freemarker:2.3.32
org.jetbrains.dokka:all-modules-page-plugin:2.0.0
org.jetbrains.dokka:analysis-markdown:2.0.0
org.jetbrains.dokka:dokka-base:2.0.0
org.jetbrains.dokka:templating-plugin:2.0.0
org.jetbrains.kotlin:kotlin-bom:2.2.0
org.jetbrains.kotlin:kotlin-reflect:2.2.0
org.jetbrains.kotlin:kotlin-stdlib-common:2.2.0
org.jetbrains.kotlin:kotlin-stdlib:2.2.0
org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.9.0
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.9.0
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0
org.jetbrains.kotlinx:kotlinx-html-jvm:0.9.1
org.jetbrains:annotations:23.0.0
org.jetbrains:markdown-jvm:0.7.3
org.jetbrains:markdown:0.7.3
org.jsoup:jsoup:1.16.1
32 changes: 30 additions & 2 deletions workflow-trace-viewer/api/workflow-trace-viewer.api
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,27 @@ public final class com/squareup/workflow1/traceviewer/MainKt {
public static synthetic fun main ([Ljava/lang/String;)V
}

public final class com/squareup/workflow1/traceviewer/model/NodeState : java/lang/Enum {
public static final field CHILDREN_CHANGED Lcom/squareup/workflow1/traceviewer/model/NodeState;
public static final field NEW Lcom/squareup/workflow1/traceviewer/model/NodeState;
public static final field PROPS_CHANGED Lcom/squareup/workflow1/traceviewer/model/NodeState;
public static final field STATE_CHANGED Lcom/squareup/workflow1/traceviewer/model/NodeState;
public static final field UNCHANGED Lcom/squareup/workflow1/traceviewer/model/NodeState;
public final fun getColor-0d7_KjU ()J
public static fun getEntries ()Lkotlin/enums/EnumEntries;
public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/traceviewer/model/NodeState;
public static fun values ()[Lcom/squareup/workflow1/traceviewer/model/NodeState;
}

public final class com/squareup/workflow1/traceviewer/ui/ColorLegendKt {
public static final fun ColorLegend (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V
}

public final class com/squareup/workflow1/traceviewer/ui/ComposableSingletons$WorkflowInfoPanelKt {
public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ui/ComposableSingletons$WorkflowInfoPanelKt;
public static field lambda-1 Lkotlin/jvm/functions/Function3;
public static field lambda-2 Lkotlin/jvm/functions/Function3;
public fun <init> ()V
public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
public final fun getLambda-2$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
}

public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$UploadFileKt {
Expand All @@ -26,6 +40,20 @@ public final class com/squareup/workflow1/traceviewer/util/ComposableSingletons$
public final fun getLambda-1$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
}

public final class com/squareup/workflow1/traceviewer/util/DiffStyles {
public static final field $stable I
public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/util/DiffStyles;
public final fun buildStringWithStyle (Landroidx/compose/ui/text/SpanStyle;Ljava/lang/String;Landroidx/compose/ui/text/AnnotatedString$Builder;)V
public final fun getDELETE ()Landroidx/compose/ui/text/SpanStyle;
public final fun getINSERT ()Landroidx/compose/ui/text/SpanStyle;
public final fun getNO_CHANGE ()Landroidx/compose/ui/text/SpanStyle;
public final fun getUNCHANGED ()Landroidx/compose/ui/text/SpanStyle;
}

public final class com/squareup/workflow1/traceviewer/util/DiffUtilsKt {
public static final fun computeAnnotatedDiff (Ljava/lang/String;Ljava/lang/String;)Landroidx/compose/ui/text/AnnotatedString;
}

public final class com/squareup/workflow1/traceviewer/util/JsonParserKt {
public static final field ROOT_ID Ljava/lang/String;
}
Expand Down
2 changes: 2 additions & 0 deletions workflow-trace-viewer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ kotlin {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
Expand All @@ -25,6 +26,7 @@ kotlin {
implementation(compose.materialIconsExtended)
implementation(libs.squareup.moshi.kotlin)
implementation(libs.filekit.dialogs.compose)
implementation(libs.java.diff.utils)
}
}
jvmTest {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,34 @@
package com.squareup.workflow1.traceviewer

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
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.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.IntSize
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.RightInfoPanel
import com.squareup.workflow1.traceviewer.ui.TraceModeToggleSwitch
import com.squareup.workflow1.traceviewer.util.RenderTrace
import com.squareup.workflow1.traceviewer.ui.control.DisplayDevices
import com.squareup.workflow1.traceviewer.ui.control.FileDump
import com.squareup.workflow1.traceviewer.ui.control.FrameSelectTab
import com.squareup.workflow1.traceviewer.ui.control.SearchBox
import com.squareup.workflow1.traceviewer.ui.control.TraceModeToggleSwitch
import com.squareup.workflow1.traceviewer.ui.control.UploadFile
import com.squareup.workflow1.traceviewer.util.SandboxBackground
import com.squareup.workflow1.traceviewer.util.UploadFile
import com.squareup.workflow1.traceviewer.util.parser.RenderTrace
import io.github.vinceglb.filekit.PlatformFile

/**
Expand All @@ -31,14 +38,20 @@ import io.github.vinceglb.filekit.PlatformFile
internal fun App(
modifier: Modifier = Modifier
) {
var appWindowSize by remember { mutableStateOf(IntSize(0, 0)) }
var selectedNode by remember { mutableStateOf<NodeUpdate?>(null) }
val workflowFrames = remember { mutableStateListOf<Node>() }
var frameSize by remember { mutableIntStateOf(0) }
var rawRenderPass by remember { mutableStateOf("") }
var frameIndex by remember { mutableIntStateOf(0) }
val sandboxState = remember { SandboxState() }
val nodeLocations = remember { mutableListOf<SnapshotStateMap<Node, Offset>>() }

// Default to File mode, and can be toggled to be in Live mode.
var active by remember { mutableStateOf(false) }
var traceMode by remember { mutableStateOf<TraceMode>(TraceMode.File(null)) }
var selectedTraceFile by remember { mutableStateOf<PlatformFile?>(null) }
// frameIndex is set to -1 when app is in Live Mode, so we increment it by one to avoid off-by-one errors
val frameInd = if (traceMode is TraceMode.Live) frameIndex + 1 else frameIndex

LaunchedEffect(sandboxState) {
snapshotFlow { frameIndex }.collect {
Expand All @@ -47,47 +60,74 @@ internal fun App(
}

Box(
modifier = modifier
modifier = modifier.onGloballyPositioned {
appWindowSize = it.size
}
) {
fun resetStates() {
selectedTraceFile = null
selectedNode = null
frameIndex = 0
workflowFrames.clear()
frameSize = 0
active = false
nodeLocations.clear()
}

// Main content
SandboxBackground(
appWindowSize = appWindowSize,
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
val readyForFileTrace = TraceMode.validateFileMode(traceMode)
val readyForLiveTrace = TraceMode.validateLiveMode(traceMode)

if (readyForFileTrace || readyForLiveTrace) {
active = true
RenderTrace(
traceSource = traceMode,
frameInd = frameIndex,
onFileParse = { workflowFrames.addAll(it) },
onNodeSelect = { node, prevNode ->
selectedNode = NodeUpdate(node, prevNode)
},
onNewFrame = { frameIndex += 1 }
onFileParse = { frameSize += it },
onNodeSelect = { selectedNode = it },
onNewFrame = { frameIndex += 1 },
onNewData = { rawRenderPass += "$it," },
storeNodeLocation = { node, loc -> nodeLocations[frameInd] += (node to loc) }
)
}
}

FrameSelectTab(
frames = workflowFrames,
currentIndex = frameIndex,
onIndexChange = { frameIndex = it },
Column(
modifier = Modifier.align(Alignment.TopCenter)
)
) {
if (active) {
FrameSelectTab(
size = frameSize,
currentIndex = frameIndex,
onIndexChange = { frameIndex = it },
)

RightInfoPanel(
selectedNode = selectedNode,
modifier = Modifier
.align(Alignment.TopEnd)
)
// Since we can jump from frame to frame, we fill in the map during each recomposition
if (nodeLocations.getOrNull(frameInd) == null) {
// frameSize has not been updated yet, so on the first frame, frameSize = nodeLocations.size = 0,
// and it will append a new map
while (nodeLocations.size <= frameSize) {
nodeLocations.add(mutableStateMapOf())
}
}

SearchBox(
nodes = nodeLocations[frameInd].keys.toList(),
onSearch = { name ->
sandboxState.scale = 1f
val node = nodeLocations[frameInd].keys.firstOrNull { it.name == name }
val newX = sandboxState.offset.x - nodeLocations[frameInd][node]!!.x + appWindowSize.width / 2
val newY = sandboxState.offset.y - nodeLocations[frameInd][node]!!.y + appWindowSize.height / 2
sandboxState.offset = Offset(x = newX, y = newY)
},
modifier = Modifier.align(Alignment.CenterHorizontally)
)
}
}

TraceModeToggleSwitch(
onToggle = {
Expand All @@ -96,13 +136,12 @@ internal fun App(
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
TraceMode.Live()
}
},
traceMode = traceMode,
Expand All @@ -120,6 +159,29 @@ internal fun App(
modifier = Modifier.align(Alignment.BottomStart)
)
}

if (traceMode is TraceMode.Live) {
if ((traceMode as TraceMode.Live).device == null) {
DisplayDevices(
onDeviceSelect = { selectedDevice ->
traceMode = TraceMode.Live(selectedDevice)
},
devices = listDevices(),
modifier = Modifier.align(Alignment.Center)
)
}

FileDump(
trace = rawRenderPass,
modifier = Modifier.align(Alignment.BottomStart)
)
}

RightInfoPanel(
selectedNode = selectedNode,
modifier = Modifier
.align(Alignment.TopEnd)
)
}
}

Expand All @@ -134,5 +196,25 @@ internal class SandboxState {

internal sealed interface TraceMode {
data class File(val file: PlatformFile?) : TraceMode
data object Live : TraceMode
data class Live(val device: String? = null) : TraceMode

companion object {
fun validateLiveMode(traceMode: TraceMode): Boolean {
return traceMode is Live && traceMode.device != null
}

fun validateFileMode(traceMode: TraceMode): Boolean {
return traceMode is File && traceMode.file != null
}
}
}

/**
* Allows users to select from multiple devices that are currently running.
*/
internal fun listDevices(): List<String> {
val process = ProcessBuilder("adb", "devices", "-l").start()
process.waitFor()
// We drop the header "List of devices attached"
return process.inputStream.bufferedReader().readLines().drop(1).dropLast(1)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,21 @@ internal data class Node(
override fun hashCode(): Int {
return id.hashCode()
}

companion object {
fun getNodeFields(): List<String> {
return listOf("Props", "State", "Rendering")
}

fun getNodeData(node: Node, field: String): String {
return when (field.lowercase()) {
"props" -> node.props
"state" -> node.state
"rendering" -> node.rendering
else -> throw IllegalArgumentException("Unknown field: $field")
}
}
}
}

internal fun Node.addChild(child: Node): Node {
Expand Down
Loading