Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 5 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ squareup-retrofit = "2.9.0"
squareup-seismic = "1.0.3"
squareup-workflow = "1.0.0"

skiko = "0.9.4"
telephoto = "0.16.0"
timber = "5.0.1"
truth = "1.4.4"
turbine = "1.0.0"
Expand Down Expand Up @@ -203,7 +205,7 @@ google-ksp = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin
hamcrest = "org.hamcrest:hamcrest-core:2.2"

java-diff-utils = { module = "io.github.java-diff-utils:java-diff-utils", version.ref = "java-diff-utils" }

telephoto = { module = "me.saket.telephoto:zoomable", version.ref = "telephoto" }
Copy link
Contributor

Choose a reason for hiding this comment

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

Was this usage via @saket 's suggestion? :). If not, he should know!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, I was just struggling with the zooming code for hours and then remembered his library and gave it a shot. There is still room to improve our implementation by replacing the custom panning code with the zoomable library as well, I briefly tried doing that but it wasn't panning.... out, so I left the hybrid approach for now. Would be great if you were interested in contributed that improvement @saket.

Copy link
Collaborator

Choose a reason for hiding this comment

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

yay, love seeing more usage inside our codebase! 🎉

I briefly tried doing that but it wasn't panning

that might be because compose multiplatform has pretty limited support for zoom events on the JVM :/ last time I checked, I wasn't seeing any trackpad or mouse scroll events. I'll double-check this today. could you share instructions for opening the trace viewer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

If you have the workflow kotlin repo cloned you can launch the trace viewer using ./gradlew :workflow-trace-viewer:run. I'm not sure if we have any sample traces in open source but you can download them from any failed test in ci I'll dm you a link.

Copy link
Collaborator

Choose a reason for hiding this comment

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

taking a look at this right now!

Copy link
Collaborator

Choose a reason for hiding this comment

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

done! #1421

jetbrains-annotations = "org.jetbrains:annotations:24.0.1"

junit = { module = "junit:junit", version.ref = "jUnit" }
Expand Down Expand Up @@ -251,6 +253,8 @@ robolectric-annotations = { module = "org.robolectric:annotations", version.ref
rxjava2-rxandroid = { module = "io.reactivex.rxjava2:rxandroid", version.ref = "rxjava2-android" }
rxjava2-rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava2-core" }

skiko = { module = "org.jetbrains.skiko:skiko-awt-runtime-macos-arm64", version.ref = "skiko" }

squareup-curtains = { module = "com.squareup.curtains:curtains", version.ref = "squareup-curtains" }

squareup-cycler = { module = "com.squareup.cycler:cycler", version.ref = "squareup-cycler" }
Expand Down
39 changes: 37 additions & 2 deletions workflow-trace-viewer/api/workflow-trace-viewer.api
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
public final class com/squareup/workflow1/traceviewer/ComposableSingletons$MainKt {
public static final field INSTANCE Lcom/squareup/workflow1/traceviewer/ComposableSingletons$MainKt;
public fun <init> ()V
public final fun getLambda$468449326$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
public final fun getLambda$-793818668$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
}

public final class com/squareup/workflow1/traceviewer/MainKt {
public static final fun main ()V
public static synthetic fun main ([Ljava/lang/String;)V
}

public abstract interface class com/squareup/workflow1/traceviewer/TraceWindow {
}

public final class com/squareup/workflow1/traceviewer/TraceWindow$DeviceWindow : com/squareup/workflow1/traceviewer/TraceWindow {
public static final field $stable I
public fun <init> (Ljava/lang/String;)V
public final fun component1 ()Ljava/lang/String;
public final fun copy (Ljava/lang/String;)Lcom/squareup/workflow1/traceviewer/TraceWindow$DeviceWindow;
public static synthetic fun copy$default (Lcom/squareup/workflow1/traceviewer/TraceWindow$DeviceWindow;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/traceviewer/TraceWindow$DeviceWindow;
public fun equals (Ljava/lang/Object;)Z
public final fun getDevice ()Ljava/lang/String;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class com/squareup/workflow1/traceviewer/TraceWindow$FileWindow : com/squareup/workflow1/traceviewer/TraceWindow {
public static final field $stable I
public fun <init> (Lio/github/vinceglb/filekit/PlatformFile;)V
public final fun component1 ()Lio/github/vinceglb/filekit/PlatformFile;
public final fun copy (Lio/github/vinceglb/filekit/PlatformFile;)Lcom/squareup/workflow1/traceviewer/TraceWindow$FileWindow;
public static synthetic fun copy$default (Lcom/squareup/workflow1/traceviewer/TraceWindow$FileWindow;Lio/github/vinceglb/filekit/PlatformFile;ILjava/lang/Object;)Lcom/squareup/workflow1/traceviewer/TraceWindow$FileWindow;
public fun equals (Ljava/lang/Object;)Z
public final fun getFile ()Lio/github/vinceglb/filekit/PlatformFile;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

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

public final class com/squareup/workflow1/traceviewer/ui/control/ComposableSingletons$SearchBoxKt {
Expand All @@ -28,3 +55,11 @@ public final class com/squareup/workflow1/traceviewer/ui/control/ComposableSingl
public final fun getLambda$-1248702605$wf1_workflow_trace_viewer ()Lkotlin/jvm/functions/Function3;
}

public final class com/squareup/workflow1/traceviewer/ui/control/DisplayDevicesKt {
public static final fun getAdb ()Ljava/lang/String;
}

public final class com/squareup/workflow1/traceviewer/util/parser/TraceParserKt {
public static final fun Error (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)V
}

16 changes: 15 additions & 1 deletion workflow-trace-viewer/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("kotlin-multiplatform")
Expand Down Expand Up @@ -27,6 +29,8 @@ kotlin {
implementation(libs.squareup.moshi.kotlin)
implementation(libs.filekit.dialogs.compose)
implementation(libs.java.diff.utils)
implementation(libs.telephoto)
implementation(libs.skiko)
}
}
jvmTest {
Expand All @@ -50,15 +54,25 @@ compose {
includeAllModules = true
targetFormats(TargetFormat.Dmg)
packageName = "Workflow Trace Viewer"
packageVersion = "1.0.0"
packageVersion = (property("VERSION_NAME") as String).substringBefore("-SNAPSHOT")
macOS {
bundleID = "com.squareup.workflow1.traceviewer"
}
}

buildTypes.release.proguard {
isEnabled.set(false)
}
}
}
}

tasks.named<Test>("jvmTest") {
useJUnitPlatform()
}

tasks.withType<KotlinCompile>().configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ package com.squareup.workflow1.traceviewer

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf
Expand All @@ -25,12 +25,9 @@ import androidx.compose.ui.unit.dp
import com.squareup.workflow1.traceviewer.model.Node
import com.squareup.workflow1.traceviewer.model.NodeUpdate
import com.squareup.workflow1.traceviewer.ui.RightInfoPanel
import com.squareup.workflow1.traceviewer.ui.control.DisplayDevices
import com.squareup.workflow1.traceviewer.ui.control.FileDump
import com.squareup.workflow1.traceviewer.ui.control.FrameNavigator
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.parser.RenderTrace
import io.github.vinceglb.filekit.PlatformFile
Expand All @@ -39,21 +36,20 @@ import io.github.vinceglb.filekit.PlatformFile
* Main composable that provides the different layers of UI.
*/
@Composable
internal fun App(
modifier: Modifier = Modifier
internal fun TraceViewerWindow(
modifier: Modifier = Modifier,
traceMode: TraceMode,
) {
var appWindowSize by remember { mutableStateOf(IntSize(0, 0)) }
var selectedNode by remember { mutableStateOf<NodeUpdate?>(null) }
var frameSize by remember { mutableIntStateOf(0) }
var rawRenderPass by remember { mutableStateOf("") }
var frameIndex by remember { mutableIntStateOf(0) }
var frameIndex by remember { mutableIntStateOf(if (traceMode is TraceMode.Live) -1 else 0) }
val sandboxState = remember { SandboxState() }
val nodeLocations = remember { mutableStateListOf<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

Expand All @@ -68,15 +64,6 @@ internal fun App(
appWindowSize = it
}
) {
fun resetStates() {
selectedTraceFile = null
selectedNode = null
frameIndex = 0
frameSize = 0
rawRenderPass = ""
active = false
nodeLocations.clear()
}

// Main content
SandboxBackground(
Expand All @@ -101,12 +88,12 @@ internal fun App(
}
}

Column(
Row(
modifier = Modifier
.align(Alignment.TopCenter)
.padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.Top,
) {
if (active) {
// Frames that appear in composition may not happen sequentially, so when the current frame
Expand All @@ -124,7 +111,6 @@ internal fun App(
SearchBox(
nodes = frameNodeLocations.keys.toList(),
onSearch = { name ->
sandboxState.scale = 1f
val node = frameNodeLocations.keys.first { it.name == name }
val newX = (sandboxState.offset.x - frameNodeLocations.getValue(node).x
+ appWindowSize.width / 2)
Expand All @@ -141,63 +127,23 @@ internal fun App(
)
}
}

TraceModeToggleSwitch(
onToggle = {
resetStates()
traceMode = if (traceMode is TraceMode.Live) {
frameIndex = 0
TraceMode.File(null)
} else {
/*
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 = 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)
)
}

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

if (traceMode is TraceMode.Live) {
FileDump(
trace = rawRenderPass,
modifier = Modifier.align(Alignment.BottomStart)
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp)
)
}
key(selectedNode) {
RightInfoPanel(
selectedNode = selectedNode,
modifier = Modifier.align(Alignment.TopEnd)
)
}

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

internal class SandboxState {
var offset by mutableStateOf(Offset.Zero)
var scale by mutableFloatStateOf(1f)

fun reset() {
offset = Offset.Zero
Expand All @@ -218,13 +164,3 @@ internal sealed interface TraceMode {
}
}
}

/**
* Allows users to select from multiple devices that are currently running.
*/
private 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
@@ -0,0 +1,74 @@
package com.squareup.workflow1.traceviewer

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.squareup.workflow1.traceviewer.ui.control.DisplayDevices
import com.squareup.workflow1.traceviewer.ui.control.UploadFile
import io.github.vinceglb.filekit.PlatformFile
import kotlinx.coroutines.delay

/**
* Main window composable that shows both file upload and device selection options.
*/
@Composable
internal fun LandingWindow(
modifier: Modifier = Modifier,
onFileSelected: (PlatformFile) -> Unit,
onDeviceSelected: (String) -> Unit
) {
Box(modifier = modifier.fillMaxSize()) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Workflow Trace Viewer",
fontSize = 24.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 48.dp)
)

// File selection section
UploadFile(
resetOnFileSelect = { file ->
file?.let { onFileSelected(it) }
}
)

Text(
text = "— OR —",
fontSize = 14.sp,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
modifier = Modifier.padding(bottom = 24.dp)
)

// Device selection section
Text(
text = "Connect to Device",
fontSize = 18.sp,
fontWeight = FontWeight.Medium,
modifier = Modifier.padding(bottom = 24.dp)
)

DisplayDevices(
onDeviceSelect = onDeviceSelected,
)
}
}
}
Loading
Loading