diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index ab4de0b7d8..1ca0a41ed0 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -172,7 +172,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Check with Gradle + - name: Lint via gradle uses: ./.github/actions/gradle-task with: task: lint @@ -187,7 +187,7 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Check with Gradle + - name: allTests via gradle uses: ./.github/actions/gradle-task with: task: | @@ -228,18 +228,24 @@ jobs: - name: Checkout uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - name: Check with Gradle + - name: jvmTest via Gradle uses: ./.github/actions/gradle-task with: task: jvmTest --continue -Pworkflow.runtime=drainExclusive restore-cache-key: main-build-artifacts + - name: test via Gradle + uses: ./.github/actions/gradle-task + with: + task: test --continue -Pworkflow.runtime=drainExclusive + restore-cache-key: main-build-artifacts + # Report as GitHub Pull Request Check. - name: Publish Test Report uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-conflate-runtime-test: name: CSR JVM Tests @@ -260,7 +266,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-stateChange-runtime-test: name: SCO JVM Tests @@ -281,7 +287,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-stable-handlers-test: name: SEH JVM Tests @@ -302,7 +308,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-partial-runtime-test: name: PTR JVM Tests @@ -323,7 +329,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-conflate-stateChange-runtime-test: name: SCO, CSR JVM Tests @@ -344,7 +350,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-conflate-partial-runtime-test: name: CSR, PTR JVM Tests @@ -365,7 +371,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-conflate-drainExclusive-runtime-test: name: CSR, DEA JVM Tests @@ -386,7 +392,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-stateChange-drainExclusive-runtime-test: name: SCO, DEA JVM Tests @@ -407,7 +413,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-partial-drainExclusive-runtime-test: name: PTR, DEA JVM Tests @@ -428,7 +434,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-conflate-stateChange-drainExclusive-runtime-test: name: SCO, CSR, DEA JVM Tests @@ -449,7 +455,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-conflate-partial-drainExclusive-runtime-test: name: CSR, PTR, DEA JVM Tests @@ -470,7 +476,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' jvm-all-runtime-test: name: ALL Optimizations JVM Tests @@ -491,7 +497,7 @@ jobs: uses: mikepenz/action-junit-report@db71d41eb79864e25ab0337e395c352e84523afe # v4 if: always() # always run even if the previous step fails with: - report_paths: '**/build/test-results/jvmTest/TEST-*.xml' + report_paths: '**/build/test-results/*[tT]est/TEST-*.xml' ios-tests: name: iOS Tests diff --git a/artifacts.json b/artifacts.json index 438a6826a5..705dcc1b98 100644 --- a/artifacts.json +++ b/artifacts.json @@ -1,13 +1,4 @@ [ - { - "gradlePath": ":trace-encoder", - "group": "com.squareup.workflow1", - "artifactId": "trace-encoder", - "description": "Trace Encoder", - "packaging": "jar", - "javaVersion": 8, - "publicationName": "maven" - }, { "gradlePath": ":workflow-core", "group": "com.squareup.workflow1", @@ -148,7 +139,16 @@ "group": "com.squareup.workflow1", "artifactId": "workflow-tracing", "description": "Workflow Tracing", - "packaging": "jar", + "packaging": "aar", + "javaVersion": 8, + "publicationName": "maven" + }, + { + "gradlePath": ":workflow-tracing-papa", + "group": "com.squareup.workflow1", + "artifactId": "workflow-tracing-papa", + "description": "Workflow Tracing Papa", + "packaging": "aar", "javaVersion": 8, "publicationName": "maven" }, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 732d223365..ce7a144260 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ androidx-activity = "1.8.2" androidx-appcompat = "1.7.0" androidx-benchmark = "1.3.3" androidx-cardview = "1.0.0" +androidx-collection = "1.5.0" # see https://developer.android.com/jetpack/compose/bom/bom-mapping androidx-compose-bom = "2025.03.01" androidx-constraintlayout = "2.1.4" @@ -127,6 +128,8 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "androidx-cardview" } +androidx-collection = { module = "androidx.collection:collection", version.ref = "androidx-collection" } + androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "androidx-compose-bom" } androidx-compose-foundation-layout = { module = "androidx.compose.foundation:foundation-layout" } diff --git a/samples/dungeon/app/build.gradle.kts b/samples/dungeon/app/build.gradle.kts index 0c15f339a6..1bdc085446 100644 --- a/samples/dungeon/app/build.gradle.kts +++ b/samples/dungeon/app/build.gradle.kts @@ -47,7 +47,6 @@ dependencies { implementation(project(":samples:dungeon:common")) implementation(project(":samples:dungeon:timemachine")) implementation(project(":samples:dungeon:timemachine-shakeable")) - implementation(project(":workflow-tracing")) implementation(project(":workflow-ui:core-android")) implementation(project(":workflow-ui:core-common")) diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt index c0eb034615..f37ef0a203 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/Component.kt @@ -56,7 +56,6 @@ class Component(context: AppCompatActivity) { val timeMachineModelFactory = TimeMachineModel.Factory( context, - timeMachineWorkflow, - traceFilesDir = context.filesDir + timeMachineWorkflow ) } diff --git a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt index ed9865f8ca..f3b0932a88 100644 --- a/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt +++ b/samples/dungeon/app/src/main/java/com/squareup/sample/dungeon/TimeMachineModel.kt @@ -8,27 +8,23 @@ import androidx.savedstate.SavedStateRegistryOwner import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.android.renderWorkflowIn import com.squareup.workflow1.config.AndroidRuntimeConfigTools -import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor import com.squareup.workflow1.ui.Screen import kotlinx.coroutines.flow.StateFlow -import java.io.File import kotlin.time.ExperimentalTime class TimeMachineModel( private val savedState: SavedStateHandle, private val workflow: TimeMachineAppWorkflow, - private val traceFilesDir: File ) : ViewModel() { @OptIn(ExperimentalTime::class, WorkflowExperimentalRuntime::class) val renderings: StateFlow by lazy { - val traceFile = traceFilesDir.resolve("workflow-trace-dungeon.json") renderWorkflowIn( workflow = workflow, prop = "simple_maze.txt", scope = viewModelScope, savedStateHandle = savedState, - interceptors = listOf(TracingWorkflowInterceptor(traceFile)), + interceptors = emptyList(), runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() ) } @@ -36,7 +32,6 @@ class TimeMachineModel( class Factory( owner: SavedStateRegistryOwner, private val workflow: TimeMachineAppWorkflow, - private val traceFilesDir: File ) : AbstractSavedStateViewModelFactory(owner, null) { override fun create( key: String, @@ -45,7 +40,7 @@ class TimeMachineModel( ): T { if (modelClass == TimeMachineModel::class.java) { @Suppress("UNCHECKED_CAST") - return TimeMachineModel(handle, workflow, traceFilesDir) as T + return TimeMachineModel(handle, workflow) as T } throw IllegalArgumentException("Unknown ViewModel type $modelClass") diff --git a/samples/tictactoe/app/build.gradle.kts b/samples/tictactoe/app/build.gradle.kts index 3bf58b7d74..e318b37708 100644 --- a/samples/tictactoe/app/build.gradle.kts +++ b/samples/tictactoe/app/build.gradle.kts @@ -39,7 +39,6 @@ dependencies { implementation(project(":samples:containers:android")) implementation(project(":samples:tictactoe:common")) - implementation(project(":workflow-tracing")) implementation(project(":workflow-ui:core-android")) implementation(project(":workflow-ui:core-common")) } diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeComponent.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeComponent.kt index 472097ea33..8a3f359f8d 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeComponent.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeComponent.kt @@ -55,7 +55,7 @@ class TicTacToeComponent : ViewModel() { private val ticTacToeWorkflow = TicTacToeWorkflow(authWorkflow(), gameWorkflow()) fun ticTacToeModelFactory(owner: AppCompatActivity): TicTacToeModel.Factory = - TicTacToeModel.Factory(owner, ticTacToeWorkflow, traceFilesDir = owner.filesDir) + TicTacToeModel.Factory(owner, ticTacToeWorkflow) companion object { init { diff --git a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt index 8fe7014641..db523f643d 100644 --- a/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt +++ b/samples/tictactoe/app/src/main/java/com/squareup/sample/mainactivity/TicTacToeModel.kt @@ -11,27 +11,23 @@ import com.squareup.sample.mainworkflow.TicTacToeWorkflow import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.android.renderWorkflowIn import com.squareup.workflow1.config.AndroidRuntimeConfigTools -import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor import com.squareup.workflow1.ui.Screen import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow -import java.io.File class TicTacToeModel( private val savedState: SavedStateHandle, private val workflow: TicTacToeWorkflow, - private val traceFilesDir: File ) : ViewModel() { private val running = Job() val renderings: StateFlow by lazy { - val traceFile = traceFilesDir.resolve("workflow-trace-tictactoe.json") renderWorkflowIn( workflow = workflow, scope = viewModelScope, savedStateHandle = savedState, - interceptors = listOf(TracingWorkflowInterceptor(traceFile)), + interceptors = emptyList(), runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() ) { running.complete() @@ -43,7 +39,6 @@ class TicTacToeModel( class Factory( owner: SavedStateRegistryOwner, private val workflow: TicTacToeWorkflow, - private val traceFilesDir: File ) : AbstractSavedStateViewModelFactory(owner, null) { override fun create( key: String, @@ -52,7 +47,7 @@ class TicTacToeModel( ): T { if (modelClass == TicTacToeModel::class.java) { @Suppress("UNCHECKED_CAST") - return TicTacToeModel(handle, workflow, traceFilesDir) as T + return TicTacToeModel(handle, workflow) as T } throw IllegalArgumentException("Unknown ViewModel type $modelClass") diff --git a/samples/todo-android/app/build.gradle.kts b/samples/todo-android/app/build.gradle.kts index c6b5b76b85..34c576d0bc 100644 --- a/samples/todo-android/app/build.gradle.kts +++ b/samples/todo-android/app/build.gradle.kts @@ -29,7 +29,6 @@ dependencies { implementation(project(":samples:containers:android")) implementation(project(":samples:containers:common")) implementation(project(":workflow-core")) - implementation(project(":workflow-tracing")) implementation(project(":workflow-ui:core-android")) implementation(project(":workflow-ui:core-common")) diff --git a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt index ff0e88bda9..6d85d9e315 100644 --- a/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt +++ b/samples/todo-android/app/src/main/java/com/squareup/sample/todo/ToDoActivity.kt @@ -12,14 +12,12 @@ import com.squareup.sample.container.overviewdetail.OverviewDetailContainer import com.squareup.workflow1.WorkflowExperimentalRuntime import com.squareup.workflow1.android.renderWorkflowIn import com.squareup.workflow1.config.AndroidRuntimeConfigTools -import com.squareup.workflow1.diagnostic.tracing.TracingWorkflowInterceptor import com.squareup.workflow1.ui.Screen import com.squareup.workflow1.ui.ViewRegistry import com.squareup.workflow1.ui.withRegistry import com.squareup.workflow1.ui.workflowContentView import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map -import java.io.File class ToDoActivity : AppCompatActivity() { @@ -31,7 +29,7 @@ class ToDoActivity : AppCompatActivity() { workflowContentView .take( lifecycle, - model.ensureWorkflow(traceFilesDir = filesDir).map { it.withRegistry(viewRegistry) } + model.ensureWorkflow().map { it.withRegistry(viewRegistry) } ) } @@ -43,15 +41,14 @@ class ToDoActivity : AppCompatActivity() { class ToDoModel(private val savedState: SavedStateHandle) : ViewModel() { private var renderings: StateFlow? = null - fun ensureWorkflow(traceFilesDir: File): StateFlow { + fun ensureWorkflow(): StateFlow { if (renderings == null) { - val traceFile = traceFilesDir.resolve("workflow-trace-todo.json") renderings = renderWorkflowIn( workflow = TodoListsAppWorkflow, scope = viewModelScope, savedStateHandle = savedState, - interceptors = listOf(TracingWorkflowInterceptor(traceFile)), + interceptors = emptyList(), runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig() ) } diff --git a/settings.gradle.kts b/settings.gradle.kts index 00f951b069..5eb9fc883c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -59,7 +59,6 @@ include( ":samples:tictactoe:app", ":samples:tictactoe:common", ":samples:todo-android:app", - ":trace-encoder", ":workflow-config:config-android", ":workflow-config:config-jvm", ":workflow-core", @@ -68,6 +67,7 @@ include( ":workflow-rx2", ":workflow-testing", ":workflow-tracing", + ":workflow-tracing-papa", ":workflow-trace-viewer", ":workflow-ui:compose", ":workflow-ui:compose-tooling", diff --git a/trace-encoder/README.md b/trace-encoder/README.md deleted file mode 100644 index e4f6c36e11..0000000000 --- a/trace-encoder/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Module trace-encoder - -This module contains a small library for generating trace files that can be viewed with Chrome by -visiting `chrome://tracing`. Trace events are represented by the various subclasses of `TraceEvent`, -and are encoded by a `TraceEncoder`. The trace file format is described in [this doc][1]. - -The trace format is designed for low-level profiling data, so events are grouped by process and -thread. `TraceEncoder` lets you create `TraceLogger`s given process and thread names. All events -logged to a `TraceLogger` are grouped under those process and thread names. - -[1]: https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU diff --git a/trace-encoder/api/trace-encoder.api b/trace-encoder/api/trace-encoder.api deleted file mode 100644 index 6facb449a2..0000000000 --- a/trace-encoder/api/trace-encoder.api +++ /dev/null @@ -1,173 +0,0 @@ -public abstract interface class com/squareup/tracing/TimeMark { - public abstract fun getElapsedNow ()J -} - -public final class com/squareup/tracing/TraceEncoder : java/io/Closeable { - public fun (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/tracing/TimeMark;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/tracing/TimeMark;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close ()V - public final fun createLogger (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/tracing/TraceLogger; - public static synthetic fun createLogger$default (Lcom/squareup/tracing/TraceEncoder;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/tracing/TraceLogger; -} - -public abstract class com/squareup/tracing/TraceEvent { - public fun getCategory ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$AsyncDurationBegin : com/squareup/tracing/TraceEvent { - public fun (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/Object; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/util/Map; - public final fun component4 ()Ljava/lang/String; - public final fun copy (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)Lcom/squareup/tracing/TraceEvent$AsyncDurationBegin; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$AsyncDurationBegin;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$AsyncDurationBegin; - public fun equals (Ljava/lang/Object;)Z - public final fun getArgs ()Ljava/util/Map; - public fun getCategory ()Ljava/lang/String; - public final fun getId ()Ljava/lang/Object; - public final fun getName ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$AsyncDurationEnd : com/squareup/tracing/TraceEvent { - public fun (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/Object; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/util/Map; - public final fun component4 ()Ljava/lang/String; - public final fun copy (Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)Lcom/squareup/tracing/TraceEvent$AsyncDurationEnd; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$AsyncDurationEnd;Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$AsyncDurationEnd; - public fun equals (Ljava/lang/Object;)Z - public final fun getArgs ()Ljava/util/Map; - public fun getCategory ()Ljava/lang/String; - public final fun getId ()Ljava/lang/Object; - public final fun getName ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$Counter : com/squareup/tracing/TraceEvent { - public fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/Long;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/util/Map; - public final fun component3 ()Ljava/lang/Long; - public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/lang/Long;)Lcom/squareup/tracing/TraceEvent$Counter; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$Counter;Ljava/lang/String;Ljava/util/Map;Ljava/lang/Long;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$Counter; - public fun equals (Ljava/lang/Object;)Z - public final fun getId ()Ljava/lang/Long; - public final fun getName ()Ljava/lang/String; - public final fun getSeries ()Ljava/util/Map; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$DurationBegin : com/squareup/tracing/TraceEvent { - public fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/util/Map; - public final fun component3 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)Lcom/squareup/tracing/TraceEvent$DurationBegin; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$DurationBegin;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$DurationBegin; - public fun equals (Ljava/lang/Object;)Z - public final fun getArgs ()Ljava/util/Map; - public fun getCategory ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$DurationEnd : com/squareup/tracing/TraceEvent { - public fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/util/Map; - public final fun component3 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)Lcom/squareup/tracing/TraceEvent$DurationEnd; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$DurationEnd;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$DurationEnd; - public fun equals (Ljava/lang/Object;)Z - public final fun getArgs ()Ljava/util/Map; - public fun getCategory ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$Instant : com/squareup/tracing/TraceEvent { - public fun (Ljava/lang/String;Ljava/util/Map;Lcom/squareup/tracing/TraceEvent$Instant$InstantScope;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Map;Lcom/squareup/tracing/TraceEvent$Instant$InstantScope;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/util/Map; - public final fun component3 ()Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; - public final fun component4 ()Ljava/lang/String; - public final fun copy (Ljava/lang/String;Ljava/util/Map;Lcom/squareup/tracing/TraceEvent$Instant$InstantScope;Ljava/lang/String;)Lcom/squareup/tracing/TraceEvent$Instant; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$Instant;Ljava/lang/String;Ljava/util/Map;Lcom/squareup/tracing/TraceEvent$Instant$InstantScope;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$Instant; - public fun equals (Ljava/lang/Object;)Z - public final fun getArgs ()Ljava/util/Map; - public fun getCategory ()Ljava/lang/String; - public final fun getName ()Ljava/lang/String; - public final fun getScope ()Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$Instant$InstantScope : java/lang/Enum { - public static final field GLOBAL Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; - public static final field PROCESS Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; - public static final field THREAD Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public static fun valueOf (Ljava/lang/String;)Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; - public static fun values ()[Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; -} - -public final class com/squareup/tracing/TraceEvent$ObjectCreated : com/squareup/tracing/TraceEvent { - public fun (JLjava/lang/String;)V - public final fun component1 ()J - public final fun component2 ()Ljava/lang/String; - public final fun copy (JLjava/lang/String;)Lcom/squareup/tracing/TraceEvent$ObjectCreated; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$ObjectCreated;JLjava/lang/String;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$ObjectCreated; - public fun equals (Ljava/lang/Object;)Z - public final fun getId ()J - public final fun getObjectType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$ObjectDestroyed : com/squareup/tracing/TraceEvent { - public fun (JLjava/lang/String;)V - public final fun component1 ()J - public final fun component2 ()Ljava/lang/String; - public final fun copy (JLjava/lang/String;)Lcom/squareup/tracing/TraceEvent$ObjectDestroyed; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$ObjectDestroyed;JLjava/lang/String;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$ObjectDestroyed; - public fun equals (Ljava/lang/Object;)Z - public final fun getId ()J - public final fun getObjectType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class com/squareup/tracing/TraceEvent$ObjectSnapshot : com/squareup/tracing/TraceEvent { - public fun (JLjava/lang/String;Ljava/lang/Object;)V - public final fun component1 ()J - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()Ljava/lang/Object; - public final fun copy (JLjava/lang/String;Ljava/lang/Object;)Lcom/squareup/tracing/TraceEvent$ObjectSnapshot; - public static synthetic fun copy$default (Lcom/squareup/tracing/TraceEvent$ObjectSnapshot;JLjava/lang/String;Ljava/lang/Object;ILjava/lang/Object;)Lcom/squareup/tracing/TraceEvent$ObjectSnapshot; - public fun equals (Ljava/lang/Object;)Z - public final fun getId ()J - public final fun getObjectType ()Ljava/lang/String; - public final fun getSnapshot ()Ljava/lang/Object; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public abstract interface class com/squareup/tracing/TraceLogger { - public abstract fun log (Lcom/squareup/tracing/TraceEvent;)V - public abstract fun log (Ljava/util/List;)V -} - diff --git a/trace-encoder/build.gradle.kts b/trace-encoder/build.gradle.kts deleted file mode 100644 index 5eee31ee1d..0000000000 --- a/trace-encoder/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - id("kotlin-jvm") - id("com.google.devtools.ksp") - id("published") -} - -dependencies { - api(libs.kotlin.jdk8) - api(libs.kotlinx.coroutines.core) - api(libs.squareup.okio) - - compileOnly(libs.jetbrains.annotations) - compileOnly(libs.squareup.moshi.codegen) - - implementation(libs.squareup.moshi) - implementation(libs.squareup.moshi.adapters) - - ksp(libs.squareup.moshi.codegen) - - testImplementation(libs.junit) - testImplementation(libs.kotlin.test.core) - testImplementation(libs.kotlin.test.jdk) -} diff --git a/trace-encoder/gradle.properties b/trace-encoder/gradle.properties deleted file mode 100644 index b6c85444e0..0000000000 --- a/trace-encoder/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -POM_ARTIFACT_ID=trace-encoder -POM_NAME=Trace Encoder -POM_PACKAGING=jar diff --git a/trace-encoder/src/main/baseline-prof.txt b/trace-encoder/src/main/baseline-prof.txt deleted file mode 100644 index f719d5621d..0000000000 --- a/trace-encoder/src/main/baseline-prof.txt +++ /dev/null @@ -1,104 +0,0 @@ -HSPLcom/squareup/tracing/ChromeTraceEvent$Companion$jsonAdapter$2;->()V -HSPLcom/squareup/tracing/ChromeTraceEvent$Companion$jsonAdapter$2;->invoke()Lcom/squareup/moshi/JsonAdapter; -HSPLcom/squareup/tracing/ChromeTraceEvent$Companion$jsonAdapter$2;->invoke()Ljava/lang/Object; -HSPLcom/squareup/tracing/ChromeTraceEvent$Companion;->()V -HSPLcom/squareup/tracing/ChromeTraceEvent$Companion;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/ChromeTraceEvent$Companion;->access$getJsonAdapter(Lcom/squareup/tracing/ChromeTraceEvent$Companion;)Lcom/squareup/moshi/JsonAdapter; -HSPLcom/squareup/tracing/ChromeTraceEvent$Companion;->getJsonAdapter()Lcom/squareup/moshi/JsonAdapter; -HSPLcom/squareup/tracing/ChromeTraceEvent$Phase;->$values()[Lcom/squareup/tracing/ChromeTraceEvent$Phase; -HSPLcom/squareup/tracing/ChromeTraceEvent$Phase;->(Ljava/lang/String;IC)V -HSPLcom/squareup/tracing/ChromeTraceEvent$Phase;->getCode$wf1_trace_encoder()C -HSPLcom/squareup/tracing/ChromeTraceEvent;->(Ljava/lang/String;Ljava/lang/String;Lcom/squareup/tracing/ChromeTraceEvent$Phase;JIILjava/lang/Object;Ljava/lang/Character;Ljava/util/Map;)V -HSPLcom/squareup/tracing/ChromeTraceEvent;->(Ljava/lang/String;Ljava/lang/String;Lcom/squareup/tracing/ChromeTraceEvent$Phase;JIILjava/lang/Object;Ljava/lang/Character;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/ChromeTraceEvent;->access$getJsonAdapter$delegate$cp()Lkotlin/Lazy; -HSPLcom/squareup/tracing/ChromeTraceEvent;->getArgs()Ljava/util/Map; -HSPLcom/squareup/tracing/ChromeTraceEvent;->getCategory()Ljava/lang/String; -HSPLcom/squareup/tracing/ChromeTraceEvent;->getId()Ljava/lang/Object; -HSPLcom/squareup/tracing/ChromeTraceEvent;->getName()Ljava/lang/String; -HSPLcom/squareup/tracing/ChromeTraceEvent;->getPhase()Lcom/squareup/tracing/ChromeTraceEvent$Phase; -HSPLcom/squareup/tracing/ChromeTraceEvent;->getProcessId()I -HSPLcom/squareup/tracing/ChromeTraceEvent;->getScope()Ljava/lang/Character; -HSPLcom/squareup/tracing/ChromeTraceEvent;->getThreadId()I -HSPLcom/squareup/tracing/ChromeTraceEvent;->getTimestampMicros()J -HSPLcom/squareup/tracing/ChromeTraceEvent;->writeTo(Lokio/BufferedSink;)V -HSPLcom/squareup/tracing/TraceEncoder$createLogger$1;->(Lcom/squareup/tracing/TraceEncoder;IILjava/lang/String;Ljava/lang/String;)V -HSPLcom/squareup/tracing/TraceEncoder$createLogger$1;->log(Lcom/squareup/tracing/TraceEvent;)V -HSPLcom/squareup/tracing/TraceEncoder$createLogger$1;->log(Ljava/util/List;)V -HSPLcom/squareup/tracing/TraceEncoder$events$1;->(Lcom/squareup/tracing/TraceEncoder;Lkotlin/coroutines/Continuation;)V -HSPLcom/squareup/tracing/TraceEncoder$events$1;->create(Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Lkotlin/coroutines/Continuation; -HSPLcom/squareup/tracing/TraceEncoder$events$1;->invokeSuspend(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/squareup/tracing/TraceEncoder;->(Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/tracing/TimeMark;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/jvm/functions/Function0;)V -HSPLcom/squareup/tracing/TraceEncoder;->(Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/tracing/TimeMark;Lkotlinx/coroutines/CoroutineDispatcher;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/TraceEncoder;->access$getSinkProvider$p(Lcom/squareup/tracing/TraceEncoder;)Lkotlin/jvm/functions/Function0; -HSPLcom/squareup/tracing/TraceEncoder;->createLogger(Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/tracing/TraceLogger; -HSPLcom/squareup/tracing/TraceEncoder;->getTimestampNow()J -HSPLcom/squareup/tracing/TraceEncoder;->log$wf1_trace_encoder(IILcom/squareup/tracing/TraceEvent;)V -HSPLcom/squareup/tracing/TraceEncoder;->log$wf1_trace_encoder(IILjava/util/List;)V -HSPLcom/squareup/tracing/TraceEncoder;->safeOffer(Lkotlinx/coroutines/channels/SendChannel;Ljava/lang/Object;)V -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationBegin;->(Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationBegin;->getArgs()Ljava/util/Map; -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationBegin;->getCategory()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationBegin;->getId()Ljava/lang/Object; -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationBegin;->getName()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationEnd;->(Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationEnd;->(Ljava/lang/Object;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationEnd;->getCategory()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationEnd;->getId()Ljava/lang/Object; -HSPLcom/squareup/tracing/TraceEvent$AsyncDurationEnd;->getName()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$Counter;->(Ljava/lang/String;Ljava/util/Map;Ljava/lang/Long;)V -HSPLcom/squareup/tracing/TraceEvent$Counter;->(Ljava/lang/String;Ljava/util/Map;Ljava/lang/Long;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/TraceEvent$Counter;->getId()Ljava/lang/Long; -HSPLcom/squareup/tracing/TraceEvent$Counter;->getName()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$Counter;->getSeries()Ljava/util/Map; -HSPLcom/squareup/tracing/TraceEvent$DurationBegin;->(Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V -HSPLcom/squareup/tracing/TraceEvent$DurationBegin;->(Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/TraceEvent$DurationBegin;->getArgs()Ljava/util/Map; -HSPLcom/squareup/tracing/TraceEvent$DurationBegin;->getCategory()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$DurationBegin;->getName()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$DurationEnd;->(Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;)V -HSPLcom/squareup/tracing/TraceEvent$DurationEnd;->(Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/TraceEvent$DurationEnd;->getArgs()Ljava/util/Map; -HSPLcom/squareup/tracing/TraceEvent$DurationEnd;->getCategory()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$DurationEnd;->getName()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$Instant$InstantScope;->$values()[Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; -HSPLcom/squareup/tracing/TraceEvent$Instant$InstantScope;->(Ljava/lang/String;I)V -HSPLcom/squareup/tracing/TraceEvent$Instant$InstantScope;->values()[Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; -HSPLcom/squareup/tracing/TraceEvent$Instant;->(Ljava/lang/String;Ljava/util/Map;Lcom/squareup/tracing/TraceEvent$Instant$InstantScope;Ljava/lang/String;)V -HSPLcom/squareup/tracing/TraceEvent$Instant;->(Ljava/lang/String;Ljava/util/Map;Lcom/squareup/tracing/TraceEvent$Instant$InstantScope;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/TraceEvent$Instant;->getArgs()Ljava/util/Map; -HSPLcom/squareup/tracing/TraceEvent$Instant;->getCategory()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$Instant;->getName()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$Instant;->getScope()Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; -HSPLcom/squareup/tracing/TraceEvent$ObjectCreated;->(JLjava/lang/String;)V -HSPLcom/squareup/tracing/TraceEvent$ObjectCreated;->getId()J -HSPLcom/squareup/tracing/TraceEvent$ObjectCreated;->getObjectType()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$ObjectDestroyed;->(JLjava/lang/String;)V -HSPLcom/squareup/tracing/TraceEvent$ObjectDestroyed;->getId()J -HSPLcom/squareup/tracing/TraceEvent$ObjectDestroyed;->getObjectType()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$ObjectSnapshot;->(JLjava/lang/String;Ljava/lang/Object;)V -HSPLcom/squareup/tracing/TraceEvent$ObjectSnapshot;->getId()J -HSPLcom/squareup/tracing/TraceEvent$ObjectSnapshot;->getObjectType()Ljava/lang/String; -HSPLcom/squareup/tracing/TraceEvent$ObjectSnapshot;->getSnapshot()Ljava/lang/Object; -HSPLcom/squareup/tracing/TraceEvent;->()V -HSPLcom/squareup/tracing/TraceEvent;->(Lkotlin/jvm/internal/DefaultConstructorMarker;)V -HSPLcom/squareup/tracing/TraceEvent;->getCategory()Ljava/lang/String; -Lcom/squareup/tracing/ChromeTraceEvent$Companion$jsonAdapter$2; -Lcom/squareup/tracing/ChromeTraceEvent$Companion; -Lcom/squareup/tracing/ChromeTraceEvent$Phase; -Lcom/squareup/tracing/ChromeTraceEvent; -Lcom/squareup/tracing/TimeMark; -Lcom/squareup/tracing/TraceEncoder$createLogger$1; -Lcom/squareup/tracing/TraceEncoder$events$1; -Lcom/squareup/tracing/TraceEncoder; -Lcom/squareup/tracing/TraceEvent$AsyncDurationBegin; -Lcom/squareup/tracing/TraceEvent$AsyncDurationEnd; -Lcom/squareup/tracing/TraceEvent$Counter; -Lcom/squareup/tracing/TraceEvent$DurationBegin; -Lcom/squareup/tracing/TraceEvent$DurationEnd; -Lcom/squareup/tracing/TraceEvent$Instant$InstantScope; -Lcom/squareup/tracing/TraceEvent$Instant; -Lcom/squareup/tracing/TraceEvent$ObjectCreated; -Lcom/squareup/tracing/TraceEvent$ObjectDestroyed; -Lcom/squareup/tracing/TraceEvent$ObjectSnapshot; -Lcom/squareup/tracing/TraceEvent; -Lcom/squareup/tracing/TraceLogger; diff --git a/trace-encoder/src/main/java/com/squareup/tracing/ChromeTraceEvent.kt b/trace-encoder/src/main/java/com/squareup/tracing/ChromeTraceEvent.kt deleted file mode 100644 index 3affb3998e..0000000000 --- a/trace-encoder/src/main/java/com/squareup/tracing/ChromeTraceEvent.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.squareup.tracing - -import com.squareup.moshi.FromJson -import com.squareup.moshi.Json -import com.squareup.moshi.JsonAdapter -import com.squareup.moshi.JsonClass -import com.squareup.moshi.Moshi -import com.squareup.moshi.ToJson -import com.squareup.tracing.ChromeTraceEvent.Companion.INSTANT_SCOPE_THREAD -import com.squareup.tracing.ChromeTraceEvent.Phase -import com.squareup.tracing.ChromeTraceEvent.Phase.METADATA -import okio.BufferedSink - -/** - * JSON-serializable model of a `chrome://tracing` event. - * - * Documentation of event format is available - * [here](https://docs.google.com/document/d/1CvAClvFfyA5R-PhYUmn5OOQtYMH4h6I0nSsKchNAySU). - */ -@JsonClass(generateAdapter = true) -internal data class ChromeTraceEvent( - @param:Json(name = "name") val name: String, - @param:Json(name = "cat") val category: String? = null, - @param:Json(name = "ph") val phase: Phase, - @param:Json(name = "ts") val timestampMicros: Long, - @param:Json(name = "pid") val processId: Int = 0, - @param:Json(name = "tid") val threadId: Int = 0, - /** Only used for ASYNC events. */ - @param:Json(name = "id") val id: Any? = null, - /** - * Only used for [Phase.INSTANT] events. - * See [INSTANT_SCOPE_THREAD], etc. - */ - @param:Json(name = "s") val scope: Char? = null, - @param:Json(name = "args") val args: Map? = null -) { - - @Suppress("unused") - enum class Phase(internal val code: Char) { - DURATION_BEGIN('B'), - DURATION_END('E'), - COMPLETE('X'), - INSTANT('i'), - COUNTER('C'), - ASYNC_BEGIN('b'), - ASYNC_INSTANT('n'), - ASYNC_END('e'), - OBJECT_CREATED('N'), - OBJECT_SNAPSHOT('O'), - OBJECT_DESTROYED('D'), - METADATA('M') - } - - /** - * Writes this event to a trace file using [jsonAdapter]. - */ - fun writeTo(sink: BufferedSink) = jsonAdapter.toJson(sink, this) - - companion object { - const val INSTANT_SCOPE_THREAD = 't' - const val INSTANT_SCOPE_PROCESS = 'p' - const val INSTANT_SCOPE_GLOBAL = 'g' - - private val jsonAdapter: JsonAdapter by lazy { - val moshi = Moshi.Builder() - .add(PhaseAdapter) - .build() - return@lazy moshi.adapter(ChromeTraceEvent::class.java) - } - } -} - -@Suppress("unused") -private object PhaseAdapter { - @ToJson fun toJson(phase: Phase) = phase.code - - @FromJson fun fromJson(code: Char) = Phase.values().single { it.code == code } -} - -internal fun createProcessNameEvent( - name: String, - processId: Int, - timestampMicros: Long -): ChromeTraceEvent = ChromeTraceEvent( - name = "process_name", - phase = METADATA, - processId = processId, - args = mapOf("name" to name), - timestampMicros = timestampMicros -) - -internal fun createThreadNameEvent( - name: String, - processId: Int, - threadId: Int, - timestampMicros: Long -): ChromeTraceEvent = ChromeTraceEvent( - name = "thread_name", - phase = METADATA, - processId = processId, - threadId = threadId, - args = mapOf("name" to name), - timestampMicros = timestampMicros -) diff --git a/trace-encoder/src/main/java/com/squareup/tracing/TimeMark.kt b/trace-encoder/src/main/java/com/squareup/tracing/TimeMark.kt deleted file mode 100644 index 2f723639b0..0000000000 --- a/trace-encoder/src/main/java/com/squareup/tracing/TimeMark.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.squareup.tracing - -/** - * Interface that represents a time point. Remains bound to the time source it was taken from and - * allows querying for the duration of time elapsed from that point (see the val [elapsedNow]). - */ -public interface TimeMark { - /** - * Returns the amount of time passed from this mark measured with the time source from which this - * mark was taken. - * - * Note that the content of this val can change on subsequent invocations. - */ - public val elapsedNow: Long -} diff --git a/trace-encoder/src/main/java/com/squareup/tracing/TraceEncoder.kt b/trace-encoder/src/main/java/com/squareup/tracing/TraceEncoder.kt deleted file mode 100644 index 0e051e26f7..0000000000 --- a/trace-encoder/src/main/java/com/squareup/tracing/TraceEncoder.kt +++ /dev/null @@ -1,129 +0,0 @@ -package com.squareup.tracing - -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.ObsoleteCoroutinesApi -import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.channels.consumeEach -import okio.BufferedSink -import java.io.Closeable -import java.util.concurrent.atomic.AtomicInteger -import kotlin.time.TimeSource - -/** - * Encodes and writes [trace events][TraceEvent] to an Okio [BufferedSink]. - * - * @param scope The [CoroutineScope] that defines the lifetime for the encoder. When the scope is - * cancelled or fails, the sink returned from [sinkProvider] will be closed. - * @param start The [TimeMark] to consider the beginning timestamp of the trace. All trace events' - * timestamps are relative to this mark. - * [TimeSource.Monotonic].[markNow][TimeSource.Monotonic.markNow] by default. - * @param ioDispatcher The [CoroutineDispatcher] to use to execute all IO operations. - * [IO] by default. - * @param sinkProvider Returns the [BufferedSink] to use to write trace events to. Called on a - * background thread. - */ -@OptIn(ObsoleteCoroutinesApi::class) -public class TraceEncoder( - scope: CoroutineScope, - private val start: TimeMark = TraceEncoderTimeMark, - ioDispatcher: CoroutineDispatcher = IO, - private val sinkProvider: () -> BufferedSink -) : Closeable { - - private val processIdCounter = AtomicInteger(0) - private val threadIdCounter = AtomicInteger(0) - - private val events: SendChannel> = - scope.actor(ioDispatcher, capacity = UNLIMITED) { - sinkProvider().use { sink -> - // Start the JSON array. Doesn't need to be closed. - sink.writeUtf8("[\n") - - @Suppress("EXPERIMENTAL_API_USAGE") - consumeEach { eventBatch -> - eventBatch.forEach { event -> - event.writeTo(sink) - sink.writeUtf8(",\n") - } - sink.flush() - } - } - } - - /** - * Allocates a new thread ID named [threadName] and returns a [TraceLogger] that will log all - * events under that thread ID. - * - * Note this does not do anything with _actual_ threads, it just affects the thread ID used in - * trace events. - */ - public fun createLogger( - processName: String = "", - threadName: String = "" - ): TraceLogger { - val processId = processIdCounter.getAndIncrement() - val threadId = threadIdCounter.getAndIncrement() - - // Log metadata to set thread and process names. - val timestamp = getTimestampNow() - val processNameEvent = createProcessNameEvent(processName, processId, timestamp) - val threadNameEvent = createThreadNameEvent(threadName, processId, threadId, timestamp) - events.trySend(listOf(processNameEvent, threadNameEvent)) - - return object : TraceLogger { - override fun log(eventBatch: List) = log(processId, threadId, eventBatch) - override fun log(event: TraceEvent) = log(processId, threadId, event) - override fun toString(): String = - " TraceLogger(" + - "processName=$processName, processId=$processId, " + - "threadName=$threadName, threadId=$threadId)" - } - } - - override fun close() { - events.close() - } - - internal fun log( - processId: Int, - threadId: Int, - eventBatch: List - ) { - val timestampMicros = getTimestampNow() - val chromeTraceEvents = eventBatch.map { - it.toChromeTraceEvent(threadId, processId, timestampMicros) - } - events.trySend(chromeTraceEvents) - } - - internal fun log( - processId: Int, - threadId: Int, - event: TraceEvent - ) { - val timestampMicros = getTimestampNow() - val chromeTraceEvents = event.toChromeTraceEvent(threadId, processId, timestampMicros) - events.trySend(listOf(chromeTraceEvents)) - } - - private fun getTimestampNow(): Long = start.elapsedNow -} - -/** - * A [TimeMark] that invokes [System.nanoTime] to calculate its start point as well as elapsed time. - */ -private object TraceEncoderTimeMark : TimeMark { - /** - * The moment at which which this [TraceEncoderTimeMark] was instantiated. - */ - val start: Long = System.nanoTime() - - override val elapsedNow: Long - get() = (System.nanoTime() - start).inWholeMicroseconds() - - private fun Long.inWholeMicroseconds(): Long = (this / 1000) -} diff --git a/trace-encoder/src/main/java/com/squareup/tracing/TraceEvent.kt b/trace-encoder/src/main/java/com/squareup/tracing/TraceEvent.kt deleted file mode 100644 index f6ee199845..0000000000 --- a/trace-encoder/src/main/java/com/squareup/tracing/TraceEvent.kt +++ /dev/null @@ -1,193 +0,0 @@ -package com.squareup.tracing - -import com.squareup.tracing.ChromeTraceEvent.Companion.INSTANT_SCOPE_GLOBAL -import com.squareup.tracing.ChromeTraceEvent.Companion.INSTANT_SCOPE_PROCESS -import com.squareup.tracing.ChromeTraceEvent.Companion.INSTANT_SCOPE_THREAD -import com.squareup.tracing.ChromeTraceEvent.Phase.ASYNC_BEGIN -import com.squareup.tracing.ChromeTraceEvent.Phase.ASYNC_END -import com.squareup.tracing.ChromeTraceEvent.Phase.COUNTER -import com.squareup.tracing.ChromeTraceEvent.Phase.DURATION_BEGIN -import com.squareup.tracing.ChromeTraceEvent.Phase.DURATION_END -import com.squareup.tracing.ChromeTraceEvent.Phase.INSTANT -import com.squareup.tracing.ChromeTraceEvent.Phase.OBJECT_CREATED -import com.squareup.tracing.ChromeTraceEvent.Phase.OBJECT_DESTROYED -import com.squareup.tracing.ChromeTraceEvent.Phase.OBJECT_SNAPSHOT -import com.squareup.tracing.TraceEvent.AsyncDurationBegin -import com.squareup.tracing.TraceEvent.AsyncDurationEnd -import com.squareup.tracing.TraceEvent.Counter -import com.squareup.tracing.TraceEvent.DurationBegin -import com.squareup.tracing.TraceEvent.DurationEnd -import com.squareup.tracing.TraceEvent.Instant -import com.squareup.tracing.TraceEvent.Instant.InstantScope.GLOBAL -import com.squareup.tracing.TraceEvent.Instant.InstantScope.PROCESS -import com.squareup.tracing.TraceEvent.Instant.InstantScope.THREAD -import com.squareup.tracing.TraceEvent.ObjectCreated -import com.squareup.tracing.TraceEvent.ObjectDestroyed -import com.squareup.tracing.TraceEvent.ObjectSnapshot - -/** - * Represents a single event in a trace. - */ -public sealed class TraceEvent { - - public open val category: String? get() = null - - public data class DurationBegin( - val name: String, - val args: Map = emptyMap(), - override val category: String? = null - ) : TraceEvent() - - public data class DurationEnd( - val name: String, - val args: Map = emptyMap(), - override val category: String? = null - ) : TraceEvent() - - public data class Instant( - val name: String, - val args: Map = emptyMap(), - val scope: InstantScope = THREAD, - override val category: String? = null - ) : TraceEvent() { - public enum class InstantScope { - THREAD, - PROCESS, - GLOBAL - } - } - - public data class AsyncDurationBegin( - val id: Any, - val name: String, - val args: Map = emptyMap(), - override val category: String? = null - ) : TraceEvent() - - public data class AsyncDurationEnd( - val id: Any, - val name: String, - val args: Map = emptyMap(), - override val category: String? = null - ) : TraceEvent() - - public data class ObjectCreated( - val id: Long, - val objectType: String - ) : TraceEvent() - - public data class ObjectDestroyed( - val id: Long, - val objectType: String - ) : TraceEvent() - - public data class ObjectSnapshot( - val id: Long, - val objectType: String, - val snapshot: Any - ) : TraceEvent() - - public data class Counter( - val name: String, - val series: Map, - val id: Long? = null - ) : TraceEvent() -} - -internal fun TraceEvent.toChromeTraceEvent( - threadId: Int, - processId: Int, - nowMicros: Long -): ChromeTraceEvent = when (this) { - is DurationBegin -> ChromeTraceEvent( - phase = DURATION_BEGIN, - name = name, - category = category, - args = args, - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) - is DurationEnd -> ChromeTraceEvent( - phase = DURATION_END, - name = name, - category = category, - args = args, - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) - is Instant -> ChromeTraceEvent( - phase = INSTANT, - name = name, - category = category, - scope = when (scope) { - THREAD -> INSTANT_SCOPE_THREAD - PROCESS -> INSTANT_SCOPE_PROCESS - GLOBAL -> INSTANT_SCOPE_GLOBAL - }, - args = args, - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) - is AsyncDurationBegin -> ChromeTraceEvent( - phase = ASYNC_BEGIN, - id = id, - name = name, - category = category, - args = args, - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) - is AsyncDurationEnd -> ChromeTraceEvent( - phase = ASYNC_END, - id = id, - name = name, - category = category, - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) - is ObjectCreated -> ChromeTraceEvent( - phase = OBJECT_CREATED, - id = id.toHex(), - name = objectType, - category = category, - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) - is ObjectDestroyed -> ChromeTraceEvent( - phase = OBJECT_DESTROYED, - id = id.toHex(), - name = objectType, - category = category, - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) - is ObjectSnapshot -> ChromeTraceEvent( - phase = OBJECT_SNAPSHOT, - id = id.toHex(), - name = objectType, - category = category, - args = mapOf("snapshot" to snapshot), - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) - is Counter -> ChromeTraceEvent( - phase = COUNTER, - id = id?.toHex(), - name = name, - args = series, - threadId = threadId, - processId = processId, - timestampMicros = nowMicros - ) -} - -@Suppress("NOTHING_TO_INLINE") -private inline fun Long.toHex() = toString(16) diff --git a/trace-encoder/src/main/java/com/squareup/tracing/TraceLogger.kt b/trace-encoder/src/main/java/com/squareup/tracing/TraceLogger.kt deleted file mode 100644 index 3b4fb9526e..0000000000 --- a/trace-encoder/src/main/java/com/squareup/tracing/TraceLogger.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.squareup.tracing - -/** - * [Logs][log] [TraceEvent]s to a [TraceEncoder] under a given process and thread name. - * - * Create with [TraceEncoder.createLogger]. - */ -public interface TraceLogger { - - /** - * Tags all events with the current timestamp and then enqueues them to be written to the trace - * file. - */ - public fun log(eventBatch: List) - - /** - * Tags event with the current timestamp and then enqueues it to be written to the trace - * file. - */ - public fun log(event: TraceEvent) -} diff --git a/trace-encoder/src/test/java/com/squareup/tracing/ChromeTraceEventTest.kt b/trace-encoder/src/test/java/com/squareup/tracing/ChromeTraceEventTest.kt deleted file mode 100644 index 293eab8911..0000000000 --- a/trace-encoder/src/test/java/com/squareup/tracing/ChromeTraceEventTest.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.squareup.tracing - -import com.squareup.tracing.ChromeTraceEvent.Companion.INSTANT_SCOPE_PROCESS -import com.squareup.tracing.ChromeTraceEvent.Phase.ASYNC_BEGIN -import okio.Buffer -import org.junit.Test -import kotlin.test.assertEquals - -class ChromeTraceEventTest { - - @Test fun `serialization golden value`() { - val traceEvent = ChromeTraceEvent( - name = "name", - category = "category", - phase = ASYNC_BEGIN, - timestampMicros = 123456, - processId = 1, - threadId = 1, - id = -123L, - scope = INSTANT_SCOPE_PROCESS, - args = mapOf("key" to "value") - ) - val serialized = Buffer() - .also { traceEvent.writeTo(it) } - .readUtf8() - val expectedValue = - """{"name":"name","cat":"category","ph":"b","ts":123456,"pid":1,"tid":1,"id":-123,""" + - """"s":"p","args":{"key":"value"}}""" - - assertEquals(expectedValue, serialized) - } -} diff --git a/trace-encoder/src/test/java/com/squareup/tracing/TraceEncoderTest.kt b/trace-encoder/src/test/java/com/squareup/tracing/TraceEncoderTest.kt deleted file mode 100644 index 48eedda096..0000000000 --- a/trace-encoder/src/test/java/com/squareup/tracing/TraceEncoderTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.squareup.tracing - -import com.squareup.tracing.TraceEvent.Instant -import kotlinx.coroutines.runBlocking -import okio.Buffer -import kotlin.test.Test -import kotlin.test.assertEquals - -internal class TraceEncoderTest { - - /** - * [TimeMark] that always returns [now] as [elapsedNow]. - */ - private class FakeTimeMark : TimeMark { - var now: Long = 0L - override val elapsedNow: Long - get() = now - } - - @Test fun `multiple events sanity check`() { - val firstBatch = listOf( - traceEvent("one"), - traceEvent("two") - ) - val secondBatch = listOf(traceEvent("three")) - - val buffer = Buffer() - runBlocking { - val fakeTimeMark = FakeTimeMark() - val encoder = TraceEncoder(this, start = fakeTimeMark) { buffer } - val logger = encoder.createLogger("process", "thread") - - fakeTimeMark.now = 1L - logger.log(firstBatch) - - fakeTimeMark.now = 2L - logger.log(secondBatch) - - encoder.close() - } - val serialized = buffer.readUtf8() - - val expectedValue = """ - [ - {"name":"process_name","ph":"M","ts":0,"pid":0,"tid":0,"args":{"name":"process"}}, - {"name":"thread_name","ph":"M","ts":0,"pid":0,"tid":0,"args":{"name":"thread"}}, - {"name":"one","ph":"i","ts":1,"pid":0,"tid":0,"s":"t","args":{}}, - {"name":"two","ph":"i","ts":1,"pid":0,"tid":0,"s":"t","args":{}}, - {"name":"three","ph":"i","ts":2,"pid":0,"tid":0,"s":"t","args":{}}, - - """.trimIndent() - - assertEquals(expectedValue, serialized) - } - - private fun traceEvent(name: String) = Instant(name = name) -} diff --git a/workflow-core/api/workflow-core.api b/workflow-core/api/workflow-core.api index d6f2a627c4..368dd6fd66 100644 --- a/workflow-core/api/workflow-core.api +++ b/workflow-core/api/workflow-core.api @@ -371,11 +371,13 @@ public final class com/squareup/workflow1/TypedWorker : com/squareup/workflow1/W public abstract interface class com/squareup/workflow1/Worker { public static final field Companion Lcom/squareup/workflow1/Worker$Companion; + public static final field WORKER_OUTPUT_ACTION_NAME Ljava/lang/String; public abstract fun doesSameWorkAs (Lcom/squareup/workflow1/Worker;)Z public abstract fun run ()Lkotlinx/coroutines/flow/Flow; } public final class com/squareup/workflow1/Worker$Companion { + public static final field WORKER_OUTPUT_ACTION_NAME Ljava/lang/String; public final fun finished ()Lcom/squareup/workflow1/Worker; public final fun timer (JLjava/lang/String;)Lcom/squareup/workflow1/Worker; public static synthetic fun timer$default (Lcom/squareup/workflow1/Worker$Companion;JLjava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/Worker; diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Worker.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Worker.kt index f9c2f394e8..ef303f4744 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Worker.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/Worker.kt @@ -166,6 +166,8 @@ public interface Worker { public companion object { + public const val WORKER_OUTPUT_ACTION_NAME: String = "EmitWorkerOutputAction" + /** * Shorthand for `flow { block() }.asWorker()`. * diff --git a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt index 64c2cd0122..e7af16828e 100644 --- a/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt +++ b/workflow-core/src/commonMain/kotlin/com/squareup/workflow1/WorkerWorkflow.kt @@ -3,6 +3,7 @@ package com.squareup.workflow1 +import com.squareup.workflow1.Worker.Companion.WORKER_OUTPUT_ACTION_NAME import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext @@ -95,7 +96,7 @@ private class EmitWorkerOutputAction( private val output: O, ) : WorkflowAction() { override val debuggingName: String = - "EmitWorkerOutputAction(worker=$worker, key=$renderKey)" + "$WORKER_OUTPUT_ACTION_NAME(worker=$worker, key=$renderKey)" override fun Updater.apply() { setOutput(output) diff --git a/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt index 41dfba87f7..a2449b15d7 100644 --- a/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt +++ b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/AndroidDispatchersRenderWorkflowInTest.kt @@ -374,7 +374,7 @@ class AndroidDispatchersRenderWorkflowInTest( private var renderPasses = 0 private val countingInterceptor = object : WorkflowInterceptor { override fun onRuntimeUpdate(update: RuntimeUpdate) { - if (update is RenderingProduced<*>) { + if (update is RenderingProduced) { renderingsProduced++ } } diff --git a/workflow-runtime/api/workflow-runtime.api b/workflow-runtime/api/workflow-runtime.api index eca0f18e41..137088108e 100644 --- a/workflow-runtime/api/workflow-runtime.api +++ b/workflow-runtime/api/workflow-runtime.api @@ -102,12 +102,15 @@ public final class com/squareup/workflow1/WorkflowInterceptor$RenderingConflated } public final class com/squareup/workflow1/WorkflowInterceptor$RenderingProduced : com/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate { - public fun (Lcom/squareup/workflow1/RenderingAndSnapshot;)V - public final fun component1 ()Lcom/squareup/workflow1/RenderingAndSnapshot; - public final fun copy (Lcom/squareup/workflow1/RenderingAndSnapshot;)Lcom/squareup/workflow1/WorkflowInterceptor$RenderingProduced; - public static synthetic fun copy$default (Lcom/squareup/workflow1/WorkflowInterceptor$RenderingProduced;Lcom/squareup/workflow1/RenderingAndSnapshot;ILjava/lang/Object;)Lcom/squareup/workflow1/WorkflowInterceptor$RenderingProduced; + public static final field INSTANCE Lcom/squareup/workflow1/WorkflowInterceptor$RenderingProduced; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/WorkflowInterceptor$RuntimeLoopTick : com/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate { + public static final field INSTANCE Lcom/squareup/workflow1/WorkflowInterceptor$RuntimeLoopTick; public fun equals (Ljava/lang/Object;)Z - public final fun getRenderingAndSnapshot ()Lcom/squareup/workflow1/RenderingAndSnapshot; public fun hashCode ()I public fun toString ()Ljava/lang/String; } diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt index e1ae5b2e56..ceeb0e6228 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/RenderWorkflow.kt @@ -6,6 +6,7 @@ import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGE import com.squareup.workflow1.WorkflowInterceptor.RenderPassSkipped import com.squareup.workflow1.WorkflowInterceptor.RenderingConflated import com.squareup.workflow1.WorkflowInterceptor.RenderingProduced +import com.squareup.workflow1.WorkflowInterceptor.RuntimeLoopTick import com.squareup.workflow1.internal.WorkStealingDispatcher import com.squareup.workflow1.internal.WorkflowRunner import com.squareup.workflow1.internal.chained @@ -174,7 +175,8 @@ public fun renderWorkflowIn( val renderingsAndSnapshots = MutableStateFlow( try { runner.nextRendering().also { - chainedInterceptor.onRuntimeUpdate(RenderingProduced(it)) + chainedInterceptor.onRuntimeUpdate(RenderingProduced) + chainedInterceptor.onRuntimeUpdate(RuntimeLoopTick) } } catch (e: Throwable) { // If any part of the workflow runtime fails, the scope should be cancelled. We're not in a @@ -237,6 +239,7 @@ public fun renderWorkflowIn( if (shouldShortCircuitForUnchangedState(actionResult)) { chainedInterceptor.onRuntimeUpdate(RenderPassSkipped) + chainedInterceptor.onRuntimeUpdate(RuntimeLoopTick) sendOutput(actionResult, onOutput) continue@outer } @@ -292,6 +295,7 @@ public fun renderWorkflowIn( // in case it is the last update! break@conflate } + chainedInterceptor.onRuntimeUpdate(RuntimeLoopTick) sendOutput(actionResult, onOutput) continue@outer } @@ -304,8 +308,9 @@ public fun renderWorkflowIn( // Pass on the rendering to the UI. renderingsAndSnapshots.value = nextRenderAndSnapshot.also { - chainedInterceptor.onRuntimeUpdate(RenderingProduced(it)) + chainedInterceptor.onRuntimeUpdate(RenderingProduced) } + chainedInterceptor.onRuntimeUpdate(RuntimeLoopTick) // Emit the Output sendOutput(actionResult, onOutput) diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt index 435e2c8667..c6c1685787 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/WorkflowInterceptor.kt @@ -181,13 +181,14 @@ public interface WorkflowInterceptor { /** * This runtime has produced a new rendering after at least one render pass. - * - * @param renderingAndSnapshot This is the rendering and snapshot that was passed out of the - * Workflow runtime. */ - public data class RenderingProduced( - public val renderingAndSnapshot: RenderingAndSnapshot - ) : RuntimeUpdate + public data object RenderingProduced : RuntimeUpdate + + /** + * The runtime has finished its work and is stable again - either skipping rendering because + * of no change ([RenderPassSkipped]), or having passed a new rendering ([RenderingProduced]). + */ + public data object RuntimeLoopTick : RuntimeUpdate /** * Information about the session of a workflow in the runtime that a [WorkflowInterceptor] method diff --git a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt index aa9c05f9d7..20a46b2558 100644 --- a/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt +++ b/workflow-runtime/src/commonTest/kotlin/com/squareup/workflow1/RenderWorkflowInTest.kt @@ -114,8 +114,7 @@ class RenderWorkflowInTest( val hasReportedRendering = Mutex(locked = true) val testInterceptor = object : WorkflowInterceptor { override fun onRuntimeUpdate(update: RuntimeUpdate) { - if (update is RenderingProduced<*>) { - assertEquals("props: foo", update.renderingAndSnapshot.rendering) + if (update is RenderingProduced) { hasReportedRendering.unlock() } } @@ -131,35 +130,6 @@ class RenderWorkflowInTest( hasReportedRendering.lock() } - @Test fun modified_rendering_is_returned() = runTest(dispatcherUsed) { - val props = MutableStateFlow("foo") - val workflow = Workflow.stateless { "props: $it" } - - val interceptedRenderings = mutableListOf() - val testInterceptor = object : WorkflowInterceptor { - override fun onRuntimeUpdate(update: RuntimeUpdate) { - if (update is RenderingProduced<*>) { - interceptedRenderings.add(update.renderingAndSnapshot.rendering) - } - } - } - - renderWorkflowIn( - workflow = workflow, - scope = backgroundScope, - props = props, - interceptors = listOf(testInterceptor), - runtimeConfig = runtimeConfig, - workflowTracer = testTracer, - ) {} - assertEquals(1, interceptedRenderings.size, "Should have intercepted 1 rendering.") - assertEquals( - "props: foo", - interceptedRenderings[0], - "Should intercept 'props: foo' as a rendering." - ) - } - @Test fun initial_rendering_is_calculated_when_scope_cancelled_before_start() = runTest(dispatcherUsed) { val props = MutableStateFlow("foo") @@ -252,11 +222,11 @@ class RenderWorkflowInTest( val props = MutableStateFlow("foo") val workflow = Workflow.stateless { "props: $it" } - val interceptedRenderings = mutableListOf() + var interceptedRenderingsCount = 0 val testInterceptor = object : WorkflowInterceptor { override fun onRuntimeUpdate(update: RuntimeUpdate) { - if (update is RenderingProduced<*>) { - interceptedRenderings.add(update.renderingAndSnapshot.rendering) + if (update is RenderingProduced) { + interceptedRenderingsCount++ } } } @@ -271,22 +241,12 @@ class RenderWorkflowInTest( ) {} advanceIfStandard() - assertEquals(1, interceptedRenderings.size, "Should have intercepted 1 rendering.") - assertEquals( - "props: foo", - interceptedRenderings[0], - "Should intercept 'props: foo' as a rendering." - ) + assertEquals(1, interceptedRenderingsCount, "Should have intercepted 1 rendering.") props.value = "bar" advanceIfStandard() - assertEquals(2, interceptedRenderings.size, "Should have intercepted 2 rendering.") - assertEquals( - "props: bar", - interceptedRenderings[1], - "Should intercept 'props: bar' as a rendering." - ) + assertEquals(2, interceptedRenderingsCount, "Should have intercepted 2 rendering.") } @Test fun saves_to_and_restores_from_snapshot( @@ -1121,12 +1081,12 @@ class RenderWorkflowInTest( runTest(dispatcherUsed) { check(runtimeConfig.contains(RENDER_ONLY_WHEN_STATE_CHANGES)) lateinit var sink: Sink - val interceptedRenderings = mutableListOf() + var interceptedRenderingsCount = 0 var skippedRenderings = 0 val testInterceptor = object : WorkflowInterceptor { override fun onRuntimeUpdate(update: RuntimeUpdate) { - if (update is RenderingProduced<*>) { - interceptedRenderings.add(update.renderingAndSnapshot.rendering) + if (update is RenderingProduced) { + interceptedRenderingsCount++ } else if (update is RenderPassSkipped) { skippedRenderings++ } @@ -1160,7 +1120,7 @@ class RenderWorkflowInTest( collectionJob.cancel() assertEquals(1, emitted.size) - assertEquals(1, interceptedRenderings.size) + assertEquals(1, interceptedRenderingsCount) assertEquals(1, skippedRenderings) } } diff --git a/workflow-tracing-papa/README.md b/workflow-tracing-papa/README.md new file mode 100644 index 0000000000..e9ce8987c6 --- /dev/null +++ b/workflow-tracing-papa/README.md @@ -0,0 +1,7 @@ +# Module workflow-tracing-papa + +This module provides the [com.squareup.workflow1.WorkflowRuntimeMonitor] which is a special +[com.squareup.workflow1.WorkflowRuntimeInterceptor] that can be used to monitor an entire runtime +on Android with bookkeeping about the workflow sessions that enables intelligent tracing of all +Workflows and the runtime. Those can be added optionally by passing in custom +[com.squareup.workflow.WorkflowRuntimeTracer]s. diff --git a/workflow-tracing-papa/api/workflow-tracing-papa.api b/workflow-tracing-papa/api/workflow-tracing-papa.api new file mode 100644 index 0000000000..30ff3d24c2 --- /dev/null +++ b/workflow-tracing-papa/api/workflow-tracing-papa.api @@ -0,0 +1,30 @@ +public final class com/squareup/workflow1/tracing/papa/PapaSafeTrace : com/squareup/workflow1/tracing/SafeTraceInterface { + public fun ()V + public fun beginAsyncSection (Ljava/lang/String;I)V + public fun beginSection (Ljava/lang/String;)V + public fun endAsyncSection (Ljava/lang/String;I)V + public fun endSection ()V + public fun isCurrentlyTracing ()Z + public fun isTraceable ()Z + public fun logSection (Ljava/lang/String;)V +} + +public final class com/squareup/workflow1/tracing/papa/WorkflowPapaTracer : com/squareup/workflow1/tracing/WorkflowRuntimeTracer { + public static final field Companion Lcom/squareup/workflow1/tracing/papa/WorkflowPapaTracer$Companion; + public fun ()V + public fun (Lcom/squareup/workflow1/tracing/SafeTraceInterface;)V + public synthetic fun (Lcom/squareup/workflow1/tracing/SafeTraceInterface;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onRenderAndSnapshot (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot; + public fun onRootPropsChanged (Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public fun onRuntimeUpdateEnhanced (Lcom/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate;ZLcom/squareup/workflow1/tracing/ConfigSnapshot;)V + public fun onSnapshotStateWithChildren (Lkotlin/jvm/functions/Function0;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/TreeSnapshot; + public fun onWorkflowSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public fun onWorkflowSessionStopped (J)V +} + +public final class com/squareup/workflow1/tracing/papa/WorkflowPapaTracer$Companion { +} + diff --git a/workflow-tracing-papa/build.gradle.kts b/workflow-tracing-papa/build.gradle.kts new file mode 100644 index 0000000000..6ab4464973 --- /dev/null +++ b/workflow-tracing-papa/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + id("com.android.library") + id("kotlin-android") + id("android-defaults") + id("published") +} + +android { + namespace = "com.squareup.workflow1.tracing.papa" +} + +dependencies { + api(libs.androidx.collection) + api(libs.kotlin.jdk8) + api(libs.kotlinx.coroutines.core) + api(libs.squareup.papa) + + api(project(":workflow-core")) + api(project(":workflow-runtime")) + api(project(":workflow-tracing")) + + testImplementation(project(":workflow-config:config-jvm")) + testImplementation(libs.junit) + testImplementation(libs.kotlin.test.core) + testImplementation(libs.kotlin.test.jdk) + testImplementation(libs.kotlinx.coroutines.test) +} diff --git a/workflow-tracing-papa/dependencies/releaseRuntimeClasspath.txt b/workflow-tracing-papa/dependencies/releaseRuntimeClasspath.txt new file mode 100644 index 0000000000..f82db811c0 --- /dev/null +++ b/workflow-tracing-papa/dependencies/releaseRuntimeClasspath.txt @@ -0,0 +1,27 @@ +androidx.annotation:annotation-experimental:1.1.0 +androidx.annotation:annotation-jvm:1.9.1 +androidx.annotation:annotation:1.9.1 +androidx.arch.core:core-common:2.0.0 +androidx.collection:collection-jvm:1.5.0 +androidx.collection:collection:1.5.0 +androidx.core:core:1.6.0 +androidx.lifecycle:lifecycle-common:2.0.0 +androidx.lifecycle:lifecycle-runtime:2.0.0 +androidx.tracing:tracing-ktx:1.1.0 +androidx.tracing:tracing:1.1.0 +androidx.versionedparcelable:versionedparcelable:1.1.1 +com.squareup.curtains:curtains:1.2.5 +com.squareup.okio:okio-jvm:3.3.0 +com.squareup.okio:okio:3.3.0 +com.squareup.papa:papa-main-trace:0.30 +com.squareup.papa:papa-safetrace:0.30 +com.squareup.papa:papa:0.30 +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.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 diff --git a/workflow-tracing-papa/gradle.properties b/workflow-tracing-papa/gradle.properties new file mode 100644 index 0000000000..43800567f8 --- /dev/null +++ b/workflow-tracing-papa/gradle.properties @@ -0,0 +1,3 @@ +POM_ARTIFACT_ID=workflow-tracing-papa +POM_NAME=Workflow Tracing Papa +POM_PACKAGING=aar diff --git a/workflow-tracing-papa/src/main/java/com/squareup/workflow1/tracing/papa/PapaSafeTrace.kt b/workflow-tracing-papa/src/main/java/com/squareup/workflow1/tracing/papa/PapaSafeTrace.kt new file mode 100644 index 0000000000..a8074b9862 --- /dev/null +++ b/workflow-tracing-papa/src/main/java/com/squareup/workflow1/tracing/papa/PapaSafeTrace.kt @@ -0,0 +1,41 @@ +package com.squareup.workflow1.tracing.papa + +import com.squareup.workflow1.tracing.SafeTraceInterface +import papa.SafeTrace + +/** + * Production implementation of [SafeTraceInterface] that delegates to the actual [SafeTrace]. + */ +class PapaSafeTrace : SafeTraceInterface { + override val isTraceable: Boolean + get() = SafeTrace.isTraceable + + override val isCurrentlyTracing: Boolean + get() = SafeTrace.isCurrentlyTracing + + override fun beginSection(label: String) { + SafeTrace.beginSection(label) + } + + override fun endSection() { + SafeTrace.endSection() + } + + override fun beginAsyncSection( + name: String, + cookie: Int + ) { + SafeTrace.beginAsyncSection(name, cookie) + } + + override fun endAsyncSection( + name: String, + cookie: Int + ) { + SafeTrace.endAsyncSection(name, cookie) + } + + override fun logSection(info: String) { + SafeTrace.logSection(info) + } +} diff --git a/workflow-tracing-papa/src/main/java/com/squareup/workflow1/tracing/papa/WorkflowPapaTracer.kt b/workflow-tracing-papa/src/main/java/com/squareup/workflow1/tracing/papa/WorkflowPapaTracer.kt new file mode 100644 index 0000000000..eb3877cec4 --- /dev/null +++ b/workflow-tracing-papa/src/main/java/com/squareup/workflow1/tracing/papa/WorkflowPapaTracer.kt @@ -0,0 +1,489 @@ +package com.squareup.workflow1.tracing.papa + +import androidx.collection.mutableLongObjectMapOf +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.RenderingAndSnapshot +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.Worker +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor +import com.squareup.workflow1.WorkflowInterceptor.RenderPassSkipped +import com.squareup.workflow1.WorkflowInterceptor.RuntimeLoopTick +import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.applyTo +import com.squareup.workflow1.tracing.ConfigSnapshot +import com.squareup.workflow1.tracing.SafeTraceInterface +import com.squareup.workflow1.tracing.WorkflowRuntimeMonitor.ActionType +import com.squareup.workflow1.tracing.WorkflowRuntimeMonitor.ActionType.CascadeAction +import com.squareup.workflow1.tracing.WorkflowRuntimeMonitor.ActionType.QueuedAction +import com.squareup.workflow1.tracing.WorkflowRuntimeTracer +import com.squareup.workflow1.tracing.getWfLogString +import com.squareup.workflow1.tracing.toLoggingShortName +import com.squareup.workflow1.tracing.workerKey +import kotlinx.coroutines.CoroutineScope +import kotlin.reflect.KType + +/** + * [WorkflowRuntimeTracer] plugin to add [SafeTraceInterface] traces. + * By default this uses [PapaSafeTrace] which will use [androidx.tracing.Trace] calls that + * will be received by the system and included in Perfetto traces. + */ +class WorkflowPapaTracer( + private val safeTrace: SafeTraceInterface = PapaSafeTrace() +) : WorkflowRuntimeTracer() { + + private data class NameAndCookie( + val name: String, + val cookie: Int + ) + + private class SystemTraceState { + var renderPassCount = 0 + + // Some render passes are skipped if they have no state change. Count all triggers separately. + var renderPassTriggerCount = 0 + val workflowAsyncSections = mutableLongObjectMapOf() + val workflowShortNamesById = mutableLongObjectMapOf() + } + + private val systemTraceState = if (safeTrace.isTraceable) { + SystemTraceState() + } else { + null + } + + private val isSystemTraceable: Boolean + get() = systemTraceState != null + + private val isCurrentlySystemTracing: Boolean + get() = systemTraceState != null && safeTrace.isCurrentlyTracing + + /** + * If the build is traceable but we're not currently tracing, reset so that we start at 0 in + * new traces. + */ + private fun SystemTraceState.resetTraceCountsIfNotTracing() { + if (!safeTrace.isCurrentlyTracing) { + renderPassCount = 0 + renderPassTriggerCount = 0 + actionIndex = 0 + effectIndex = 0 + } + } + + private fun renderPassNumber(): String { + return ":${systemTraceState?.renderPassTriggerCount ?: 0}:" + } + + /** + * Log a Perfetto trace section that is simply meta-data that we use in post-processing. + * + * Format for labels in these sections: + * - "W:" is for a Workflow + * - "R:" is for a Worker + * - "A:" is for an Action + */ + private fun infoSection(info: String) { + safeTrace.logSection(info) + } + + /** SECTION: [WorkflowRuntimeTracer] specific methods. **/ + + override fun onWorkflowSessionStarted( + workflowScope: CoroutineScope, + session: WorkflowSession + ) { + systemTraceState?.let { + val sessionId = session.sessionId + // We are tracing, so set up some initial components for this workflow. + val shortName = "WKF$sessionId ${session.name}" + val nameWithKey = "WKF$sessionId ${session.logName}" + + val parentPart = session.parent?.let { parentSession -> + " parent:${parentSession.traceName}" + } ?: "" + val asyncSectionName = "$nameWithKey$parentPart" + + val atraceCookie = workflowRuntimeTraceContext.runtimeName.hashCode() * sessionId.toInt() + it.workflowAsyncSections[sessionId] = NameAndCookie(asyncSectionName, atraceCookie) + it.workflowShortNamesById[sessionId] = shortName + + safeTrace.beginAsyncSection(asyncSectionName, atraceCookie) + + // Reason for render pass if we are the root. + if (session.isRootWorkflow) { + // This could be the first thing that happens after a trace has finished, so check if we need + // to reset it here. + it.resetTraceCountsIfNotTracing() + it.renderPassTriggerCount++ + safeTrace.beginSection( + "CREATE_RENDER${it.renderPassTriggerCount}, Runner:$workflowRuntimeTraceContext.runtimeName" + ) + // These are both short, but CAUSE: is long in the regular case, so leave it as a separate + // info section. + infoSection("SUM:${it.renderPassTriggerCount}: Skipped:N, StateChange:Y") + infoSection("CAUSE:${it.renderPassTriggerCount}: RootWFCreated:W($${session.name})") + } + } + } + + override fun onWorkflowSessionStopped( + sessionId: Long + ) { + systemTraceState?.let { + val asyncSection = it.workflowAsyncSections.remove(sessionId) + // TODO (RF-9493) Investigate asyncSection being null instead of ignoring the problem + if (asyncSection != null) { + safeTrace.endAsyncSection(asyncSection.name, asyncSection.cookie) + } + } + } + + override fun onRuntimeUpdateEnhanced( + runtimeUpdate: RuntimeUpdate, + currentActionHandlingChangedState: Boolean, + configSnapshot: ConfigSnapshot + ) { + if (!isSystemTraceable) return + if (runtimeUpdate == RenderPassSkipped) { + // Helps understanding traces. + infoSection("CAUSE${renderPassNumber()} ${workflowRuntimeTraceContext.previousRenderCause}") + // Skipping, end the section started when renderIncomingCause was set. + safeTrace.endSection() + } + if (runtimeUpdate == RuntimeLoopTick) { + // Build and add the summary! + val summary = buildString { + append("SUM${renderPassNumber()} ") + append("Config:") + if (configSnapshot.shortCircuitConfig) { + append("ROWSC, ") + } + if (configSnapshot.csrConfig) { + append("CSR, ") + } + if (configSnapshot.ptrConfig) { + append("PTR, ") + } + if (!configSnapshot.shortCircuitConfig && + !configSnapshot.csrConfig && + !configSnapshot.ptrConfig + ) { + append("Base, ") + } + append("StateChange:") + if (currentActionHandlingChangedState) { + append("Y, ") + } else { + append("N, ") + } + } + infoSection(summary) + } + } + + override fun onRootPropsChanged(session: WorkflowSession) { + if (systemTraceState != null) { + safeTrace.beginSection( + "PROPS_RENDER${++systemTraceState.renderPassTriggerCount}, Runner:${workflowRuntimeTraceContext.runtimeName}" + ) + infoSection("SUM:${systemTraceState.renderPassTriggerCount}: Skipped:N, StateChange:Y") + infoSection("CAUSE:${systemTraceState.renderPassTriggerCount}: RootWFProps:${session.name}") + } + } + + /** END SECTION: [WorkflowRuntimeTracer] specific methods. **/ + + /** SECTION: [WorkflowInterceptor] override methods. **/ + + override fun onInitialState( + props: P, + snapshot: Snapshot?, + workflowScope: CoroutineScope, + proceed: (P, Snapshot?, CoroutineScope) -> S, + session: WorkflowSession + ): S { + return trace( + systemTraceLabel = { "InitialState ${session.traceName}" }, + ) { + proceed(props, snapshot, workflowScope) + } + } + + override fun onPropsChanged( + old: P, + new: P, + state: S, + proceed: (P, P, S) -> S, + session: WorkflowSession + ): S { + return trace( + systemTraceLabel = { "PropsChanged ${session.traceName}" }, + ) { + proceed(old, new, state) + } + } + + override fun onRenderAndSnapshot( + renderProps: P, + proceed: (P) -> RenderingAndSnapshot, + session: WorkflowSession + ): RenderingAndSnapshot { + systemTraceState?.resetTraceCountsIfNotTracing() + return trace( + systemTraceLabel = { + "RENDER${++systemTraceState!!.renderPassCount}" + + " ${workflowRuntimeTraceContext.runtimeName}" + }, + ) { + proceed(renderProps).also { + if (systemTraceState != null) { + // Ends the section that's always started right when a [QueuedAction] is applied. + safeTrace.endSection() + } + } + } + } + + override fun onRender( + renderProps: P, + renderState: S, + context: BaseRenderContext, + proceed: (P, S, RenderContextInterceptor?) -> R, + session: WorkflowSession + ): R { + val workflowName = session.traceName + return trace( + systemTraceLabel = { "Render $workflowName" } + ) { + proceed( + renderProps, + renderState, + PapaRenderContextInterceptor( + isRoot = session.isRootWorkflow, + workflowName = workflowName + ) + ) + } + } + + override fun onSnapshotStateWithChildren( + proceed: () -> TreeSnapshot, + session: WorkflowSession + ): TreeSnapshot { + return trace( + systemTraceLabel = { "Snapshot ${workflowRuntimeTraceContext.runtimeName}" } + ) { + proceed() + } + } + + /** END SECTION: [WorkflowInterceptor] override methods. **/ + + /** + * [RenderContextInterceptor] that adds Perfetto tracing through Papa. + */ + private inner class PapaRenderContextInterceptor( + private val isRoot: Boolean, + private val workflowName: String + ) : RenderContextInterceptor { + + override fun onActionSent( + action: WorkflowAction, + proceed: (WorkflowAction) -> Unit + ) { + val actionName = action.toLoggingShortName() + val actionIndexLabel = "ACT${actionIndex++}" + val traceActionName = if (isSystemTraceable) { + "$actionIndexLabel A(${actionName.ifBlank { "" }})/W($workflowName)" + } else { + null + } + val queuedActionDetails = QueuedAction + trace( + systemTraceLabel = { "Send $traceActionName}" } + ) { + proceed( + PerfettoTraceWorkflowAction( + delegateAction = action, + actionName = actionName, + actionType = queuedActionDetails, + actionIndex = actionIndexLabel, + ) + ) + } + } + + override fun onRunningSideEffect( + key: String, + sideEffect: suspend () -> Unit, + proceed: (key: String, sideEffect: suspend () -> Unit) -> Unit + ) { + val label = if (isSystemTraceable) { + "EFF${effectIndex++} Key[$key]" + } else { + null + } + trace( + systemTraceLabel = { "SideEffect $label" } + ) { + proceed(key, sideEffect) + } + } + + override fun onRenderChild( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction, + proceed: ( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction + ) -> CR + ): CR { + // onRenderChild is not traced (the child's own render will be traced), + // but we trace the action handler. + return proceed(child, childProps, key) { output -> + val childOutputString = getWfLogString(output) + trace( + systemTraceLabel = { "Send Output[$childOutputString] to $workflowName" } + ) { + val delegateAction = handler(output) + val actionName = delegateAction.toLoggingShortName() + PerfettoTraceWorkflowAction( + delegateAction = delegateAction, + actionName = actionName, + actionType = CascadeAction( + childOutputString = childOutputString + ), + ) + } + } + } + + override fun onRemember( + key: String, + resultType: KType, + inputs: Array, + calculation: () -> CResult, + proceed: ( + key: String, + resultType: KType, + inputs: Array, + calculation: () -> CResult + ) -> CResult + ): CResult { + return trace( + systemTraceLabel = { "Remember $key" } + ) { + proceed(key, resultType, inputs, calculation) + } + } + + /** + * Class to trace the application of actions. + */ + private inner class PerfettoTraceWorkflowAction( + private val delegateAction: WorkflowAction, + private val actionName: String, + private val actionType: ActionType, + private val actionIndex: String? = null + ) : WorkflowAction() { + // Forward debugging name so we do not include anything about this tracing action. + override val debuggingName: String + get() = delegateAction.debuggingName + + /** + * Trace application of the action. + */ + override fun Updater.apply() { + // See https://github.com/square/workflow-kotlin/issues/391. We have to listen to the 2nd + // action in the cascade to get a useful ref on which Worker's handler was firing. This is + // because Workers use an underlying Workflow and an intermediate action, which is the + // QueuedAction, in their implementation. So yes, we use an implementation detail here to + // detect that. The issue still tracks upstreaming this into the library. + val isWorkerQueuedAction = actionName.contains(Worker.WORKER_OUTPUT_ACTION_NAME) + if (actionType is QueuedAction) { + if (isSystemTraceable) { + safeTrace.beginSection( + "MAYBE_RENDER${++systemTraceState!!.renderPassTriggerCount}:" + + " $actionIndex," + + " Runner:${workflowRuntimeTraceContext.runtimeName}" + ) + } + } + val (_, actionApplied) = trace( + systemTraceLabel = { + val actionNameOrBlank = actionName.ifBlank { "" } + val queuedApplyName = if (isWorkerQueuedAction) { + "$workflowName(key=${actionNameOrBlank.workerKey()})" + } else { + actionNameOrBlank + } + if (actionType is CascadeAction) { + "CascadeApply:$queuedApplyName," + + " Cause:${workflowRuntimeTraceContext.renderIncomingCauses.lastOrNull()}" + } else { + "QueuedApply:$queuedApplyName" + } + }, + ) { + delegateAction.applyTo(props, state).also { (newState, actionApplied) -> + state = newState + actionApplied.output?.let { setOutput(it.value) } + } + } + + if (isRoot || actionApplied.output == null) { + // This action's application is ending a cascade, let's sum up what happened + sumUpActionCascade() + } + } + + private fun sumUpActionCascade() { + if (isSystemTraceable) { + val causeLabel = buildString { + append("CAUSE${renderPassNumber()} ") + if (workflowRuntimeTraceContext.renderIncomingCauses.isEmpty()) { + append("Unknown") + } else { + append(workflowRuntimeTraceContext.renderIncomingCauses.last()) + } + } + infoSection(causeLabel) + } + } + } + } + + /** + * This method is inlined, so that when tracing is disabled there's no additional lambda creation. + */ + private inline fun trace( + crossinline systemTraceLabel: () -> String, + crossinline block: () -> T + ): T { + val systemTrace = isCurrentlySystemTracing + if (systemTrace) { + safeTrace.beginSection(systemTraceLabel()) + } + try { + return block() + } finally { + if (systemTrace) { + safeTrace.endSection() + } + } + } + + companion object { + // Ensure index is unique across all workflow runtimes + private var actionIndex = 0 + private var effectIndex = 0 + } +} diff --git a/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/FakeSafeTrace.kt b/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/FakeSafeTrace.kt new file mode 100644 index 0000000000..7ca6ae2c9c --- /dev/null +++ b/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/FakeSafeTrace.kt @@ -0,0 +1,53 @@ +package com.squareup.workflow1.tracing.papa + +import com.squareup.workflow1.tracing.SafeTraceInterface + +/** + * Fake implementation of [SafeTraceInterface] for testing purposes. + * Records all trace calls for verification in tests. + */ +class FakeSafeTrace( + override val isTraceable: Boolean = true, + override val isCurrentlyTracing: Boolean = true +) : SafeTraceInterface { + + data class TraceCall( + val type: String, + val label: String? = null, + val name: String? = null, + val cookie: Int? = null + ) + + private val _traceCalls = mutableListOf() + val traceCalls: List get() = _traceCalls.toList() + + fun clearTraceCalls() { + _traceCalls.clear() + } + + override fun beginSection(label: String) { + _traceCalls.add(TraceCall("beginSection", label = label)) + } + + override fun endSection() { + _traceCalls.add(TraceCall("endSection")) + } + + override fun beginAsyncSection( + name: String, + cookie: Int + ) { + _traceCalls.add(TraceCall("beginAsyncSection", name = name, cookie = cookie)) + } + + override fun endAsyncSection( + name: String, + cookie: Int + ) { + _traceCalls.add(TraceCall("endAsyncSection", name = name, cookie = cookie)) + } + + override fun logSection(info: String) { + _traceCalls.add(TraceCall("logSection", label = info)) + } +} diff --git a/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/WorkflowPapaTracerTest.kt b/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/WorkflowPapaTracerTest.kt new file mode 100644 index 0000000000..e0fdb5cc65 --- /dev/null +++ b/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/WorkflowPapaTracerTest.kt @@ -0,0 +1,283 @@ +package com.squareup.workflow1.tracing.papa + +import com.squareup.workflow1.RenderingAndSnapshot +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.RuntimeConfigOptions +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.WorkflowInterceptor.RenderPassSkipped +import com.squareup.workflow1.WorkflowInterceptor.RuntimeLoopTick +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.identifier +import com.squareup.workflow1.tracing.ConfigSnapshot +import com.squareup.workflow1.tracing.RenderCause +import com.squareup.workflow1.tracing.RuntimeTraceContext +import com.squareup.workflow1.tracing.RuntimeUpdateLogLine +import com.squareup.workflow1.tracing.WorkflowSessionInfo +import kotlinx.coroutines.test.TestScope +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +internal class WorkflowPapaTracerTest { + + private val fakeTrace = FakeSafeTrace() + private val papaTracer = WorkflowPapaTracer(fakeTrace) + + @Test + fun `onWorkflowSessionStarted creates async section for root workflow`() { + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Attach runtime context to tracer + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + // Add session info to the context as would normally be done by WorkflowRuntimeMonitor + testContext.workflowSessionInfo[rootSession.sessionId] = WorkflowSessionInfo(rootSession) + + papaTracer.onWorkflowSessionStarted(testScope, rootSession) + + val asyncSectionCalls = fakeTrace.traceCalls.filter { it.type == "beginAsyncSection" } + assertEquals(1, asyncSectionCalls.size) + assertTrue(asyncSectionCalls.first().name!!.contains("WKF1")) + } + + @Test + fun `onInitialState traces section`() { + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Attach runtime context to tracer + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + // Add session info to the context as would normally be done by WorkflowRuntimeMonitor + testContext.workflowSessionInfo[rootSession.sessionId] = WorkflowSessionInfo(rootSession) + + val result = papaTracer.onInitialState( + props = "testProps", + snapshot = null, + workflowScope = testScope, + proceed = { _, _, _ -> "initialState" }, + session = rootSession + ) + + assertEquals("initialState", result) + val beginSectionCalls = fakeTrace.traceCalls.filter { it.type == "beginSection" } + val endSectionCalls = fakeTrace.traceCalls.filter { it.type == "endSection" } + assertTrue(beginSectionCalls.any { it.label!!.contains("InitialState") }) + assertTrue(endSectionCalls.isNotEmpty()) + } + + @Test + fun `onRenderAndSnapshot traces render section`() { + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + + // Attach runtime context to tracer + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + val result = papaTracer.onRenderAndSnapshot( + renderProps = "props", + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + assertEquals(renderingAndSnapshot, result) + val beginSectionCalls = fakeTrace.traceCalls.filter { it.type == "beginSection" } + assertTrue(beginSectionCalls.any { it.label!!.contains("RENDER") }) + } + + @Test + fun `tracer can be instantiated`() { + assertNotNull(papaTracer) + } + + @Test + fun `onPropsChanged delegates to proceed function`() { + val testWorkflow = TestWorkflow() + val mockSession = testWorkflow.createMockSession() + + // Attach runtime context to tracer + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + // Add session info to the context as would normally be done by WorkflowRuntimeMonitor + testContext.workflowSessionInfo[mockSession.sessionId] = WorkflowSessionInfo(mockSession) + + val result = papaTracer.onPropsChanged( + old = "old", + new = "new", + state = "current", + proceed = { _, _, state -> state }, + session = mockSession + ) + + assertEquals("current", result) + } + + @Test + fun `onRenderAndSnapshot delegates to proceed function`() { + val testWorkflow = TestWorkflow() + val mockSession = testWorkflow.createMockSession() + + // Attach runtime context to tracer + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + val result = papaTracer.onRenderAndSnapshot( + renderProps = "props", + proceed = { renderingAndSnapshot }, + session = mockSession + ) + + assertEquals(renderingAndSnapshot, result) + } + + @Test + fun `onSnapshotStateWithChildren delegates to proceed function`() { + val testWorkflow = TestWorkflow() + val mockSession = testWorkflow.createMockSession() + + // Attach runtime context to tracer + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + val treeSnapshot = TreeSnapshot.forRootOnly(null) + + val result = papaTracer.onSnapshotStateWithChildren( + proceed = { treeSnapshot }, + session = mockSession + ) + + assertEquals(treeSnapshot, result) + } + + @Test + fun `onRuntimeUpdateEnhanced handles different runtime updates`() { + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + val configSnapshot = ConfigSnapshot(TestRuntimeConfig()) + + // Should not throw for RenderPassSkipped + papaTracer.onRuntimeUpdateEnhanced(RenderPassSkipped, false, configSnapshot) + + // Should not throw for RuntimeLoopTick + papaTracer.onRuntimeUpdateEnhanced(RuntimeLoopTick, true, configSnapshot) + } + + @Test + fun `workflow sessions can be started and stopped`() { + val testWorkflow = TestWorkflow() + val mockSession = testWorkflow.createMockSession() + + // Attach runtime context to tracer + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + // Add session info to the context as would normally be done by WorkflowRuntimeMonitor + testContext.workflowSessionInfo[mockSession.sessionId] = WorkflowSessionInfo(mockSession) + + // Should not throw + papaTracer.onWorkflowSessionStarted(TestScope(), mockSession) + papaTracer.onWorkflowSessionStopped(123L) + } + + @Test + fun `onRootPropsChanged completes without error`() { + val testWorkflow = TestWorkflow() + val mockSession = testWorkflow.createMockSession() + + // Attach runtime context to tracer + val testContext = TestRuntimeTraceContext() + papaTracer.attachRuntimeContext(testContext) + + // Add session info to the context as would normally be done by WorkflowRuntimeMonitor + testContext.workflowSessionInfo[mockSession.sessionId] = WorkflowSessionInfo(mockSession) + + // Should not throw + papaTracer.onRootPropsChanged(mockSession) + } + + private class TestWorkflow : StatefulWorkflow() { + + override fun initialState( + props: String, + snapshot: Snapshot? + ): String = props + + override fun render( + renderProps: String, + renderState: String, + context: RenderContext + ): String = renderState + + override fun snapshotState(state: String): Snapshot = Snapshot.of(state) + + fun createRootSession(): WorkflowSession = TestWorkflowSession( + workflow = this, + sessionId = 1L, + renderKey = "root", + parent = null + ) + + fun createMockSession() = TestWorkflowSession( + workflow = this, + sessionId = 123L, + renderKey = "test", + parent = null + ) + } + + private class TestWorkflowSession( + private val workflow: TestWorkflow, + override val sessionId: Long, + override val renderKey: String, + override val parent: WorkflowSession? + ) : WorkflowSession { + override val identifier = workflow.identifier + override val runtimeConfig = TestRuntimeConfig() + override val workflowTracer = null + } + + private class TestRuntimeConfig : RuntimeConfig { + override fun contains(element: RuntimeConfigOptions): Boolean = false + override val size: Int = 0 + override fun containsAll(elements: Collection): Boolean = false + override fun isEmpty(): Boolean = true + override fun iterator(): Iterator = + emptyList().iterator() + override fun toString(): String = "TestRuntimeConfig" + } + + private class TestRuntimeTraceContext : RuntimeTraceContext { + override val runtimeName: String = "TestRuntime" + override val workflowSessionInfo = + androidx.collection.mutableLongObjectMapOf() + override val renderIncomingCauses: MutableList = mutableListOf() + override var previousRenderCause: RenderCause? = null + override var currentRenderCause: RenderCause? = null + override lateinit var configSnapshot: ConfigSnapshot + + override fun addRuntimeUpdate(event: RuntimeUpdateLogLine) { + // No-op for testing + } + + init { + configSnapshot = ConfigSnapshot(TestRuntimeConfig()) + } + } +} diff --git a/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/WorkflowTracingIntegrationTest.kt b/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/WorkflowTracingIntegrationTest.kt new file mode 100644 index 0000000000..daf73084a3 --- /dev/null +++ b/workflow-tracing-papa/src/test/java/com/squareup/workflow1/tracing/papa/WorkflowTracingIntegrationTest.kt @@ -0,0 +1,520 @@ +package com.squareup.workflow1.tracing.papa + +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.action +import com.squareup.workflow1.asWorker +import com.squareup.workflow1.config.JvmTestRuntimeConfigTools +import com.squareup.workflow1.renderWorkflowIn +import com.squareup.workflow1.runningWorker +import com.squareup.workflow1.tracing.ConfigSnapshot +import com.squareup.workflow1.tracing.RuntimeUpdates +import com.squareup.workflow1.tracing.WorkflowRuntimeLoopListener +import com.squareup.workflow1.tracing.WorkflowRuntimeMonitor +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +/** + * Integration tests that verify end-to-end tracing functionality by using the real workflow + * runtime with renderWorkflowIn and WorkflowRuntimeMonitor as an interceptor. + */ +internal class WorkflowTracingIntegrationTest { + + private val runtimeName = "IntegrationTestRuntime" + private val runtimeConfig = JvmTestRuntimeConfigTools.getTestRuntimeConfig() + + @Test + fun `integration test - root workflow creation is fully traced`() = + runTest { + val runtimeLoopMutex = Mutex(locked = true) + val runtimeLoopListener = WorkflowRuntimeLoopListener { _, _ -> + runtimeLoopMutex.unlock() + } + val fakeTrace = FakeSafeTrace() + val papaTracer = WorkflowPapaTracer(fakeTrace) + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(papaTracer), + runtimeLoopListener = runtimeLoopListener, + ) + + val props = MutableStateFlow("initial") + val workflow = TestWorkflow() + + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(monitor), + runtimeConfig = runtimeConfig, + ) {} + + runtimeLoopMutex.lock() + + // Verify initial rendering + assertEquals("state: initial", renderings.value.rendering) + + // Verify comprehensive tracing occurred + val traceCalls = fakeTrace.traceCalls + assertTrue(traceCalls.isNotEmpty()) + + // Should have async section for workflow session + assertTrue(traceCalls.any { it.type == "beginAsyncSection" && it.name!!.contains("WKF") }) + + // Should have initial state tracing + assertTrue( + traceCalls.any { it.type == "beginSection" && it.label!!.contains("InitialState") } + ) + + // Should have render pass tracing + assertTrue(traceCalls.any { it.type == "beginSection" && it.label!!.contains("RENDER") }) + + // Should have root creation render tracing + assertTrue( + traceCalls.any { it.type == "beginSection" && it.label!!.contains("CREATE_RENDER") } + ) + + // Should have matching end sections + val beginSections = traceCalls.count { it.type == "beginSection" } + val endSections = traceCalls.count { it.type == "endSection" } + assertEquals( + beginSections, + endSections, + "All begin sections should have matching end sections" + ) + } + + @Test + fun `integration test - props change is fully traced`() = runTest { + + val runtimeLoopMutex = Mutex(locked = true) + val runtimeLoopListener = WorkflowRuntimeLoopListener { _, _ -> + runtimeLoopMutex.unlock() + } + val fakeTrace = FakeSafeTrace() + val papaTracer = WorkflowPapaTracer(fakeTrace) + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(papaTracer), + runtimeLoopListener = runtimeLoopListener, + ) + + val props = MutableStateFlow("initial") + val workflow = TestWorkflow() + + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(monitor), + runtimeConfig = runtimeConfig, + ) {} + + // Wait for the lock (runtime loop complete). + runtimeLoopMutex.lock() + + assertEquals("state: initial", renderings.value.rendering) + + fakeTrace.clearTraceCalls() + + // Change props to trigger new render + props.value = "updated" + + // Wait for the lock (runtime loop complete). + runtimeLoopMutex.lock() + + assertEquals("state: updated", renderings.value.rendering) + + val traceCalls = fakeTrace.traceCalls + + // Should have props change render tracing + assertTrue(traceCalls.any { it.type == "beginSection" && it.label!!.contains("PROPS_RENDER") }) + + // Should have render pass tracing + assertTrue(traceCalls.any { it.type == "beginSection" && it.label!!.contains("RENDER") }) + + // Should have info sections for props change + assertTrue(traceCalls.any { it.type == "logSection" && it.label!!.contains("RootWFProps") }) + } + + @Test + fun `integration test - action-triggered render is fully traced`() = + runTest { + val runtimeLoopMutex = Mutex(locked = true) + val runtimeLoopListener = WorkflowRuntimeLoopListener { _, _ -> + runtimeLoopMutex.unlock() + } + val fakeTrace = FakeSafeTrace() + val papaTracer = WorkflowPapaTracer(fakeTrace) + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(papaTracer), + runtimeLoopListener = runtimeLoopListener, + ) + + val props = MutableStateFlow("initial") + val workflow = InteractiveTestWorkflow() + + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(monitor), + runtimeConfig = runtimeConfig, + ) {} + + runtimeLoopMutex.lock() + + val initialRendering = renderings.value.rendering + assertEquals("state: initial", initialRendering.text) + + fakeTrace.clearTraceCalls() + + // Trigger an action by calling the callback + initialRendering.onAction("action-triggered") + runtimeLoopMutex.lock() + + val updatedRendering = renderings.value.rendering + assertEquals("state: action-triggered", updatedRendering.text) + + val traceCalls = fakeTrace.traceCalls + + // Should have action render tracing + assertTrue( + traceCalls.any { it.type == "beginSection" && it.label!!.contains("MAYBE_RENDER") } + ) + + // Should have render pass tracing + assertTrue(traceCalls.any { it.type == "beginSection" && it.label!!.contains("RENDER") }) + + // Verify sections are properly closed + val beginSections = traceCalls.count { it.type == "beginSection" } + val endSections = traceCalls.count { it.type == "endSection" } + assertEquals(beginSections, endSections) + } + + @Test + fun `integration test - worker triggers render correctly traced`() = + runTest { + val runtimeLoopMutex = Mutex(locked = true) + val runtimeLoopListener = WorkflowRuntimeLoopListener { _, _ -> + runtimeLoopMutex.unlock() + } + val fakeTrace = FakeSafeTrace() + val papaTracer = WorkflowPapaTracer(fakeTrace) + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(papaTracer), + runtimeLoopListener = runtimeLoopListener + ) + + val props = MutableStateFlow("initial") + val workflow = WorkerTestWorkflow() + + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(monitor), + runtimeConfig = runtimeConfig, + ) {} + + runtimeLoopMutex.lock() + + val initialRendering = renderings.value.rendering + assertEquals("state: initial", initialRendering.text) + + fakeTrace.clearTraceCalls() + + // Trigger the worker by sending a value + initialRendering.triggerWorker("worker-result") + runtimeLoopMutex.lock() + + val updatedRendering = renderings.value.rendering + assertEquals("state: worker-result", updatedRendering.text) + + val traceCalls = fakeTrace.traceCalls + + // Should have action render tracing from worker + assertTrue( + traceCalls.any { it.type == "beginSection" && it.label!!.contains("MAYBE_RENDER") } + ) + + // Should have render pass tracing + assertTrue(traceCalls.any { it.type == "beginSection" && it.label!!.contains("RENDER") }) + } + + @Test + fun `integration test - child workflow renders are traced`() = + runTest { + val runtimeLoopMutex = Mutex(locked = true) + val runtimeLoopListener = WorkflowRuntimeLoopListener { _, _ -> + runtimeLoopMutex.unlock() + } + val fakeTrace = FakeSafeTrace() + val papaTracer = WorkflowPapaTracer(fakeTrace) + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(papaTracer), + runtimeLoopListener = runtimeLoopListener + ) + + val props = MutableStateFlow("parent-initial") + val workflow = ParentWorkflow() + + val renderings = renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(monitor), + runtimeConfig = runtimeConfig, + ) {} + + runtimeLoopMutex.lock() + + val initialRendering = renderings.value.rendering + assertEquals("parent: parent-initial, child: child-parent-initial", initialRendering.text) + + val traceCalls = fakeTrace.traceCalls + + // Should have async sections for both parent and child workflows + val asyncSections = traceCalls.filter { it.type == "beginAsyncSection" } + assertTrue( + asyncSections.size >= 2, + "Should have at least 2 async sections (parent and child)" + ) + + // Should have render sections for both workflows + val renderSections = + traceCalls.filter { it.type == "beginSection" && it.label!!.contains("RENDER") } + assertTrue(renderSections.size >= 2, "Should have at least 2 render sections") + + // Should have initial state tracing for both workflows + val initialStateSections = + traceCalls.filter { it.type == "beginSection" && it.label!!.contains("InitialState") } + assertTrue(initialStateSections.size >= 2, "Should have at least 2 initial state sections") + } + + @Test + fun `integration test - runtime loop processing is traced`() = + runTest { + val runtimeLoopMutex = Mutex(locked = true) + val fakeTrace = FakeSafeTrace() + val papaTracer = WorkflowPapaTracer(fakeTrace) + val runtimeListener = TestWorkflowRuntimeLoopListener(runtimeLoopMutex) + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(papaTracer), + runtimeLoopListener = runtimeListener + ) + + val props = MutableStateFlow("initial") + val workflow = TestWorkflow() + + renderWorkflowIn( + workflow = workflow, + scope = backgroundScope, + props = props, + interceptors = listOf(monitor), + runtimeConfig = runtimeConfig, + ) {} + + runtimeLoopMutex.lock() + + // Verify that runtime loop listener was called + assertTrue(runtimeListener.onRuntimeLoopTickCalled) + assertNotNull(runtimeListener.runtimeUpdatesReceived) + + val traceCalls = fakeTrace.traceCalls + + // Should have summary information from runtime loop tick + assertTrue(traceCalls.any { it.type == "logSection" && it.label!!.contains("SUM") }) + + // Should include configuration information + assertTrue(traceCalls.any { it.type == "logSection" && it.label!!.contains("Config:") }) + } + + // Test helper classes + private class TestWorkflow : StatefulWorkflow() { + override fun initialState( + props: String, + snapshot: Snapshot? + ): String { + return props + } + + override fun onPropsChanged( + old: String, + new: String, + state: String + ): String { + return new + } + + override fun render( + renderProps: String, + renderState: String, + context: RenderContext + ): String { + return "state: $renderState" + } + + override fun snapshotState(state: String): Snapshot = Snapshot.of(state) + } + + private data class InteractiveRendering( + val text: String, + val onAction: (String) -> Unit + ) + + private class InteractiveTestWorkflow : StatefulWorkflow() { + override fun initialState( + props: String, + snapshot: Snapshot? + ): String = props + + override fun onPropsChanged( + old: String, + new: String, + state: String + ): String = new + + override fun render( + renderProps: String, + renderState: String, + context: RenderContext + ): InteractiveRendering { + return InteractiveRendering( + text = "state: $renderState", + onAction = { newState -> + context.actionSink.send(action("user-action") { state = newState }) + } + ) + } + + override fun snapshotState(state: String): Snapshot = Snapshot.of(state) + } + + private data class WorkerRendering( + val text: String, + val triggerWorker: (String) -> Unit + ) + + private class WorkerTestWorkflow : StatefulWorkflow() { + override fun initialState( + props: String, + snapshot: Snapshot? + ): String = props + + override fun onPropsChanged( + old: String, + new: String, + state: String + ): String = new + + override fun render( + renderProps: String, + renderState: String, + context: RenderContext + ): WorkerRendering { + // Create a worker that can be triggered externally + val triggerChannel = Channel(capacity = Channel.UNLIMITED) + + val triggerFunction = { value: String -> + triggerChannel.trySend(value) + Unit + } + + context.runningWorker( + triggerChannel.receiveAsFlow().asWorker() + ) { result -> + action("worker-action") { state = result } + } + + return WorkerRendering( + text = "state: $renderState", + triggerWorker = triggerFunction + ) + } + + override fun snapshotState(state: String): Snapshot = Snapshot.of(state) + } + + private data class ParentRendering(val text: String) + + private class ParentWorkflow : StatefulWorkflow() { + override fun initialState( + props: String, + snapshot: Snapshot? + ): String = props + + override fun onPropsChanged( + old: String, + new: String, + state: String + ): String = new + + override fun render( + renderProps: String, + renderState: String, + context: RenderContext + ): ParentRendering { + val childRendering = context.renderChild( + child = ChildWorkflow(), + props = "child-$renderState" + ) { + // Child doesn't emit output in this test + WorkflowAction.noAction() + } + + return ParentRendering("parent: $renderState, child: $childRendering") + } + + override fun snapshotState(state: String): Snapshot = Snapshot.of(state) + } + + private class ChildWorkflow : StatefulWorkflow() { + override fun initialState( + props: String, + snapshot: Snapshot? + ): String = props + + override fun onPropsChanged( + old: String, + new: String, + state: String + ): String = new + + override fun render( + renderProps: String, + renderState: String, + context: RenderContext + ): String = renderState + + override fun snapshotState(state: String): Snapshot = Snapshot.of(state) + } + + private class TestWorkflowRuntimeLoopListener( + val runtimeLoopMutex: Mutex, + ) : WorkflowRuntimeLoopListener { + var onRuntimeLoopTickCalled = false + var runtimeUpdatesReceived: RuntimeUpdates? = null + + override fun onRuntimeLoopTick( + configSnapshot: ConfigSnapshot, + runtimeUpdates: RuntimeUpdates + ) { + runtimeLoopMutex.unlock() + onRuntimeLoopTickCalled = true + runtimeUpdatesReceived = runtimeUpdates + } + } +} diff --git a/workflow-tracing/README.md b/workflow-tracing/README.md index 47db0e3030..141221daa0 100644 --- a/workflow-tracing/README.md +++ b/workflow-tracing/README.md @@ -1,5 +1,7 @@ # Module workflow-tracing -This module provides a `WorkflowDiagnosticListener` called `TracingDiagnosticListener` that can be -plugged into a `WorkflowSession` to generate a Chrome-viewable tracing file. For more information -on the tracing file format, see the [`trace-encoder` README](../trace-encoder/README.md). +This module provides the [com.squareup.workflow1.WorkflowRuntimeMonitor] which is a special +[com.squareup.workflow1.WorkflowRuntimeInterceptor] that can be used to monitor an entire runtime +on Android with bookkeeping about the workflow sessions that enables intelligent tracing of all +Workflows and the runtime. Those can be added optionally by passing in custom +[com.squareup.workflow.WorkflowRuntimeTracer]s. diff --git a/workflow-tracing/api/workflow-tracing.api b/workflow-tracing/api/workflow-tracing.api index 0892bdeb77..6bbd32b024 100644 --- a/workflow-tracing/api/workflow-tracing.api +++ b/workflow-tracing/api/workflow-tracing.api @@ -1,17 +1,167 @@ -public abstract interface class com/squareup/workflow1/diagnostic/tracing/MemoryStats { - public abstract fun freeMemory ()J - public abstract fun totalMemory ()J +public final class com/squareup/workflow1/tracing/ActionAppliedLogLine : com/squareup/workflow1/tracing/RuntimeUpdateLogLine { + public fun (Lcom/squareup/workflow1/tracing/ActionAppliedLogLine$WorkflowActionLogType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/WorkflowOutput;Ljava/lang/String;)V + public final fun getActionName ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getNewState ()Ljava/lang/Object; + public final fun getOldState ()Ljava/lang/Object; + public final fun getOutputOrNull ()Lcom/squareup/workflow1/WorkflowOutput; + public final fun getOutputReceivedString ()Ljava/lang/String; + public final fun getPropsOrNull ()Ljava/lang/Object; + public final fun getType ()Lcom/squareup/workflow1/tracing/ActionAppliedLogLine$WorkflowActionLogType; + public fun log (Ljava/lang/StringBuilder;)V } -public final class com/squareup/workflow1/diagnostic/tracing/RuntimeMemoryStats : com/squareup/workflow1/diagnostic/tracing/MemoryStats { - public static final field INSTANCE Lcom/squareup/workflow1/diagnostic/tracing/RuntimeMemoryStats; - public fun freeMemory ()J - public fun totalMemory ()J +public final class com/squareup/workflow1/tracing/ActionAppliedLogLine$WorkflowActionLogType : java/lang/Enum { + public static final field CASCADE Lcom/squareup/workflow1/tracing/ActionAppliedLogLine$WorkflowActionLogType; + public static final field RENDERING_CALLBACK Lcom/squareup/workflow1/tracing/ActionAppliedLogLine$WorkflowActionLogType; + public static final field WORKER_OUTPUT Lcom/squareup/workflow1/tracing/ActionAppliedLogLine$WorkflowActionLogType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/workflow1/tracing/ActionAppliedLogLine$WorkflowActionLogType; + public static fun values ()[Lcom/squareup/workflow1/tracing/ActionAppliedLogLine$WorkflowActionLogType; } -public final class com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor : com/squareup/workflow1/WorkflowInterceptor { - public fun (Lcom/squareup/workflow1/diagnostic/tracing/MemoryStats;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Lcom/squareup/workflow1/diagnostic/tracing/MemoryStats;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class com/squareup/workflow1/tracing/ChainedWorkflowRuntimeTracerKt { + public static final fun wrap (Lcom/squareup/workflow1/WorkflowInterceptor$RenderContextInterceptor;Lcom/squareup/workflow1/WorkflowInterceptor$RenderContextInterceptor;)Lcom/squareup/workflow1/WorkflowInterceptor$RenderContextInterceptor; +} + +public final class com/squareup/workflow1/tracing/ConfigSnapshot { + public fun (Ljava/util/Set;)V + public final fun getConfigAsString ()Ljava/lang/String; + public final fun getCsrConfig ()Z + public final fun getDeaConfig ()Z + public final fun getPtrConfig ()Z + public final fun getSehConfig ()Z + public final fun getShortCircuitConfig ()Z + public final fun getWsdConfig ()Z +} + +public abstract interface class com/squareup/workflow1/tracing/Loggable { + public abstract fun toLogString ()Ljava/lang/String; +} + +public abstract interface class com/squareup/workflow1/tracing/RenderCause { +} + +public final class com/squareup/workflow1/tracing/RenderCause$Action : com/squareup/workflow1/tracing/RenderCause { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun getActionName ()Ljava/lang/String; + public final fun getWorkerIncomingName ()Ljava/lang/String; + public final fun getWorkflowName ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/RenderCause$Callback : com/squareup/workflow1/tracing/RenderCause { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun getActionName ()Ljava/lang/String; + public final fun getWorkflowName ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/RenderCause$RootCreation : com/squareup/workflow1/tracing/RenderCause { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun getRunnerName ()Ljava/lang/String; + public final fun getWorkflowName ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/RenderCause$RootPropsChanged : com/squareup/workflow1/tracing/RenderCause { + public static final field INSTANCE Lcom/squareup/workflow1/tracing/RenderCause$RootPropsChanged; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/RenderCause$WaitingForOutput : com/squareup/workflow1/tracing/RenderCause { + public fun (Ljava/lang/String;)V + public final fun getWorkflowName ()Ljava/lang/String; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/RenderLogLine : com/squareup/workflow1/tracing/RuntimeUpdateLogLine { + public static final field INSTANCE Lcom/squareup/workflow1/tracing/RenderLogLine; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun log (Ljava/lang/StringBuilder;)V + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/RenderPassInfo { + public synthetic fun (Ljava/lang/String;Lcom/squareup/workflow1/tracing/RenderCause;JLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getDurationUptime-UwyO8pc ()J + public final fun getRenderCause ()Lcom/squareup/workflow1/tracing/RenderCause; + public final fun getRunnerName ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/RuntimeLoggingUtilsKt { + public static final fun getWfLogString (Ljava/lang/Object;)Ljava/lang/String; + public static final fun toLoggingShortName (Lcom/squareup/workflow1/WorkflowAction;)Ljava/lang/String; + public static final fun toWfLoggingName (Lcom/squareup/workflow1/WorkflowAction;)Ljava/lang/String; + public static final fun workerKey (Ljava/lang/String;)Ljava/lang/String; +} + +public abstract interface class com/squareup/workflow1/tracing/RuntimeTraceContext { + public abstract fun addRuntimeUpdate (Lcom/squareup/workflow1/tracing/RuntimeUpdateLogLine;)V + public abstract fun getConfigSnapshot ()Lcom/squareup/workflow1/tracing/ConfigSnapshot; + public abstract fun getCurrentRenderCause ()Lcom/squareup/workflow1/tracing/RenderCause; + public abstract fun getPreviousRenderCause ()Lcom/squareup/workflow1/tracing/RenderCause; + public abstract fun getRenderIncomingCauses ()Ljava/util/List; + public abstract fun getRuntimeName ()Ljava/lang/String; + public abstract fun getWorkflowSessionInfo ()Landroidx/collection/MutableLongObjectMap; + public abstract fun setConfigSnapshot (Lcom/squareup/workflow1/tracing/ConfigSnapshot;)V + public abstract fun setCurrentRenderCause (Lcom/squareup/workflow1/tracing/RenderCause;)V + public abstract fun setPreviousRenderCause (Lcom/squareup/workflow1/tracing/RenderCause;)V +} + +public abstract interface class com/squareup/workflow1/tracing/RuntimeUpdateLogLine { + public abstract fun log (Ljava/lang/StringBuilder;)V +} + +public final class com/squareup/workflow1/tracing/RuntimeUpdates { + public fun ()V + public final fun readAndClear ()Ljava/util/List; +} + +public abstract interface class com/squareup/workflow1/tracing/SafeTraceInterface { + public abstract fun beginAsyncSection (Ljava/lang/String;I)V + public abstract fun beginSection (Ljava/lang/String;)V + public abstract fun endAsyncSection (Ljava/lang/String;I)V + public abstract fun endSection ()V + public abstract fun isCurrentlyTracing ()Z + public abstract fun isTraceable ()Z + public abstract fun logSection (Ljava/lang/String;)V +} + +public final class com/squareup/workflow1/tracing/SkipLogLine : com/squareup/workflow1/tracing/RuntimeUpdateLogLine { + public static final field INSTANCE Lcom/squareup/workflow1/tracing/SkipLogLine; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun log (Ljava/lang/StringBuilder;)V + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/UiUpdateLogLine : com/squareup/workflow1/tracing/RuntimeUpdateLogLine { + public fun (Ljava/lang/String;)V + public final fun getNote ()Ljava/lang/String; + public fun log (Ljava/lang/StringBuilder;)V +} + +public abstract interface class com/squareup/workflow1/tracing/WorkflowRenderPassTracker { + public abstract fun recordRenderPass (Lcom/squareup/workflow1/tracing/RenderPassInfo;)V +} + +public abstract interface class com/squareup/workflow1/tracing/WorkflowRuntimeLoopListener { + public abstract fun onRuntimeLoopTick (Lcom/squareup/workflow1/tracing/ConfigSnapshot;Lcom/squareup/workflow1/tracing/RuntimeUpdates;)V +} + +public final class com/squareup/workflow1/tracing/WorkflowRuntimeMonitor : com/squareup/workflow1/WorkflowInterceptor, com/squareup/workflow1/tracing/RuntimeTraceContext { + public field configSnapshot Lcom/squareup/workflow1/tracing/ConfigSnapshot; + public fun (Ljava/lang/String;Ljava/util/List;Lcom/squareup/workflow1/tracing/WorkflowRenderPassTracker;Lcom/squareup/workflow1/tracing/WorkflowRuntimeLoopListener;)V + public synthetic fun (Ljava/lang/String;Ljava/util/List;Lcom/squareup/workflow1/tracing/WorkflowRenderPassTracker;Lcom/squareup/workflow1/tracing/WorkflowRuntimeLoopListener;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun addRuntimeUpdate (Lcom/squareup/workflow1/tracing/RuntimeUpdateLogLine;)V + public fun getConfigSnapshot ()Lcom/squareup/workflow1/tracing/ConfigSnapshot; + public fun getCurrentRenderCause ()Lcom/squareup/workflow1/tracing/RenderCause; + public fun getPreviousRenderCause ()Lcom/squareup/workflow1/tracing/RenderCause; + public fun getRenderIncomingCauses ()Ljava/util/List; + public fun getRuntimeName ()Ljava/lang/String; + public fun getWorkflowSessionInfo ()Landroidx/collection/MutableLongObjectMap; public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; @@ -20,12 +170,63 @@ public final class com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInte public fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot; public fun onSnapshotStateWithChildren (Lkotlin/jvm/functions/Function0;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/TreeSnapshot; + public fun setConfigSnapshot (Lcom/squareup/workflow1/tracing/ConfigSnapshot;)V + public fun setCurrentRenderCause (Lcom/squareup/workflow1/tracing/RenderCause;)V + public fun setPreviousRenderCause (Lcom/squareup/workflow1/tracing/RenderCause;)V +} + +public abstract interface class com/squareup/workflow1/tracing/WorkflowRuntimeMonitor$ActionType { +} + +public final class com/squareup/workflow1/tracing/WorkflowRuntimeMonitor$ActionType$CascadeAction : com/squareup/workflow1/tracing/WorkflowRuntimeMonitor$ActionType { + public fun (Ljava/lang/String;)V + public final fun getChildOutputString ()Ljava/lang/String; +} + +public final class com/squareup/workflow1/tracing/WorkflowRuntimeMonitor$ActionType$QueuedAction : com/squareup/workflow1/tracing/WorkflowRuntimeMonitor$ActionType { + public static final field INSTANCE Lcom/squareup/workflow1/tracing/WorkflowRuntimeMonitor$ActionType$QueuedAction; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract class com/squareup/workflow1/tracing/WorkflowRuntimeTracer : com/squareup/workflow1/WorkflowInterceptor { + protected field workflowRuntimeTraceContext Lcom/squareup/workflow1/tracing/RuntimeTraceContext; + public fun ()V + public fun attachRuntimeContext (Lcom/squareup/workflow1/tracing/RuntimeTraceContext;)V + protected final fun getKey (Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/String; + protected final fun getLogName (Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/String; + protected final fun getName (Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/String; + protected final fun getSessionInfo ()Landroidx/collection/LongObjectMap; + protected final fun getTraceName (Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/String; + protected final fun getWorkflowRuntimeTraceContext ()Lcom/squareup/workflow1/tracing/RuntimeTraceContext; + public fun onInitialState (Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlinx/coroutines/CoroutineScope;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onPropsChanged (Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onRender (Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; + public fun onRenderAndSnapshot (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/RenderingAndSnapshot; + public fun onRootPropsChanged (Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public final fun onRuntimeUpdate (Lcom/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate;)V + public fun onRuntimeUpdateEnhanced (Lcom/squareup/workflow1/WorkflowInterceptor$RuntimeUpdate;ZLcom/squareup/workflow1/tracing/ConfigSnapshot;)V + public final fun onSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public fun onSnapshotState (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot; + public fun onSnapshotStateWithChildren (Lkotlin/jvm/functions/Function0;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/TreeSnapshot; + public fun onWorkflowSessionStarted (Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public fun onWorkflowSessionStopped (J)V + protected final fun setWorkflowRuntimeTraceContext (Lcom/squareup/workflow1/tracing/RuntimeTraceContext;)V +} + +public final class com/squareup/workflow1/tracing/WorkflowSessionInfo { + public static final field Companion Lcom/squareup/workflow1/tracing/WorkflowSessionInfo$Companion; + public static final field MAX_KEY_LENGTH I + public static final field MAX_TRACE_NAME_LENGTH I + public fun (Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V + public fun (Ljava/lang/String;Ljava/lang/String;)V + public final fun getKey ()Ljava/lang/String; + public final fun getLogName ()Ljava/lang/String; + public final fun getName ()Ljava/lang/String; + public final fun getTraceName ()Ljava/lang/String; } -public final class com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptorKt { - public static final fun TracingWorkflowInterceptor (Ljava/io/File;Ljava/lang/String;)Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor; - public static final fun TracingWorkflowInterceptor (Ljava/lang/String;Lcom/squareup/workflow1/diagnostic/tracing/MemoryStats;Lkotlin/jvm/functions/Function1;)Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor; - public static synthetic fun TracingWorkflowInterceptor$default (Ljava/io/File;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor; - public static synthetic fun TracingWorkflowInterceptor$default (Ljava/lang/String;Lcom/squareup/workflow1/diagnostic/tracing/MemoryStats;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor; +public final class com/squareup/workflow1/tracing/WorkflowSessionInfo$Companion { } diff --git a/workflow-tracing/build.gradle.kts b/workflow-tracing/build.gradle.kts index 87f9dc6e80..748e4b0f95 100644 --- a/workflow-tracing/build.gradle.kts +++ b/workflow-tracing/build.gradle.kts @@ -1,25 +1,24 @@ plugins { - id("kotlin-jvm") + id("com.android.library") + id("kotlin-android") + id("android-defaults") id("published") } +android { + namespace = "com.squareup.workflow1.tracing" +} + dependencies { + api(libs.androidx.collection) api(libs.kotlin.jdk8) api(libs.kotlinx.coroutines.core) - api(project(":trace-encoder")) api(project(":workflow-core")) api(project(":workflow-runtime")) - compileOnly(libs.jetbrains.annotations) - - implementation(libs.squareup.moshi) - implementation(libs.squareup.moshi.adapters) - implementation(libs.squareup.okio) - testImplementation(libs.junit) testImplementation(libs.kotlin.test.core) testImplementation(libs.kotlin.test.jdk) - testImplementation(libs.mockito.core) - testImplementation(libs.mockito.kotlin) + testImplementation(libs.kotlinx.coroutines.test) } diff --git a/trace-encoder/dependencies/runtimeClasspath.txt b/workflow-tracing/dependencies/releaseRuntimeClasspath.txt similarity index 57% rename from trace-encoder/dependencies/runtimeClasspath.txt rename to workflow-tracing/dependencies/releaseRuntimeClasspath.txt index 49346f8ae7..1c00b07c03 100644 --- a/trace-encoder/dependencies/runtimeClasspath.txt +++ b/workflow-tracing/dependencies/releaseRuntimeClasspath.txt @@ -1,8 +1,11 @@ -com.squareup.moshi:moshi-adapters:1.15.2 -com.squareup.moshi:moshi:1.15.2 -com.squareup.okio:okio-jvm:3.7.0 -com.squareup.okio:okio:3.7.0 +androidx.annotation:annotation-jvm:1.9.1 +androidx.annotation:annotation:1.9.1 +androidx.collection:collection-jvm:1.5.0 +androidx.collection:collection:1.5.0 +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 diff --git a/workflow-tracing/gradle.properties b/workflow-tracing/gradle.properties index 0043f373fb..905fe35581 100644 --- a/workflow-tracing/gradle.properties +++ b/workflow-tracing/gradle.properties @@ -1,3 +1,3 @@ POM_ARTIFACT_ID=workflow-tracing POM_NAME=Workflow Tracing -POM_PACKAGING=jar +POM_PACKAGING=aar diff --git a/workflow-tracing/src/main/baseline-prof.txt b/workflow-tracing/src/main/baseline-prof.txt deleted file mode 100644 index 2854042e8c..0000000000 --- a/workflow-tracing/src/main/baseline-prof.txt +++ /dev/null @@ -1,62 +0,0 @@ -HSPLcom/squareup/workflow1/diagnostic/tracing/GcDetector$GcCanary;->(Lcom/squareup/workflow1/diagnostic/tracing/GcDetector;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/GcDetector$GcCanary;->finalize()V -HSPLcom/squareup/workflow1/diagnostic/tracing/GcDetector;->(Lkotlin/jvm/functions/Function0;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/GcDetector;->access$getOnGcDetected$p(Lcom/squareup/workflow1/diagnostic/tracing/GcDetector;)Lkotlin/jvm/functions/Function0; -HSPLcom/squareup/workflow1/diagnostic/tracing/GcDetector;->access$getRunning$p(Lcom/squareup/workflow1/diagnostic/tracing/GcDetector;)Z -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$1;->()V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$1;->invoke(Lkotlin/jvm/functions/Function0;)Lcom/squareup/workflow1/diagnostic/tracing/GcDetector; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$TracingAction;->(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;Lcom/squareup/workflow1/WorkflowAction;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$TracingAction;->apply(Lcom/squareup/workflow1/WorkflowAction$Updater;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$TracingContextInterceptor;->(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$TracingContextInterceptor;->onActionSent(Lcom/squareup/workflow1/WorkflowAction;Lkotlin/jvm/functions/Function1;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$TracingContextInterceptor;->onRenderChild(Lcom/squareup/workflow1/Workflow;Ljava/lang/Object;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;)Ljava/lang/Object; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$TracingContextInterceptor;->onRunningSideEffect(Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onRuntimeStarted$1;->(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onRuntimeStarted$1;->invoke()Ljava/lang/Object; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onRuntimeStarted$1;->invoke()V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onSessionStarted$1;->(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onSessionStarted$1;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onSessionStarted$1;->invoke(Ljava/lang/Throwable;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onSessionStarted$2;->(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->(Lcom/squareup/workflow1/diagnostic/tracing/MemoryStats;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->(Lcom/squareup/workflow1/diagnostic/tracing/MemoryStats;Lkotlin/jvm/functions/Function2;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->access$createMemoryEvent(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;)Lcom/squareup/tracing/TraceEvent$Counter; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->access$getLogger$p(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;)Lcom/squareup/tracing/TraceLogger; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->access$getMemoryStats$p(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;)Lcom/squareup/workflow1/diagnostic/tracing/MemoryStats; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->access$onSinkReceived(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;JLcom/squareup/workflow1/WorkflowAction;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->access$onWorkflowAction(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;JLcom/squareup/workflow1/WorkflowAction;Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/WorkflowOutput;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->access$onWorkflowStopped(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;J)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->createMemoryEvent()Lcom/squareup/tracing/TraceEvent$Counter; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onAfterRenderPass(Ljava/lang/Object;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onAfterSnapshotPass()V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onAfterWorkflowRendered(JLjava/lang/Object;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onBeforeRenderPass(Ljava/lang/Object;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onBeforeSnapshotPass()V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onBeforeWorkflowRendered(JLjava/lang/Object;Ljava/lang/Object;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onInitialState(Ljava/lang/Object;Lcom/squareup/workflow1/Snapshot;Lkotlin/jvm/functions/Function2;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onPropsChanged(Ljava/lang/Long;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onPropsChanged(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onRender(Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/BaseRenderContext;Lkotlin/jvm/functions/Function3;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Ljava/lang/Object; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onRuntimeStarted(Lkotlinx/coroutines/CoroutineScope;Ljava/lang/String;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onSessionStarted(Lkotlinx/coroutines/CoroutineScope;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onSinkReceived(JLcom/squareup/workflow1/WorkflowAction;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onSnapshotState(Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lcom/squareup/workflow1/WorkflowInterceptor$WorkflowSession;)Lcom/squareup/workflow1/Snapshot; -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onWorkflowAction(JLcom/squareup/workflow1/WorkflowAction;Ljava/lang/Object;Ljava/lang/Object;Lcom/squareup/workflow1/WorkflowOutput;)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onWorkflowStarted(JLjava/lang/Long;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;Z)V -HSPLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onWorkflowStopped(J)V -Lcom/squareup/workflow1/diagnostic/tracing/GcDetector$GcCanary; -Lcom/squareup/workflow1/diagnostic/tracing/GcDetector; -Lcom/squareup/workflow1/diagnostic/tracing/MemoryStats; -Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$1; -Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$TracingAction; -Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$TracingContextInterceptor; -Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onRuntimeStarted$1; -Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onSessionStarted$1; -Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onSessionStarted$2; -Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor; -PLcom/squareup/workflow1/diagnostic/tracing/GcDetector;->stop()V -PLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onSessionStarted$2;->invoke(Ljava/lang/Object;)Ljava/lang/Object; -PLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor$onSessionStarted$2;->invoke(Ljava/lang/Throwable;)V -PLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->access$onRuntimeStopped(Lcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;)V -PLcom/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor;->onRuntimeStopped()V \ No newline at end of file diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/GcDetector.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/GcDetector.kt deleted file mode 100644 index 2a5740263f..0000000000 --- a/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/GcDetector.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.squareup.workflow1.diagnostic.tracing - -internal typealias GcDetectorConstructor = (onGcDetected: () -> Unit) -> GcDetector - -/** - * Class that does rough logging of garbage collection runs by allocating an unowned object that - * logs a trace event when its finalizer is ran. - * - * Internal and open for testing. - */ -internal open class GcDetector(private val onGcDetected: () -> Unit) { - - @Volatile private var running = true - - private inner class GcCanary { - @Throws(Throwable::class) - protected fun finalize() { - if (!running) return - - onGcDetected() - GcCanary() - } - } - - init { - GcCanary() - } - - fun stop() { - running = false - } -} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/MemoryStats.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/MemoryStats.kt deleted file mode 100644 index 8eb9ce3348..0000000000 --- a/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/MemoryStats.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.squareup.workflow1.diagnostic.tracing - -/** - * Reports free/available memory. - * - * @see RuntimeMemoryStats - */ -public interface MemoryStats { - public fun freeMemory(): Long - public fun totalMemory(): Long -} - -/** - * A [MemoryStats] that reports memory stats using this [Runtime] instance. - */ -public object RuntimeMemoryStats : MemoryStats { - private val runtime: Runtime = Runtime.getRuntime() - override fun freeMemory(): Long = runtime.freeMemory() - override fun totalMemory(): Long = runtime.totalMemory() -} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor.kt deleted file mode 100644 index 8759b2679f..0000000000 --- a/workflow-tracing/src/main/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptor.kt +++ /dev/null @@ -1,528 +0,0 @@ -package com.squareup.workflow1.diagnostic.tracing - -import com.squareup.tracing.TraceEncoder -import com.squareup.tracing.TraceEvent.AsyncDurationBegin -import com.squareup.tracing.TraceEvent.AsyncDurationEnd -import com.squareup.tracing.TraceEvent.Counter -import com.squareup.tracing.TraceEvent.DurationBegin -import com.squareup.tracing.TraceEvent.DurationEnd -import com.squareup.tracing.TraceEvent.Instant -import com.squareup.tracing.TraceEvent.Instant.InstantScope.GLOBAL -import com.squareup.tracing.TraceEvent.Instant.InstantScope.PROCESS -import com.squareup.tracing.TraceEvent.ObjectCreated -import com.squareup.tracing.TraceEvent.ObjectDestroyed -import com.squareup.tracing.TraceEvent.ObjectSnapshot -import com.squareup.tracing.TraceLogger -import com.squareup.workflow1.ActionApplied -import com.squareup.workflow1.BaseRenderContext -import com.squareup.workflow1.Snapshot -import com.squareup.workflow1.WorkflowAction -import com.squareup.workflow1.WorkflowIdentifier -import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable -import com.squareup.workflow1.WorkflowIdentifierType.Unsnapshottable -import com.squareup.workflow1.WorkflowInterceptor -import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor -import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession -import com.squareup.workflow1.applyTo -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import okio.buffer -import okio.sink -import java.io.File -import kotlin.LazyThreadSafetyMode.NONE -import kotlin.reflect.KClass -import kotlin.reflect.KType - -/** - * A [WorkflowInterceptor] that generates a trace file that can be viewed in Chrome by - * visiting `chrome://tracing`. - * - * @param file The [File] to write the trace to. - * @param name If non-empty, will be used to set the "process name" in the trace file. If empty, - * the workflow type is used for the process name. - */ -public fun TracingWorkflowInterceptor( - file: File, - name: String = "" -): TracingWorkflowInterceptor = TracingWorkflowInterceptor(name) { workflowScope -> - TraceEncoder(workflowScope) { - file.sink() - .buffer() - } -} - -/** - * A [WorkflowInterceptor] that generates a trace file that can be viewed in Chrome by - * visiting `chrome://tracing`. - * - * @param name If non-empty, will be used to set the "process name" in the trace file. If empty, - * the workflow type is used for the process name. - * @param encoderProvider A function that returns a [TraceEncoder] that will be used to write trace - * events. The function gets the [CoroutineScope] that the workflow runtime is running in. - */ -public fun TracingWorkflowInterceptor( - name: String = "", - memoryStats: MemoryStats = RuntimeMemoryStats, - encoderProvider: (workflowScope: CoroutineScope) -> TraceEncoder -): TracingWorkflowInterceptor = - TracingWorkflowInterceptor(memoryStats = memoryStats) { workflowScope, type -> - provideLogger(name, workflowScope, type, encoderProvider) - } - -internal fun provideLogger( - name: String, - workflowScope: CoroutineScope, - workflowType: String, - encoderProvider: (workflowScope: CoroutineScope) -> TraceEncoder -): TraceLogger { - val encoder = encoderProvider(workflowScope) - val processName = name.ifEmpty { workflowType } - return encoder.createLogger( - processName = processName, - threadName = "Profiling" - ) -} - -/** - * A [WorkflowInterceptor] that generates a trace file that can be viewed in Chrome by - * visiting `chrome://tracing`. - * - * @constructor The primary constructor is internal so that it can inject [GcDetector] for tests. - */ -public class TracingWorkflowInterceptor internal constructor( - private val memoryStats: MemoryStats, - private val gcDetectorConstructor: GcDetectorConstructor, - private val loggerProvider: ( - workflowScope: CoroutineScope, - workflowType: String - ) -> TraceLogger -) : WorkflowInterceptor { - - /** - * A [WorkflowInterceptor] that generates a trace file that can be viewed in Chrome by - * visiting `chrome://tracing`. - * - * @param loggerProvider A function that returns a [TraceLogger] that will be used to write trace - * events. The function gets the [CoroutineScope] that the workflow runtime is running in, as well - * as a description of the type of the workflow. - */ - public constructor( - memoryStats: MemoryStats = RuntimeMemoryStats, - loggerProvider: ( - workflowScope: CoroutineScope, - workflowType: String - ) -> TraceLogger - ) : this(memoryStats, ::GcDetector, loggerProvider) - - /** - * [NONE] is fine here because it will get initialized by [onRuntimeStarted] and there's no - * race conditions. - */ - private var logger: TraceLogger? = null - private var gcDetector: GcDetector? = null - - private val workflowNamesById = mutableMapOf() - - override fun onSessionStarted( - workflowScope: CoroutineScope, - session: WorkflowSession - ) { - val workflowJob = workflowScope.coroutineContext[Job]!! - - // Invoke this before runtime logic since cancellation handlers are invoked in the same order - // in which they were registered, and we want to emit workflow stopped before runtime stopped. - workflowJob.invokeOnCompletion { - onWorkflowStopped(session.sessionId) - } - - if (session.parent == null) { - onRuntimeStarted(workflowScope, session.identifier.toLoggingName()) - workflowJob.invokeOnCompletion { - onRuntimeStopped() - } - } - } - - override fun onInitialState( - props: P, - snapshot: Snapshot?, - workflowScope: CoroutineScope, - proceed: (P, Snapshot?, CoroutineScope) -> S, - session: WorkflowSession - ): S { - val initialState = proceed(props, snapshot, workflowScope) - - onWorkflowStarted( - workflowId = session.sessionId, - parentId = session.parent?.sessionId, - workflowType = session.identifier.toLoggingName(), - key = session.renderKey, - initialProps = props, - initialState = initialState, - restoredFromSnapshot = snapshot != null - ) - - return initialState - } - - override fun onPropsChanged( - old: P, - new: P, - state: S, - proceed: (P, P, S) -> S, - session: WorkflowSession - ): S { - val newState = proceed(old, new, state) - if (session.parent == null) { - // Fake getting the props changed event from the runtime directly. - onPropsChanged( - workflowId = null, - oldProps = old, - newProps = new, - oldState = state, - newState = newState - ) - } - onPropsChanged( - workflowId = session.sessionId, - oldProps = old, - newProps = new, - oldState = state, - newState = newState - ) - return newState - } - - override fun onRender( - renderProps: P, - renderState: S, - context: BaseRenderContext, - proceed: (P, S, RenderContextInterceptor?) -> R, - session: WorkflowSession - ): R { - if (session.parent == null) { - // Track the overall render pass for the whole tree. - onBeforeRenderPass(renderProps) - } - onBeforeWorkflowRendered(session.sessionId, renderProps, renderState) - - val rendering = proceed(renderProps, renderState, TracingContextInterceptor(session)) - - onAfterWorkflowRendered(session.sessionId, rendering) - if (session.parent == null) { - onAfterRenderPass(rendering) - } - return rendering - } - - override fun onSnapshotState( - state: S, - proceed: (S) -> Snapshot?, - session: WorkflowSession - ): Snapshot? { - if (session.parent == null) { - onBeforeSnapshotPass() - } - - val snapshot = proceed(state) - - if (session.parent == null) { - onAfterSnapshotPass() - } - return snapshot - } - - private fun onRuntimeStarted( - workflowScope: CoroutineScope, - rootWorkflowType: String - ) { - logger = loggerProvider(workflowScope, rootWorkflowType) - - // Log garbage collections in case they correlate with unusually long render times. - gcDetector = gcDetectorConstructor { - logger?.log( - listOf( - Instant( - name = "GC detected", - scope = GLOBAL, - category = "system", - args = mapOf( - "freeMemory" to memoryStats.freeMemory(), - "totalMemory" to memoryStats.totalMemory() - ) - ), - createMemoryEvent() - ) - ) - } - } - - private fun onRuntimeStopped() { - gcDetector?.stop() - } - - private fun onBeforeRenderPass(props: Any?) { - logger?.log( - listOf( - DurationBegin( - name = "Render Pass", - category = "rendering", - args = mapOf("props" to props.toString()) - ), - createMemoryEvent() - ) - ) - } - - private fun onAfterRenderPass(rendering: Any?) { - logger?.log( - listOf( - DurationEnd( - name = "Render Pass", - category = "rendering", - args = mapOf("rendering" to rendering.toString()) - ), - createMemoryEvent() - ) - ) - } - - private fun onWorkflowStarted( - workflowId: Long, - parentId: Long?, - workflowType: String, - key: String, - initialProps: Any?, - initialState: Any?, - restoredFromSnapshot: Boolean - ) { - val keyPart = if (key.isEmpty()) "" else ":$key" - val name = "$workflowType$keyPart (${workflowId.toHex()})" - workflowNamesById[workflowId] = name - logger?.log( - listOf( - AsyncDurationBegin( - id = "workflow", - name = name, - category = "workflow", - args = mapOf( - "workflowId" to workflowId.toHex(), - "initialProps" to initialProps.toString(), - "initialState" to initialState.toString(), - "restoredFromSnapshot" to restoredFromSnapshot, - "parent" to workflowNamesById[parentId] - ) - ), - ObjectCreated( - id = workflowId, - objectType = name - ) - ) - ) - } - - private fun onWorkflowStopped(workflowId: Long) { - val name = workflowNamesById.getValue(workflowId) - logger?.log( - listOf( - AsyncDurationEnd( - id = "workflow", - name = name, - category = "workflow" - ), - ObjectDestroyed( - id = workflowId, - objectType = name - ) - ) - ) - workflowNamesById -= workflowId - } - - private fun onBeforeWorkflowRendered( - workflowId: Long, - props: Any?, - state: Any? - ) { - val name = workflowNamesById.getValue(workflowId) - logger?.log( - DurationBegin( - name, - args = mapOf( - "workflowId" to workflowId.toHex(), - "props" to props.toString(), - "state" to state.toString() - ), - category = "rendering" - ) - ) - } - - private fun onAfterWorkflowRendered( - workflowId: Long, - rendering: Any? - ) { - val name = workflowNamesById.getValue(workflowId) - logger?.log( - DurationEnd( - name, - args = mapOf("rendering" to rendering.toString()), - category = "rendering" - ) - ) - } - - private fun onBeforeSnapshotPass() { - logger?.log(DurationBegin(name = "Snapshot")) - } - - private fun onAfterSnapshotPass() { - logger?.log(DurationEnd(name = "Snapshot")) - } - - private fun onSinkReceived( - workflowId: Long, - action: WorkflowAction<*, *, *> - ) { - val name = workflowNamesById.getValue(workflowId) - logger?.log( - Instant( - name = "Sink received: $name", - category = "update", - args = mapOf("action" to action.toString()) - ) - ) - } - - private fun onPropsChanged( - workflowId: Long?, - oldProps: Any?, - newProps: Any?, - oldState: Any?, - newState: Any? - ) { - val name = workflowNamesById[workflowId] ?: "{root}" - logger?.log( - Instant( - name = "Props changed: $name", - args = mapOf( - "oldProps" to oldProps.toString(), - "newProps" to if (oldProps == newProps) "{no change}" else newProps.toString(), - "oldState" to oldState.toString(), - "newState" to if (oldState == newState) "{no change}" else newState.toString() - ) - ) - ) - } - - private fun onWorkflowAction( - workflowId: Long, - action: WorkflowAction<*, *, *>, - oldState: Any?, - newState: Any?, - output: ActionApplied<*>? - ) { - val name = workflowNamesById.getValue(workflowId) - - logger?.log( - listOf( - Instant( - name = "WorkflowAction: $name", - category = "update", - scope = PROCESS, - args = mapOf( - "action" to action.toString(), - "oldState" to oldState.toString(), - "newState" to if (oldState == newState) "{no change}" else newState.toString(), - "output" to (output?.let { it.output?.value.toString() } ?: "{no output}") - ) - ), - ObjectSnapshot( - id = workflowId, - objectType = name, - snapshot = newState.toString() - ) - ) - ) - } - - private fun createMemoryEvent(): Counter { - val freeMemory = memoryStats.freeMemory() - val usedMemory = memoryStats.totalMemory() - freeMemory - return Counter( - name = "used/free memory", - series = mapOf( - // This map is ordered. The stacked chart is shown in reverse order so it looks like a - // typical memory usage graph. - "usedMemory" to usedMemory, - "freeMemory" to freeMemory - ) - ) - } - - private inner class TracingContextInterceptor( - private val session: WorkflowSession - ) : RenderContextInterceptor { - override fun onActionSent( - action: WorkflowAction, - proceed: (WorkflowAction) -> Unit - ) { - onSinkReceived(session.sessionId, action) - val wrapperAction = TracingAction(action, session) - proceed(wrapperAction) - } - } - - private inner class TracingAction( - private val delegate: WorkflowAction, - private val session: WorkflowSession - ) : WorkflowAction() { - override fun Updater.apply() { - val oldState = state - val (newState, output) = delegate.applyTo(props, state) - state = newState - output.output?.let { setOutput(it.value) } - onWorkflowAction( - workflowId = session.sessionId, - action = delegate, - oldState = oldState, - newState = newState, - output = output - ) - } - } -} - -private fun WorkflowIdentifier.toLoggingName(): String { - val type = realType - return when { - type is Snapshottable && type.kClass != null -> type.kClass!!.toLoggingName() - type is Unsnapshottable -> type.kType.toLoggingName() - else -> type.typeName - } -} - -private fun KType.toLoggingName(): String { - if (classifier == null) return toString() - - val classifierName = when (val c = classifier) { - is KClass<*> -> c.toLoggingName() - else -> toString() - } - - val params = arguments.map { projection -> - when (val type = projection.type) { - is KType -> type.toLoggingName() - else -> "*" - } - } - - return if (params.isEmpty()) classifierName else "$classifierName<${params.joinToString(", ")}>" -} - -private fun KClass<*>.toLoggingName(): String { - return simpleName ?: toString() -} - -@Suppress("NOTHING_TO_INLINE") -private inline fun Long.toHex() = toString(16) diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/ChainedWorkflowRuntimeTracer.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/ChainedWorkflowRuntimeTracer.kt new file mode 100644 index 0000000000..7d848b2b53 --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/ChainedWorkflowRuntimeTracer.kt @@ -0,0 +1,226 @@ +package com.squareup.workflow1.tracing + +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.RenderingAndSnapshot +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor +import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import kotlinx.coroutines.CoroutineScope +import kotlin.reflect.KType + +internal fun List.chained(): WorkflowRuntimeTracer = + when { + isEmpty() -> NoopWorkflowRuntimeTracer + size == 1 -> single() + else -> ChainedWorkflowRuntimeTracer(this) + } + +internal object NoopWorkflowRuntimeTracer : WorkflowRuntimeTracer() + +/** + * This is what we use in [WorkflowRuntimeMonitor] to make sure we call all + * [WorkflowRuntimeMonitor.workflowRuntimeTracers] with all needed interceptor hooks. + */ +internal class ChainedWorkflowRuntimeTracer( + private val tracers: List +) : WorkflowRuntimeTracer() { + + override fun onRootPropsChanged( + session: WorkflowSession + ) { + tracers.forEach { it.onRootPropsChanged(session) } + } + + override fun attachRuntimeContext( + workflowRuntimeTraceContext: RuntimeTraceContext + ) { + tracers.forEach { it.attachRuntimeContext(workflowRuntimeTraceContext) } + } + + override fun onWorkflowSessionStarted( + workflowScope: CoroutineScope, + session: WorkflowSession + ) { + tracers.forEach { it.onWorkflowSessionStarted(workflowScope, session) } + } + + override fun onWorkflowSessionStopped( + sessionId: Long + ) { + tracers.forEach { it.onWorkflowSessionStopped(sessionId) } + } + + override fun onRuntimeUpdateEnhanced( + runtimeUpdate: RuntimeUpdate, + currentActionHandlingChangedState: Boolean, + configSnapshot: ConfigSnapshot, + ) { + tracers.forEach { + it.onRuntimeUpdateEnhanced( + runtimeUpdate, + currentActionHandlingChangedState, + configSnapshot + ) + } + } + + override fun onInitialState( + props: P, + snapshot: Snapshot?, + workflowScope: CoroutineScope, + proceed: (P, Snapshot?, CoroutineScope) -> S, + session: WorkflowSession + ): S { + val chainedProceed = tracers.foldRight(proceed) { workflowRuntimeTracer, proceedAcc -> + { props, snapshot, workflowScope -> + workflowRuntimeTracer.onInitialState(props, snapshot, workflowScope, proceedAcc, session) + } + } + return chainedProceed(props, snapshot, workflowScope) + } + + override fun onPropsChanged( + old: P, + new: P, + state: S, + proceed: (P, P, S) -> S, + session: WorkflowSession + ): S { + val chainedProceed = tracers.foldRight(proceed) { workflowRuntimeTracer, proceedAcc -> + { old, new, state -> + workflowRuntimeTracer.onPropsChanged(old, new, state, proceedAcc, session) + } + } + return chainedProceed(old, new, state) + } + + override fun onRenderAndSnapshot( + renderProps: P, + proceed: (P) -> RenderingAndSnapshot, + session: WorkflowSession + ): RenderingAndSnapshot { + val chainedProceed = tracers.foldRight(proceed) { workflowRuntimeTracer, proceedAcc -> + { renderProps -> + workflowRuntimeTracer.onRenderAndSnapshot(renderProps, proceedAcc, session) + } + } + return chainedProceed(renderProps) + } + + override fun onRender( + renderProps: P, + renderState: S, + context: BaseRenderContext, + proceed: (P, S, RenderContextInterceptor?) -> R, + session: WorkflowSession + ): R { + val chainedProceed = tracers.foldRight(proceed) { workflowRuntimeTracer, proceedAcc -> + { props, state, outerContextInterceptor -> + workflowRuntimeTracer.onRender( + props, + state, + context, + proceed = { p, s, innerContextInterceptor: RenderContextInterceptor? -> + val contextInterceptor = outerContextInterceptor.wrap(innerContextInterceptor) + proceedAcc(p, s, contextInterceptor) + }, + session = session, + ) + } + } + return chainedProceed(renderProps, renderState, null) + } + + override fun onSnapshotStateWithChildren( + proceed: () -> TreeSnapshot, + session: WorkflowSession + ): TreeSnapshot { + val chainedProceed = tracers.foldRight(proceed) { workflowRuntimeTracer, proceedAcc -> + { + workflowRuntimeTracer.onSnapshotStateWithChildren(proceedAcc, session) + } + } + return chainedProceed() + } + + override fun onSnapshotState( + state: S, + proceed: (S) -> Snapshot?, + session: WorkflowSession + ): Snapshot? { + val chainedProceed = tracers.foldRight(proceed) { workflowRuntimeTracer, proceedAcc -> + { state -> + workflowRuntimeTracer.onSnapshotState(state, proceedAcc, session) + } + } + return chainedProceed(state) + } +} + +public fun RenderContextInterceptor?.wrap( + inner: RenderContextInterceptor? +) = when { + this == null && inner == null -> null + this == null -> inner + inner == null -> this + else -> object : RenderContextInterceptor { + // If we don't use !!, the compiler complains about the non-elvis dot accesses below. + @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") + val outer = this@wrap!! + + override fun onActionSent( + action: WorkflowAction, + proceed: (WorkflowAction) -> Unit + ) { + outer.onActionSent(action) { interceptedAction -> + inner.onActionSent(interceptedAction, proceed) + } + } + + override fun onRenderChild( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction, + proceed: ( + child: Workflow, + props: CP, + key: String, + handler: (CO) -> WorkflowAction + ) -> CR + ): CR = outer.onRenderChild(child, childProps, key, handler) { c, p, k, h -> + inner.onRenderChild(c, p, k, h, proceed) + } + + override fun onRunningSideEffect( + key: String, + sideEffect: suspend () -> Unit, + proceed: (key: String, sideEffect: suspend () -> Unit) -> Unit + ) { + outer.onRunningSideEffect(key, sideEffect) { iKey, iSideEffect -> + inner.onRunningSideEffect(iKey, iSideEffect, proceed) + } + } + + override fun onRemember( + key: String, + resultType: KType, + inputs: Array, + calculation: () -> CResult, + proceed: (String, KType, Array, () -> CResult) -> CResult + ): CResult { + return outer.onRemember( + key, + resultType, + inputs, + calculation + ) { iKey, iResultType, iInputs, iCalculation -> + inner.onRemember(iKey, iResultType, iInputs, iCalculation, proceed) + } + } + } +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/ConfigSnapshot.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/ConfigSnapshot.kt new file mode 100644 index 0000000000..9ec7f4f9ca --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/ConfigSnapshot.kt @@ -0,0 +1,25 @@ +package com.squareup.workflow1.tracing + +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.RuntimeConfigOptions.CONFLATE_STALE_RENDERINGS +import com.squareup.workflow1.RuntimeConfigOptions.DRAIN_EXCLUSIVE_ACTIONS +import com.squareup.workflow1.RuntimeConfigOptions.PARTIAL_TREE_RENDERING +import com.squareup.workflow1.RuntimeConfigOptions.RENDER_ONLY_WHEN_STATE_CHANGES +import com.squareup.workflow1.RuntimeConfigOptions.STABLE_EVENT_HANDLERS +import com.squareup.workflow1.RuntimeConfigOptions.WORK_STEALING_DISPATCHER +import com.squareup.workflow1.WorkflowExperimentalRuntime + +/** + * Snapshot of the current [RuntimeConfig] + */ +@OptIn(WorkflowExperimentalRuntime::class) +public class ConfigSnapshot(config: RuntimeConfig) { + val shortCircuitConfig = config.contains(RENDER_ONLY_WHEN_STATE_CHANGES) + val csrConfig = config.contains(CONFLATE_STALE_RENDERINGS) + val ptrConfig = config.contains(PARTIAL_TREE_RENDERING) + val deaConfig = config.contains(DRAIN_EXCLUSIVE_ACTIONS) + val sehConfig = config.contains(STABLE_EVENT_HANDLERS) + val wsdConfig = config.contains(WORK_STEALING_DISPATCHER) + + val configAsString = config.toString() +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/Loggable.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/Loggable.kt new file mode 100644 index 0000000000..5207e60262 --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/Loggable.kt @@ -0,0 +1,9 @@ +package com.squareup.workflow1.tracing + +/** + * Optional interface implemented by workflow `PropsT`, `StateT` `OutputT` classes + * to customize their logging names for [WorkflowRuntimeMonitor] output. + */ +public interface Loggable { + public fun toLogString(): String +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeLoggingUtils.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeLoggingUtils.kt new file mode 100644 index 0000000000..e97fe8ba4a --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeLoggingUtils.kt @@ -0,0 +1,200 @@ +package com.squareup.workflow1.tracing + +import com.squareup.workflow1.Worker +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowIdentifier +import com.squareup.workflow1.WorkflowIdentifierType.Snapshottable +import com.squareup.workflow1.WorkflowIdentifierType.Unsnapshottable +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import kotlin.reflect.KClass +import kotlin.reflect.KType + +internal fun WorkflowSession.toWfLoggingName(): String { + val renderKey = renderKey + return if (renderKey.isEmpty()) { + identifier.toWfLoggingName() + } else { + "${identifier.toWfLoggingName()}($renderKey)" + } +} + +internal fun WorkflowIdentifier.toWfLoggingName(): String { + return when (val type = realType) { + is Snapshottable -> type.kClass?.toWfLoggingName() ?: type.typeName + is Unsnapshottable -> type.kType.toWfLoggingName() + }.run { + wfRemoveReflectionNotAvailable() + } +} + +/** + * Useful Action Logging with our utilities to remove extra strings as needed. + */ +public fun WorkflowAction.toLoggingShortName(): String { + return debuggingName + .wfRemoveReflectionNotAvailable() + .wfStripSquarePackage() +} + +/** + * Same as [WorkflowAction.toLoggingShortName()] but wraps it in "Action()" to help + * identify what it is. + */ +public fun WorkflowAction.toWfLoggingName(): String { + return "Action(${toLoggingShortName()})" +} + +/** + * Extract key for worker action names using knowledge of implementation details. + */ +public fun String.workerKey(): String { + return if (contains(Worker.WORKER_OUTPUT_ACTION_NAME)) { + substringAfter("key=").substringBefore(')') + } else { + "" + } +} + +internal fun KType.toWfLoggingName(): String { + if (classifier == null) return toString().wfStripSquarePackage() + + val classifierName = when (val c = classifier) { + is KClass<*> -> c.toWfLoggingName() + else -> toString().wfStripSquarePackage() + } + + val params = arguments.map { projection -> + when (val type = projection.type) { + is KType -> type.toWfLoggingName() + else -> "*" + } + } + + return if (params.isEmpty()) classifierName else "$classifierName<${params.joinToString(", ")}>" +} + +/** + * Gets a class's simple name. If an inner class, its wrapping class's simple name is added as a + * prefix. + * + * For example, `java.util.Map` would be `Map`, and `java.util.Map.Entry` would be `Map.Entry`. + */ +internal fun getWfHumanClassName(obj: Any): String { + val objClass: Class<*> = when (obj) { + is KClass<*> -> obj.java + is Class<*> -> obj + else -> obj.javaClass + } + var humanName = objClass.simpleName.takeIf { it.isNotBlank() } + ?: objClass.name.substringAfterLast(".") + + var memberClass: Class<*>? = objClass + while (memberClass?.isMemberClass == true) { + val tempMemberClass: Class<*>? = memberClass.declaringClass?.also { + memberClass = it + } + humanName = tempMemberClass?.simpleName + "." + humanName + } + return humanName +} + +internal fun KClass<*>.toWfLoggingName(): String { + return getWfHumanClassName(this) +} + +/** + * Alternative to [toString] used by Workflow logging. + */ +public fun getWfLogString(log: Any?): String { + return when (log) { + null, + is Boolean, + is Enum<*>, + is Number -> log.toString() + + is String -> log + is Pair<*, *> -> "Pair(${getWfLogString(log.first)}, ${getWfLogString(log.second)})" + is Triple<*, *, *> -> + "Triple(${getWfLogString(log.first)}, ${ + getWfLogString( + log.second + ) + }, ${getWfLogString(log.third)})" + + is Loggable -> log.toLogString() + is WorkflowAction<*, *, *> -> log.toWfLoggingName() + + else -> log::class.toWfLoggingName() + } +} + +/** + * Returns an ellipsized string if this string is longer than [maxLength]. + * + * @param maxLength The maximum length the string can be before ellipsizing will occur. This must be + * a positive number. + */ +internal fun String.wfEllipsizeEnd(maxLength: Int): String { + require(maxLength > 0) + + return if (maxLength < length) { + take(maxLength - 1).trimEnd().plus(Typography.ellipsis) + } else { + this + } +} + +/** + * Removes the string from kotlin.jvm.internal.Reflection#REFLECTION_NOT_AVAILABLE + */ +internal fun String.wfRemoveReflectionNotAvailable() = replace( + " (Kotlin reflection is not available)", + "" +) + +/** + * Returns the contents of the receiving string with all "com.squareup.*" packages + * stripped out of it. + * + * This will help make things more readable for classes within this library. + */ +internal fun String.wfStripSquarePackage(): String { + // Find the index of every "com.squareup". + var cursor = 0 + var packages: MutableList? = null + do { + val packageNameIndex = indexOf("com.squareup", cursor) + if (packageNameIndex > -1) { + if (packages == null) packages = ArrayList() + packages.add(packageNameIndex) + cursor = packageNameIndex + 1 + } else { + break + } + } while (cursor < length) + + // None found, punt. Note that we haven't allocated anything yet, that's nice. + if (packages == null) return this + + val builder: StringBuilder = StringBuilder() + cursor = 0 + for (packageNameIndex in packages) { + // Append everything before the next package + builder.append(substring(cursor, packageNameIndex)) + + // Skip the package name. + cursor = packageNameIndex + do { + val c = this[cursor] + if (c in 'a'..'z' || c in '0'..'9' || c == '.') { + cursor++ + } else { + break + } + } while (cursor < length) + } + + // Append everything after the last package name. + builder.append(substring(cursor)) + return builder.toString() +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeTraceContext.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeTraceContext.kt new file mode 100644 index 0000000000..c89b7ffcf3 --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeTraceContext.kt @@ -0,0 +1,56 @@ +package com.squareup.workflow1.tracing + +import androidx.collection.MutableLongObjectMap +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession + +/** + * Context information about a workflow runtime that is tracked by [WorkflowRuntimeMonitor]. + */ +public interface RuntimeTraceContext { + /** + * The name of the runtime. + */ + public val runtimeName: String + + /** + * A map of [WorkflowSessionInfo] that keeps track of active [WorkflowSession] keyed by the + * sessionId (which is a Long). + */ + public val workflowSessionInfo: MutableLongObjectMap + + /** + * Snapshot of the [RuntimeConfig]. + */ + public var configSnapshot: ConfigSnapshot + + /** + * A list of all causes for the current runtime loop processing (it can be multiple in the case + * of some optimizations). + */ + public val renderIncomingCauses: MutableList + + public var previousRenderCause: RenderCause? + public var currentRenderCause: RenderCause? + + /** + * Add an update into the [RuntimeUpdates] tracked by [WorkflowRuntimeMonitor]. See more + * information about those at [RuntimeUpdateLogLine]. + * + * Consider calling this from the `onNavigate` function you provide to + * `reportNavigation`, e.g. + * + * val renderings: Flow by lazy { + * renderWorkflowIn( + * workflow = RootNavigationWorkflow, + * scope = viewModelScope, + * savedStateHandle = savedState, + * runtimeConfig = RuntimeConfigOptions.ALL + * ).reportNavigation { + * runtimeMonitor.addRuntimeUpdate( + * UiUpdateLogLine(getWfLogString(it)) + * ) + * } + */ + public fun addRuntimeUpdate(event: RuntimeUpdateLogLine) +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeUpdateLogLine.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeUpdateLogLine.kt new file mode 100644 index 0000000000..3ff75f9e75 --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/RuntimeUpdateLogLine.kt @@ -0,0 +1,131 @@ +package com.squareup.workflow1.tracing + +import com.squareup.workflow1.WorkflowOutput +import com.squareup.workflow1.tracing.ActionAppliedLogLine.WorkflowActionLogType.CASCADE +import com.squareup.workflow1.tracing.ActionAppliedLogLine.WorkflowActionLogType.RENDERING_CALLBACK +import com.squareup.workflow1.tracing.ActionAppliedLogLine.WorkflowActionLogType.WORKER_OUTPUT + +/** + * PLEASE NOTE: these log lines are turned into strings in production, all the time, and there + * are many of them. + * So we take good care of keeping this code tight and performant, by leveraging StringBuilder and + * not doing any Kotlin string interpolation (which creates intermediate builder objects) as well + * as preferring the Java builder methods over the kotlin extension functions (e.g. append('\n') + * instead of appendLine()): even though the Kotlin extension functions are inlined, they also + * strengthen the types from the Java compatibility "maybe nullable" to "definitely not null" and + * the compiler adds 2 call to Intrinsics.checkNotNullExpressionValue per use (for the callee and + * the result, both of which are StringBuilder!). + */ +public sealed interface RuntimeUpdateLogLine { + fun log(builder: StringBuilder) +} + +/** + * The "UI" has updated, whatever that means for your app. + * You must manually add this to the [WorkflowRuntimeMonitor]'s [RuntimeUpdates] by calling + * [WorkflowRuntimeMonitor.addRuntimeUpdate]. + */ +public class UiUpdateLogLine( + val note: String +) : RuntimeUpdateLogLine { + override fun log(builder: StringBuilder) { + builder + .append("UI UPDATE: ") + .append(note) + .append('\n') + } +} + +/** + * The Workflow runtime has executed a render pass. + */ +public data object RenderLogLine : RuntimeUpdateLogLine { + override fun log(builder: StringBuilder) { + builder + .append("RENDERED") + .append('\n') + } +} + +/** + * The Workflow runtime has skipped a render pass. + */ +public data object SkipLogLine : RuntimeUpdateLogLine { + override fun log(builder: StringBuilder) { + builder + .append("SKIP RENDER") + .append('\n') + } +} + +/** + * The Workflow runtime has applied an action. + */ +public class ActionAppliedLogLine( + val type: WorkflowActionLogType, + val name: String, + val actionName: String, + val propsOrNull: Any?, + val oldState: Any?, + val newState: Any?, + val outputOrNull: WorkflowOutput<*>?, + val outputReceivedString: String?, +) : RuntimeUpdateLogLine { + + public enum class WorkflowActionLogType { + RENDERING_CALLBACK, + WORKER_OUTPUT, + CASCADE, + } + + override fun log(builder: StringBuilder) { + val lineBuilder = StringBuilder().apply { + when (type) { + RENDERING_CALLBACK -> append("Rendering Callback: ") + WORKER_OUTPUT -> append("Worker Output: ") + CASCADE -> append("Cascade: ") + } + if (outputReceivedString != null) { + append(outputReceivedString) + append(": ") + } + if (actionName.isNotBlank()) { + append("A(") + append(actionName) + append(")") + append("/") + } + append(name) + append(": ") + append( + if (outputOrNull != null) { + getWfLogString(outputOrNull.value) + } else { + "(no output)" + } + ) + append('\n') + + propsOrNull?.let { + append(" props = ") + append(getWfLogString(it)) + append('\n') + } + + if (oldState != newState) { + append(" oldState = ") + append(getWfLogString(oldState)) + append('\n') + append(" newState = ") + append(getWfLogString(newState)) + append('\n') + } else { + append(" state = ") + append(getWfLogString(oldState)) + append('\n') + } + } + + builder.append(lineBuilder) + } +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/SafeTraceInterface.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/SafeTraceInterface.kt new file mode 100644 index 0000000000..e72f8a75a3 --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/SafeTraceInterface.kt @@ -0,0 +1,23 @@ +package com.squareup.workflow1.tracing + +/** + * Interface abstracting tracing functionality to allow for testing with fake implementations. + */ +interface SafeTraceInterface { + val isTraceable: Boolean + val isCurrentlyTracing: Boolean + + fun beginSection(label: String) + fun endSection() + fun beginAsyncSection( + name: String, + cookie: Int + ) + + fun endAsyncSection( + name: String, + cookie: Int + ) + + fun logSection(info: String) +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRenderPassTracker.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRenderPassTracker.kt new file mode 100644 index 0000000000..008e5e52cc --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRenderPassTracker.kt @@ -0,0 +1,88 @@ +package com.squareup.workflow1.tracing + +import kotlin.time.Duration + +/** + * Can be passed to a [WorkflowRuntimeMonitor] to track each render pass as it happens, and the + * cause of it. + */ +fun interface WorkflowRenderPassTracker { + + /** + * Records that a render pass happened. + */ + fun recordRenderPass(renderPass: RenderPassInfo) +} + +/** + * A bundle of little info about a render pass. + */ +class RenderPassInfo( + val runnerName: String, + val renderCause: RenderCause, + val durationUptime: Duration +) + +/** + * Explanation of what caused a render pass. [toString] implementations + * provide a concise, Perfetto-friendly description with key: + * + * - A(action name) + * - R(worker name) + * - W(workflow name) + */ +public sealed interface RenderCause { + /** + * The props passed into the runtime have changed. + */ + public object RootPropsChanged : RenderCause { + override fun toString() = "Root Props changed" + } + + /** + * First creation of the root workflow for the runtime. + */ + public class RootCreation( + val runnerName: String, + val workflowName: String, + ) : RenderCause { + override fun toString(): String { + return "Creation of $runnerName root workflow $workflowName" + } + } + + /** + * An action was handled. + */ + public class Action( + val actionName: String, + val workerIncomingName: String?, + val workflowName: String, + ) : RenderCause { + override fun toString(): String { + return "Output:A($actionName)/R($workerIncomingName)/W($workflowName)" + } + } + + /** + * A worker's wrapping workflow is waiting to receive the output. + * This should not ever be the only cause of a render pass. + */ + public class WaitingForOutput(val workflowName: String) : RenderCause { + override fun toString(): String { + return "W($workflowName)/WaitingForOutput" + } + } + + /** + * A rendering callback was invoked. + */ + public class Callback( + val actionName: String, + val workflowName: String, + ) : RenderCause { + override fun toString(): String { + return "Callback:A($actionName)/W($workflowName)" + } + } +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeLoopListener.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeLoopListener.kt new file mode 100644 index 0000000000..7c9582d888 --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeLoopListener.kt @@ -0,0 +1,38 @@ +package com.squareup.workflow1.tracing + +/** + * Can be passed to a [WorkflowRuntimeMonitor] listen for every runtime loop that executes. The list + * of [RuntimeUpdates] in this runtime loop will be provided. + * + * This can be extremely useful for establishing a "trail of breadcrumbs" for what your + * application has done. + */ +public fun interface WorkflowRuntimeLoopListener { + + /** + * Called whenever the runtime loop completes with all the update events that have happened in + * that loop. + */ + public fun onRuntimeLoopTick( + configSnapshot: ConfigSnapshot, + runtimeUpdates: RuntimeUpdates + ) +} + +/** + * Simple wrapper object for a list of [RuntimeUpdateLogLine]s. This allows us to add updates + * and provides a [readAndClear] API to clear the list after it is read. + */ +public class RuntimeUpdates { + private val updateLines = mutableListOf() + internal fun logUpdate(updateLine: RuntimeUpdateLogLine) { + updateLines += updateLine + } + + /** + * Get the list of [RuntimeUpdateLogLine]s and then clear it. + */ + public fun readAndClear(): List = updateLines.toList().also { + updateLines.clear() + } +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeMonitor.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeMonitor.kt new file mode 100644 index 0000000000..985e4021fd --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeMonitor.kt @@ -0,0 +1,519 @@ +package com.squareup.workflow1.tracing + +import androidx.collection.mutableLongObjectMapOf +import com.squareup.workflow1.ActionApplied +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.RenderingAndSnapshot +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.Worker +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor +import com.squareup.workflow1.WorkflowInterceptor.RenderPassSkipped +import com.squareup.workflow1.WorkflowInterceptor.RenderingConflated +import com.squareup.workflow1.WorkflowInterceptor.RenderingProduced +import com.squareup.workflow1.WorkflowInterceptor.RuntimeLoopTick +import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.applyTo +import com.squareup.workflow1.tracing.ActionAppliedLogLine.WorkflowActionLogType +import com.squareup.workflow1.tracing.ActionAppliedLogLine.WorkflowActionLogType.CASCADE +import com.squareup.workflow1.tracing.ActionAppliedLogLine.WorkflowActionLogType.RENDERING_CALLBACK +import com.squareup.workflow1.tracing.ActionAppliedLogLine.WorkflowActionLogType.WORKER_OUTPUT +import com.squareup.workflow1.tracing.RenderCause.Action +import com.squareup.workflow1.tracing.RenderCause.Callback +import com.squareup.workflow1.tracing.RenderCause.RootCreation +import com.squareup.workflow1.tracing.RenderCause.RootPropsChanged +import com.squareup.workflow1.tracing.RenderCause.WaitingForOutput +import com.squareup.workflow1.tracing.WorkflowRuntimeMonitor.ActionType.CascadeAction +import com.squareup.workflow1.tracing.WorkflowRuntimeMonitor.ActionType.QueuedAction +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlin.time.Duration.Companion.nanoseconds + +/** + * This class has the following responsibilities: + * 1. Collects records of each workflow action and each render pass with causes etc. (this is held + * by the [RuntimeTraceContext].) + * 2. Forwards that information and the runtime update events to any [workflowRuntimeTracers]. + * 3. Tracks render passes via [renderPassTracker]. + * 4. Sends an update of all events that have occurred for each runtime loop to the [runtimeLoopListener]. + */ +public class WorkflowRuntimeMonitor( + override val runtimeName: String, + private val workflowRuntimeTracers: List = emptyList(), + private val renderPassTracker: WorkflowRenderPassTracker? = null, + private val runtimeLoopListener: WorkflowRuntimeLoopListener? = null, +) : WorkflowInterceptor, RuntimeTraceContext { + + private val chainedWorkflowRuntimeTracer: WorkflowRuntimeTracer = workflowRuntimeTracers + .chained().apply { + attachRuntimeContext(this@WorkflowRuntimeMonitor) + } + + private var workerIncomingName: String? = null + private val runtimeUpdates = RuntimeUpdates() + + private val rendering: Boolean + get() = currentRenderCause != null + + private var lastRootProps: Any? = null + private var currentActionHandlingChangedState = false + + // Cache workflow names by session id to save time. + override val workflowSessionInfo = mutableLongObjectMapOf() + override val renderIncomingCauses: MutableList = mutableListOf() + override var previousRenderCause: RenderCause? = null + override var currentRenderCause: RenderCause? = null + override lateinit var configSnapshot: ConfigSnapshot + + override fun addRuntimeUpdate(event: RuntimeUpdateLogLine) { + runtimeUpdates.logUpdate(event) + } + + /** + * Called the first time any Workflow is rendered - which starts its 'session'. + * @see [WorkflowSession] for more information on the lifecycle of a session. + */ + override fun onSessionStarted( + workflowScope: CoroutineScope, + session: WorkflowSession + ) { + onWorkflowStarted(session) + chainedWorkflowRuntimeTracer.onWorkflowSessionStarted(workflowScope, session) + + val workflowJob = workflowScope.coroutineContext[Job]!! + workflowJob.invokeOnCompletion { + onWorkflowStopped(session.sessionId) + chainedWorkflowRuntimeTracer.onWorkflowSessionStopped(session.sessionId) + } + } + + /** + * Helper method to populate tracking meta-data for a [WorkflowSession]. + * + * Note that if `session.parent == null` (this is the root workflow), then this is actually called + * before [onRenderAndSnapshot] as the root workflow's node is created. + */ + private fun onWorkflowStarted( + session: WorkflowSession + ) { + val sessionInfo = WorkflowSessionInfo(session) + workflowSessionInfo[session.sessionId] = sessionInfo + + if (session.isRootWorkflow) { + // Cache the config snapshot for this whole runtime. + configSnapshot = ConfigSnapshot(session.runtimeConfig) + check(renderIncomingCauses.isEmpty()) { + "Workflow runtime for $runtimeName already has incoming render on creation triggered by " + + "${renderIncomingCauses.lastOrNull()}" + } + renderIncomingCauses.add( + RootCreation( + runnerName = runtimeName, + workflowName = sessionInfo.logName + ) + ) + } else { + check(rendering) { + "Non root workflow ${sessionInfo.name} in $runtimeName first created outside of a" + + " rendering. This should not be possible." + } + } + } + + /** + * Helper function called when the job backing the [WorkflowSession] ends. + */ + private fun onWorkflowStopped(workflowSessionId: Long) { + workflowSessionInfo -= workflowSessionId + } + + /** + * Forwards calls to [Workflow::initialState] to [workflowRuntimeTracers]. + */ + override fun onInitialState( + props: P, + snapshot: Snapshot?, + workflowScope: CoroutineScope, + proceed: (P, Snapshot?, CoroutineScope) -> S, + session: WorkflowSession + ): S { + if (session.isRootWorkflow) { + // For the root workflow, this is called before [onRenderAndSnapshot]. Setup the 'lastProps' + // before that, so we don't think they've changed. We've set the `renderIncomingCause` as + // creation in `onWorkflowStarted`. + lastRootProps = props + } + return chainedWorkflowRuntimeTracer.onInitialState( + props, + snapshot, + workflowScope, + proceed, + session + ) + } + + /** + * Forwards all calls to [Workflow::onPropsChanged] to the [workflowRuntimeTracers]. + */ + override fun onPropsChanged( + old: P, + new: P, + state: S, + proceed: (P, P, S) -> S, + session: WorkflowSession + ): S { + return chainedWorkflowRuntimeTracer.onPropsChanged(old, new, state, proceed, session) + } + + /** + * Instruments a full 'render pass', which includes render for the whole tree, as well as + * snapshotting the whole tree. We forward to [workflowRuntimeTracers], and also count it with the + * [renderPassTracker]. + */ + override fun onRenderAndSnapshot( + renderProps: P, + proceed: (P) -> RenderingAndSnapshot, + session: WorkflowSession + ): RenderingAndSnapshot { + // Workflow deduplicates new props that are equal, so we don't need to do an equality check here + // we know that !== is semantically equivalent to != (and faster) + if (renderProps !== lastRootProps) { + check(renderIncomingCauses.isEmpty()) { + "$runtimeName onRenderAndSnapshot() triggered by changing props, should not already have " + + "render incoming ${renderIncomingCauses.lastOrNull()}" + } + lastRootProps = renderProps + renderIncomingCauses.add(RootPropsChanged) + chainedWorkflowRuntimeTracer.onRootPropsChanged(session) + } else { + check(renderIncomingCauses.isNotEmpty()) { + "$runtimeName onRenderAndSnapshot() even though renderIncomingCauses is empty. " + + "previousRenderCause=$previousRenderCause, " + + "rendering=${currentRenderCause != null}" + } + } + + check(!rendering) { + "$runtimeName onRenderAndSnapshot() even though rendering already true. " + + "renderIncomingCause=${renderIncomingCauses.lastOrNull()}" + } + // At this point renderIncomingCause can't be empty, we covered all cases. + // The last render cause will be the most recent one. + val localCurrentRenderCause = renderIncomingCauses.last() + previousRenderCause = localCurrentRenderCause + currentRenderCause = localCurrentRenderCause + workerIncomingName = null + + val renderPassStartUptimeNanos = System.nanoTime() + return chainedWorkflowRuntimeTracer + .onRenderAndSnapshot(renderProps, proceed, session) + .also { + currentRenderCause = null + runtimeUpdates.logUpdate(RenderLogLine) + val renderPassDurationUptimeNanos = System.nanoTime() - renderPassStartUptimeNanos + renderPassTracker?.recordRenderPass( + RenderPassInfo( + runnerName = runtimeName, + renderCause = localCurrentRenderCause, + durationUptime = renderPassDurationUptimeNanos.nanoseconds, + ) + ) + } + } + + /** + * Instruments all calls to [Workflow::render], forwarding them to [workflowRuntimeTracers]. + */ + override fun onRender( + renderProps: P, + renderState: S, + context: BaseRenderContext, + proceed: (P, S, RenderContextInterceptor?) -> R, + session: WorkflowSession + ): R { + check(rendering) { + "$runtimeName should be rendering" + } + + val monitoringRenderContextInterceptor = MonitoringRenderContextInterceptor( + workflowName = requireNotNull(workflowSessionInfo[session.sessionId]) { + "Expected session info for sessionId ${session.sessionId} but found none." + }.logName + ) + return chainedWorkflowRuntimeTracer.onRender( + renderProps = renderProps, + renderState = renderState, + context = context, + session = session, + proceed = { p: P, s: S, rci: RenderContextInterceptor? -> + proceed( + p, + s, + rci?.let { monitoringRenderContextInterceptor.wrap(rci) } + ?: monitoringRenderContextInterceptor + ) + } + ) + } + + /** + * Instruments all calls to [Workflow::snapshotState], forwarding them to [workflowRuntimeTracers]. + */ + override fun onSnapshotStateWithChildren( + proceed: () -> TreeSnapshot, + session: WorkflowSession + ): TreeSnapshot { + return chainedWorkflowRuntimeTracer.onSnapshotStateWithChildren(proceed, session) + } + + /** + * Updates the [runtimeLoopListener], instruments the render pass or skip. + */ + override fun onRuntimeUpdate(update: RuntimeUpdate) { + chainedWorkflowRuntimeTracer.onRuntimeUpdateEnhanced( + update, + currentActionHandlingChangedState, + configSnapshot + ) + when (update) { + RenderPassSkipped -> { + previousRenderCause = renderIncomingCauses.lastOrNull() + runtimeUpdates.logUpdate(SkipLogLine) + } + + RenderingConflated -> { + // runtimeUpdates.logUpdate(ConflatedLogLine) + } + + RenderingProduced -> { + // runtimeUpdates.logUpdate(ProducedLogLine) + } + + RuntimeLoopTick -> { + runtimeLoopListener?.onRuntimeLoopTick( + configSnapshot, + runtimeUpdates + ) + currentActionHandlingChangedState = false + renderIncomingCauses.clear() + } + } + } + + /** + * Wrapped [RenderContextInterceptor] that adds needed metadata tracking for children, side effects, + * and actions. + */ + private inner class MonitoringRenderContextInterceptor( + private val workflowName: String + ) : RenderContextInterceptor { + + /** + * Instruments when an action is sent to the actionSink by a Workflow's handler (either + * an event handler from the UI, or `renderChild`/`runningWorker` output handler). + * + * We wrap the action here with a [RuntimeMonitoringAction] so that we can monitor when it gets + * applied. + * Any action sent to the RenderContext's actionSink will become part of the global "queue" of + * actions that the Workflow runtime is looping over to process. The runtime will resume the loop + * when any of these actions can be processed, which then initiates a render pass to update the + * rendering after the (presumed) state change. Because these actions are coming off the queue to + * start an action cascade, we call these types of actions [QueuedAction]. + * + * The render pass will not occur if it is skipped due to the state equality optimization. + */ + override fun onActionSent( + action: WorkflowAction, + proceed: (WorkflowAction) -> Unit + ) { + proceed( + RuntimeMonitoringAction( + delegateAction = action, + actionName = action.toLoggingShortName(), + actionType = QueuedAction, + ) + ) + } + + /** + * This intercepts the corresponding output handler for the child to add wrapping + * to the action that is produced from it. We wrap this action with [RuntimeMonitoringAction] + * but we specify it as a [CascadeAction] as this is an action that is applied synchronously + * as part of an action cascade that originated with a [QueuedAction]. + * + * Note that for every [com.squareup.workflow1.Worker] a child workflow is created and rendered, + * so this will be part of the callstack for that. In that case the output of the handler will + * contain the worker action signature. We can detect that with + * [Worker.WORKER_OUTPUT_ACTION_NAME]. + */ + override fun onRenderChild( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction, + proceed: ( + child: Workflow, + childProps: CP, + key: String, + handler: (CO) -> WorkflowAction + ) -> CR + ): CR { + return proceed(child, childProps, key) { output -> + val childOutputString = getWfLogString(output) + val delegateAction = handler(output) + val actionName = delegateAction.toLoggingShortName() + RuntimeMonitoringAction( + delegateAction = delegateAction, + actionName = actionName, + actionType = CascadeAction( + childOutputString = childOutputString + ), + ) + } + } + + /** + * Class to instrument the application of actions. We simply wrap the [delegateAction] with + * meta-data that helps us trace it. This class is used for both a [QueuedAction] that was sent to + * the actionSink by an asynchronous event, and for a [CascadeAction] that is applied synchronously + * as part of the action cascade. That is specified by [actionType]. + */ + private inner class RuntimeMonitoringAction( + private val delegateAction: WorkflowAction, + private val actionName: String, + private val actionType: ActionType + ) : WorkflowAction() { + // Use the delegate [debuggingName] so from the perspective of breadcrumbs this tracing wrapper + // is invisible. + override val debuggingName: String + get() = delegateAction.debuggingName + + /** + * Adds instrumentation to the application of an action (a state change). + * This is also where we establish rendering causes from whatever [QueuedAction] was applied + * to start the action cascade. + */ + override fun Updater.apply() { + // See https://github.com/square/workflow-kotlin/issues/391. We have to listen to the 2nd + // action in the cascade to get a useful ref on which Worker's handler was firing. This is + // because Workers use an underlying Workflow and an intermediate action, which is the + // QueuedAction, in their implementation. So yes, we use an implementation detail here to + // detect that. The issue still tracks upstreaming this into the library. + val isWorkerQueuedAction = actionName.contains(Worker.WORKER_OUTPUT_ACTION_NAME) + // This is non-null when we are applying the action from the Worker Output handler, in that + // case it is equal to the name of the underlying Worker workflow, which is the type of the + // worker. + val workerLogName: String? = workerIncomingName + if (actionType is QueuedAction) { + val newRenderingCause: RenderCause = if (isWorkerQueuedAction) { + workerIncomingName = workflowName + WaitingForOutput(workflowName) + } else { + Callback(actionName, workflowName) + } + renderIncomingCauses.add(newRenderingCause) + } else if ( + actionType is CascadeAction && + workerIncomingName != null + ) { + // WaitingForOutput should be the last cause added. + val lastCause = renderIncomingCauses.removeLastOrNull() + check(lastCause!! is WaitingForOutput) { + "Expecting to receive action handling for worker output. Instead $lastCause." + } + // This is the real output handler action from the runningWorker call, thus the more + // 'recognizable' cause of the render pass. + renderIncomingCauses.add(Action(actionName, workerIncomingName, workflowName)) + workerIncomingName = null + } + val oldState = state + // Apply the actual action! + val (newState, actionApplied) = delegateAction.applyTo(props, state) + .also { (newState, actionApplied) -> + state = newState + actionApplied.output?.let { setOutput(it.value) } + } + currentActionHandlingChangedState = + // In a cascade from a child or applying multiple actions. + currentActionHandlingChangedState || + actionApplied.stateChanged + + if (!isWorkerQueuedAction) { + // Do not log for the first 'QueueAction' of the Worker (implementation detail that is + // handled in the logging of the first CascadeAction). + logActionApplied( + workerCause = workerLogName, + actionName = actionName, + actionType = actionType, + props = props, + oldState = oldState, + newState = newState, + actionApplied = actionApplied + ) + } + } + + private fun logActionApplied( + workerCause: String?, + actionName: String, + actionType: ActionType, + props: P, + oldState: S, + newState: S, + actionApplied: ActionApplied, + ) { + val workflowLogType: WorkflowActionLogType = if (workerCause != null) { + WORKER_OUTPUT + } else if (actionType is QueuedAction) { + // We don't call `logOutcome` for the initial Worker queued action. So all are callbacks. + RENDERING_CALLBACK + } else { + CASCADE + } + val outputReceivedString: String? = if (actionType is CascadeAction) { + actionType.childOutputString + } else { + null + } + val name = if (workerCause != null) { + "R($workerCause)/W($workflowName)" + } else { + "W($workflowName)" + } + runtimeUpdates.logUpdate( + ActionAppliedLogLine( + type = workflowLogType, + name = name, + actionName = actionName, + propsOrNull = props.takeIf { it !is Unit }, + oldState = oldState, + newState = newState, + outputOrNull = actionApplied.output, + outputReceivedString = outputReceivedString, + ) + ) + } + } + } + + public sealed interface ActionType { + /** + * A [QueuedAction] is an action that is queued at the event sink (from Worker Output or a + * Rendering Callback. + */ + public data object QueuedAction : ActionType + + /** + * A [CascadeAction] is an action applied as part of the cascade of Output up the hierarchy. + * + * @param childOutputString contains debug info about the output received by this action. + */ + public class CascadeAction( + val childOutputString: String, + ) : ActionType + } +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeTracer.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeTracer.kt new file mode 100644 index 0000000000..43ffb89718 --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowRuntimeTracer.kt @@ -0,0 +1,109 @@ +package com.squareup.workflow1.tracing + +import androidx.collection.LongObjectMap +import com.squareup.workflow1.WorkflowInterceptor +import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import kotlinx.coroutines.CoroutineScope + +/** + * Interface for a pluggable [WorkflowRuntimeTracer] that can be used with the monitoring + * provided by [WorkflowRuntimeMonitor]. Note that this extends [WorkflowInterceptor] to allow + * [WorkflowRuntimeTracer]s to use those methods as well for tracing, except in a couple of + * cases where it provides enhanced hooks, it prevents the underlying hooks from being used. + */ +public abstract class WorkflowRuntimeTracer : WorkflowInterceptor { + + protected lateinit var workflowRuntimeTraceContext: RuntimeTraceContext + + /** + * Initialize the context for this tracer by attaching it to a [RuntimeTraceContext]. + * + * This should not really be overridden (only [ChainedWorkflowRuntimeTracer] uses that + * to chain this for all [WorkflowRuntimeTracer]). + */ + public open fun attachRuntimeContext( + workflowRuntimeTraceContext: RuntimeTraceContext + ) { + this.workflowRuntimeTraceContext = workflowRuntimeTraceContext + } + + protected val sessionInfo: LongObjectMap + inline get() = workflowRuntimeTraceContext.workflowSessionInfo + + protected val WorkflowSession.name: String + inline get() = requireNotNull(sessionInfo[sessionId]) { + "Expected session info for sessionId $sessionId but found none." + }.name + + protected val WorkflowSession.key: String + inline get() = requireNotNull(sessionInfo[sessionId]) { + "Expected session info for sessionId $sessionId but found none." + }.key + + protected val WorkflowSession.logName: String + inline get() = requireNotNull(sessionInfo[sessionId]) { + "Expected session info for sessionId $sessionId but found none." + }.logName + + protected val WorkflowSession.traceName: String + inline get() = requireNotNull(sessionInfo[sessionId]) { + "Expected session info for sessionId $sessionId but found none." + }.traceName + + /** + * The props passed into the root workflow have changed. + */ + public open fun onRootPropsChanged( + session: WorkflowSession, + ) = Unit + + /** + * A workflow session has started. + */ + public open fun onWorkflowSessionStarted( + workflowScope: CoroutineScope, + session: WorkflowSession + ) = Unit + + /** + * A workflow session has stopped. + */ + public open fun onWorkflowSessionStopped( + sessionId: Long + ) = Unit + + /** + * A [RuntimeUpdate] has occurred, we may want to trace something. This is to be preferred to + * the [WorkflowInterceptor.onRuntimeUpdate] method as it provides more context about what is + * currently happening in the runtime. + */ + public open fun onRuntimeUpdateEnhanced( + runtimeUpdate: RuntimeUpdate, + currentActionHandlingChangedState: Boolean, + configSnapshot: ConfigSnapshot, + ) = Unit + + /** SECTION: [WorkflowInterceptor] overrides. */ + + /** + * Prevents [WorkflowRuntimeTracer]s from overriding this method, they should use + * [onWorkflowSessionStarted] instead. + */ + final override fun onSessionStarted( + workflowScope: CoroutineScope, + session: WorkflowSession + ) { + super.onSessionStarted(workflowScope, session) + } + + /** + * Prevent [WorkflowRuntimeTracer] from overriding this function, as they should use + * [onRuntimeUpdateEnhanced] instead. + */ + final override fun onRuntimeUpdate(update: RuntimeUpdate) { + super.onRuntimeUpdate(update) + } + + /** END SECTION [WorkflowInterceptor] overrides. */ +} diff --git a/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowSessionInfo.kt b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowSessionInfo.kt new file mode 100644 index 0000000000..4bdabd7acb --- /dev/null +++ b/workflow-tracing/src/main/java/com/squareup/workflow1/tracing/WorkflowSessionInfo.kt @@ -0,0 +1,41 @@ +package com.squareup.workflow1.tracing + +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession + +/** + * Little bundle of data that is cached by the [WorkflowRuntimeMonitor] for each [WorkflowSession]. + * + * @param name - the shorter name of the workflow used for tracing. + * @param key - the key that the workflow was rendered with (could be empty). + */ +public class WorkflowSessionInfo( + public val name: String, + public val key: String, +) { + + public constructor( + session: WorkflowSession, + ) : this( + name = session.identifier.toWfLoggingName(), + // Keys can be long, ellipsize + key = session.renderKey.wfEllipsizeEnd(MAX_KEY_LENGTH) + ) + + public val logName: String = if (key.isEmpty()) { + name + } else { + "$name($key)" + } + + public val traceName: String = logName.wfEllipsizeEnd(MAX_TRACE_NAME_LENGTH) + + companion object { + + /** + * Reasonable limits so that we can get more concatenated info into trace section labels capped + * at 127 chars. + */ + const val MAX_KEY_LENGTH: Int = 15 + const val MAX_TRACE_NAME_LENGTH: Int = 50 + } +} diff --git a/workflow-tracing/src/test/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptorTest.kt b/workflow-tracing/src/test/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptorTest.kt deleted file mode 100644 index 0ba23d2c40..0000000000 --- a/workflow-tracing/src/test/java/com/squareup/workflow1/diagnostic/tracing/TracingWorkflowInterceptorTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.squareup.workflow1.diagnostic.tracing - -import com.squareup.tracing.TimeMark -import com.squareup.tracing.TraceEncoder -import com.squareup.workflow1.Snapshot -import com.squareup.workflow1.StatefulWorkflow -import com.squareup.workflow1.action -import com.squareup.workflow1.asWorker -import com.squareup.workflow1.renderWorkflowIn -import com.squareup.workflow1.runningWorker -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers.Unconfined -import kotlinx.coroutines.cancel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.Channel.Factory.UNLIMITED -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.receiveAsFlow -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.takeWhile -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.yield -import okio.Buffer -import okio.buffer -import okio.source -import org.mockito.kotlin.mock -import kotlin.test.Test -import kotlin.test.assertEquals - -internal class TracingWorkflowInterceptorTest { - - private lateinit var onGcDetected: () -> Unit - - @Test fun `golden value`() { - val buffer = Buffer() - val memoryStats = object : MemoryStats { - override fun freeMemory(): Long = 42 - override fun totalMemory(): Long = 43 - } - val gcDetector = mock() - val scope = CoroutineScope(Unconfined) - val encoder = TraceEncoder( - scope = scope, - start = ZeroTimeMark, - ioDispatcher = Unconfined, - sinkProvider = { buffer } - ) - val listener = TracingWorkflowInterceptor( - memoryStats = memoryStats, - gcDetectorConstructor = { - onGcDetected = it - gcDetector - } - ) { workflowScope, type -> - provideLogger("", workflowScope, type) { encoder } - } - val props = (0..100).asFlow() - // Real use cases almost never feed a firehose of changing root props, they change rarely if - // at all, and almost certainly allow processing of dispatched coroutines in between. This - // yield represents that more accurately. - .onEach { - yield() - yield() - } - - runBlocking(scope.coroutineContext) { - val renderings = renderWorkflowIn( - TestWorkflow(), - scope, - props.stateIn(this), - interceptors = listOf(listener), - onOutput = {} - ).map { it.rendering } - - renderings.takeWhile { it != "final" } - .collect() - } - scope.cancel() - - val expected = TracingWorkflowInterceptorTest::class.java - .getResourceAsStream("expected_trace_file.txt") - .source() - .buffer() - .readUtf8() - assertEquals(expected, buffer.readUtf8().removeActionHashCodes()) - } - - inner class TestWorkflow : StatefulWorkflow() { - - private val channel = Channel(UNLIMITED) - - override fun toString(): String = - "TestWorkflow" - - fun triggerWorker(value: String) { - channel.trySend(value).isSuccess - } - - override fun initialState( - props: Int, - snapshot: Snapshot? - ): String { - // Pretend to detect a garbage collection whenever a workflow starts. - onGcDetected() - return "initial" - } - - override fun onPropsChanged( - old: Int, - new: Int, - state: String - ): String { - if (old == 2 && new == 3) triggerWorker("fired!") - return if (old == 0 && new == 1) "changed state" else state - } - - override fun render( - renderProps: Int, - renderState: String, - context: StatefulWorkflow.RenderContext - ): String { - if (renderProps == 0) return "initial" - if (renderProps in 1..6) context.renderChild(this, 0) { bubbleUp(it) } - if (renderProps in 4..5) context.renderChild(this, props = 1, key = "second") { bubbleUp(it) } - if (renderProps in 2..3) { - context.runningWorker( - channel.receiveAsFlow() - .asWorker() - ) { bubbleUp(it) } - } - - return if (renderProps > 10) "final" else "rendering" - } - - override fun snapshotState(state: String): Snapshot? = null - - private fun bubbleUp(output: String) = action("bubbleUp") { setOutput(output) } - } -} - -// [WorkflowAction::toString] includes "@-${hashCode()}", so strip it. -private fun String.removeActionHashCodes(): String = replace(Regex("-@([0-9]*)"), "") - -private object ZeroTimeMark : TimeMark { - override val elapsedNow: Long = 0L -} diff --git a/workflow-tracing/src/test/java/com/squareup/workflow1/tracing/WorkflowRuntimeMonitorTest.kt b/workflow-tracing/src/test/java/com/squareup/workflow1/tracing/WorkflowRuntimeMonitorTest.kt new file mode 100644 index 0000000000..c185d1f33c --- /dev/null +++ b/workflow-tracing/src/test/java/com/squareup/workflow1/tracing/WorkflowRuntimeMonitorTest.kt @@ -0,0 +1,835 @@ +package com.squareup.workflow1.tracing + +import com.squareup.workflow1.BaseRenderContext +import com.squareup.workflow1.RenderingAndSnapshot +import com.squareup.workflow1.RuntimeConfig +import com.squareup.workflow1.RuntimeConfigOptions +import com.squareup.workflow1.Sink +import com.squareup.workflow1.Snapshot +import com.squareup.workflow1.StatefulWorkflow +import com.squareup.workflow1.TreeSnapshot +import com.squareup.workflow1.Workflow +import com.squareup.workflow1.WorkflowAction +import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor +import com.squareup.workflow1.WorkflowInterceptor.RenderPassSkipped +import com.squareup.workflow1.WorkflowInterceptor.RenderingConflated +import com.squareup.workflow1.WorkflowInterceptor.RenderingProduced +import com.squareup.workflow1.WorkflowInterceptor.RuntimeLoopTick +import com.squareup.workflow1.WorkflowInterceptor.RuntimeUpdate +import com.squareup.workflow1.WorkflowInterceptor.WorkflowSession +import com.squareup.workflow1.identifier +import com.squareup.workflow1.tracing.RenderCause.RootCreation +import com.squareup.workflow1.tracing.RenderCause.RootPropsChanged +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.TestScope +import kotlin.reflect.KType +import kotlin.test.Test +import kotlin.test.assertContains +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +internal class WorkflowRuntimeMonitorTest { + + private val runtimeName = "TestRuntime" + private val fakeRuntimeTracer = TestWorkflowRuntimeTracer() + private val fakeRenderPassTracker = TestWorkflowRenderPassTracker() + private val fakeRuntimeLoopListener = TestWorkflowRuntimeLoopListener() + + @Test + fun `monitor can be instantiated with empty dependencies`() { + val monitor = WorkflowRuntimeMonitor(runtimeName) + assertNotNull(monitor) + assertEquals(runtimeName, monitor.runtimeName) + } + + @Test + fun `monitor can be instantiated with all dependencies`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer), + renderPassTracker = fakeRenderPassTracker, + runtimeLoopListener = fakeRuntimeLoopListener + ) + assertNotNull(monitor) + assertEquals(runtimeName, monitor.runtimeName) + } + + @Test + fun `onSessionStarted handles root workflow session`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + monitor.onSessionStarted(testScope, rootSession) + + assertTrue(fakeRuntimeTracer.onWorkflowSessionStartedCalled) + assertEquals(1, monitor.workflowSessionInfo.size) + assertEquals(1, monitor.renderIncomingCauses.size) + assertTrue(monitor.renderIncomingCauses.first() is RootCreation) + } + + @Test + fun `onSessionStarted handles child workflow session`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val childSession = testWorkflow.createChildSession(rootSession) + val testScope = TestScope() + + // Start root first to establish rendering context + monitor.onSessionStarted(testScope, rootSession) + // Simulate rendering + monitor.currentRenderCause = RootCreation(runtimeName, "TestWorkflow") + + monitor.onSessionStarted(testScope, childSession) + + assertEquals(2, fakeRuntimeTracer.onWorkflowSessionStartedCallCount) + assertEquals(2, monitor.workflowSessionInfo.size) + } + + @Test + fun `onInitialState delegates to tracers and handles root workflow`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + val result = monitor.onInitialState( + props = "testProps", + snapshot = null, + workflowScope = testScope, + proceed = { _, _, _ -> "initialState" }, + session = rootSession + ) + + assertEquals("initialState", result) + assertTrue(fakeRuntimeTracer.onInitialStateCalled) + } + + @Test + fun `onPropsChanged delegates to tracers`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val session = testWorkflow.createRootSession() + + val result = monitor.onPropsChanged( + old = "oldProps", + new = "newProps", + state = "currentState", + proceed = { _, _, state -> state }, + session = session + ) + + assertEquals("currentState", result) + assertTrue(fakeRuntimeTracer.onPropsChangedCalled) + } + + @Test + fun `onRenderAndSnapshot handles root workflow rendering`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer), + renderPassTracker = fakeRenderPassTracker + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + val initialProps = "props" + + // Initialize session and set up render cause + monitor.onSessionStarted(testScope, rootSession) + monitor.onInitialState(initialProps, null, testScope, { _, _, _ -> "state" }, rootSession) + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + // Use the same props reference to simulate normal rendering (not props change) + val result = monitor.onRenderAndSnapshot( + renderProps = initialProps, + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + assertEquals(renderingAndSnapshot, result) + assertTrue(fakeRuntimeTracer.onRenderAndSnapshotCalled) + assertEquals(1, fakeRenderPassTracker.renderPassCount) + assertNotNull(monitor.previousRenderCause) + } + + @Test + fun `onRenderAndSnapshot handles props change`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize with initial props + monitor.onSessionStarted(testScope, rootSession) + monitor.onInitialState("initialProps", null, testScope, { _, _, _ -> "state" }, rootSession) + + // Clear the initial render causes + monitor.renderIncomingCauses.clear() + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + // Render with different props - this should trigger props change detection + val result = monitor.onRenderAndSnapshot( + renderProps = "newProps", + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + assertEquals(renderingAndSnapshot, result) + assertTrue(monitor.renderIncomingCauses.any { it is RootPropsChanged }) + assertTrue(fakeRuntimeTracer.onRootPropsChangedCalled) + } + + @Test + fun `onRender creates monitoring interceptor`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + val mockContext = TestBaseRenderContext() + + // Set up monitoring state + monitor.onSessionStarted(testScope, rootSession) + monitor.currentRenderCause = RootCreation(runtimeName, "TestWorkflow") + + var interceptorReceived: Any? = null + val result = monitor.onRender( + renderProps = "props", + renderState = "state", + context = mockContext, + proceed = { _, _, interceptor -> + interceptorReceived = interceptor + "rendered" + }, + session = rootSession + ) + + assertEquals("rendered", result) + assertNotNull(interceptorReceived) + assertTrue(fakeRuntimeTracer.onRenderCalled) + } + + @Test + fun `onSnapshotStateWithChildren delegates to tracers`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val session = testWorkflow.createRootSession() + val treeSnapshot = TreeSnapshot.forRootOnly(null) + + val result = monitor.onSnapshotStateWithChildren( + proceed = { treeSnapshot }, + session = session + ) + + assertEquals(treeSnapshot, result) + assertTrue(fakeRuntimeTracer.onSnapshotStateWithChildrenCalled) + } + + @Test + fun `onRuntimeUpdate handles RenderPassSkipped`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize session to set up configSnapshot + monitor.onSessionStarted(testScope, rootSession) + + // Set up a render cause to be marked as previous + monitor.renderIncomingCauses.add(RootCreation(runtimeName, "TestWorkflow")) + + monitor.onRuntimeUpdate(RenderPassSkipped) + + assertTrue(fakeRuntimeTracer.onRuntimeUpdateEnhancedCalled) + assertNotNull(monitor.previousRenderCause) + } + + @Test + fun `onRuntimeUpdate handles RuntimeLoopTick`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer), + runtimeLoopListener = fakeRuntimeLoopListener + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize to set up config snapshot + monitor.onSessionStarted(testScope, rootSession) + + monitor.onRuntimeUpdate(RuntimeLoopTick) + + assertTrue(fakeRuntimeTracer.onRuntimeUpdateEnhancedCalled) + assertTrue(fakeRuntimeLoopListener.onRuntimeLoopTickCalled) + assertTrue(monitor.renderIncomingCauses.isEmpty()) + } + + @Test + fun `onRuntimeUpdate handles RenderingConflated and RenderingProduced`() { + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer) + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize session to set up configSnapshot + monitor.onSessionStarted(testScope, rootSession) + + monitor.onRuntimeUpdate(RenderingConflated) + monitor.onRuntimeUpdate(RenderingProduced) + + // These should complete without error and call tracers + assertTrue(fakeRuntimeTracer.onRuntimeUpdateEnhancedCallCount >= 2) + } + + @Test + fun `addRuntimeUpdate adds to runtime updates`() { + val runtimeListener = TestWorkflowRuntimeLoopListener() + val monitor = WorkflowRuntimeMonitor( + runtimeName, + runtimeLoopListener = runtimeListener + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize session to set up configSnapshot + monitor.onSessionStarted(testScope, rootSession) + + val logLine = UiUpdateLogLine("Test update") + + monitor.addRuntimeUpdate(logLine) + monitor.onRuntimeUpdate(RuntimeLoopTick) + + assertContains(runtimeListener.runtimeUpdatesReceived!!.readAndClear(), logLine) + } + + @Test + fun `RenderPassSkipped adds to runtime updates`() { + val runtimeListener = TestWorkflowRuntimeLoopListener() + val monitor = WorkflowRuntimeMonitor( + runtimeName, + runtimeLoopListener = runtimeListener + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize session to set up configSnapshot + monitor.onSessionStarted(testScope, rootSession) + + // Set up a render cause to be marked as previous + monitor.renderIncomingCauses.add(RootCreation(runtimeName, "TestWorkflow")) + + monitor.onRuntimeUpdate(RenderPassSkipped) + monitor.onRuntimeUpdate(RuntimeLoopTick) + + val updates = runtimeListener.runtimeUpdatesReceived!!.readAndClear() + assertTrue(updates.any { it is SkipLogLine }) + } + + @Test + fun `RenderingConflated does not add to runtime updates`() { + val runtimeListener = TestWorkflowRuntimeLoopListener() + val monitor = WorkflowRuntimeMonitor( + runtimeName, + runtimeLoopListener = runtimeListener + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize session to set up configSnapshot + monitor.onSessionStarted(testScope, rootSession) + + monitor.onRuntimeUpdate(RenderingConflated) + monitor.onRuntimeUpdate(RuntimeLoopTick) + + val updates = runtimeListener.runtimeUpdatesReceived!!.readAndClear() + // RenderingConflated is commented out in the implementation, so no log line should be added + assertFalse(updates.any { it.toString().contains("Conflated") }) + } + + @Test + fun `RenderingProduced does not add to runtime updates`() { + val runtimeListener = TestWorkflowRuntimeLoopListener() + val monitor = WorkflowRuntimeMonitor( + runtimeName, + runtimeLoopListener = runtimeListener + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize session to set up configSnapshot + monitor.onSessionStarted(testScope, rootSession) + + monitor.onRuntimeUpdate(RenderingProduced) + monitor.onRuntimeUpdate(RuntimeLoopTick) + + val updates = runtimeListener.runtimeUpdatesReceived!!.readAndClear() + // RenderingProduced is commented out in the implementation, so no log line should be added + assertFalse(updates.any { it.toString().contains("Produced") }) + } + + @Test + fun `onRenderAndSnapshot adds RenderLogLine to runtime updates`() { + val runtimeListener = TestWorkflowRuntimeLoopListener() + val monitor = WorkflowRuntimeMonitor( + runtimeName, + runtimeLoopListener = runtimeListener + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + val initialProps = "props" + + // Initialize session and set up render cause + monitor.onSessionStarted(testScope, rootSession) + monitor.onInitialState(initialProps, null, testScope, { _, _, _ -> "state" }, rootSession) + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + // Perform render and snapshot + monitor.onRenderAndSnapshot( + renderProps = initialProps, + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + monitor.onRuntimeUpdate(RuntimeLoopTick) + + val updates = runtimeListener.runtimeUpdatesReceived!!.readAndClear() + assertTrue(updates.any { it is RenderLogLine }) + } + + @Test + fun `isRoot property is correct`() { + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val childSession = testWorkflow.createChildSession(rootSession) + + assertTrue(rootSession.isRootWorkflow) + assertFalse(childSession.isRootWorkflow) + } + + @Test + fun `render pass tracker receives correct RenderPassInfo for root creation`() { + val renderPassTracker = TestWorkflowRenderPassTracker() + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer), + renderPassTracker = renderPassTracker + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + val initialProps = "props" + + // Initialize session and set up render cause + monitor.onSessionStarted(testScope, rootSession) + monitor.onInitialState(initialProps, null, testScope, { _, _, _ -> "state" }, rootSession) + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + // Perform render and snapshot + monitor.onRenderAndSnapshot( + renderProps = initialProps, + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + // Verify render pass was recorded + assertEquals(1, renderPassTracker.renderPassCount) + assertNotNull(renderPassTracker.renderPassInfoReceived) + + val renderPassInfo = renderPassTracker.renderPassInfoReceived!! + assertEquals(runtimeName, renderPassInfo.runnerName) + assertTrue(renderPassInfo.renderCause is RootCreation) + assertTrue(renderPassInfo.durationUptime.inWholeNanoseconds > 0) + } + + @Test + fun `render pass tracker receives correct RenderPassInfo for props change`() { + val renderPassTracker = TestWorkflowRenderPassTracker() + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer), + renderPassTracker = renderPassTracker + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + + // Initialize with initial props + monitor.onSessionStarted(testScope, rootSession) + monitor.onInitialState("initialProps", null, testScope, { _, _, _ -> "state" }, rootSession) + + // Clear the initial render causes + monitor.renderIncomingCauses.clear() + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + // Reset tracker to ensure we're only measuring the props change render + renderPassTracker.renderPassCount = 0 + renderPassTracker.renderPassInfoReceived = null + + // Render with different props - this should trigger props change detection + monitor.onRenderAndSnapshot( + renderProps = "newProps", + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + // Verify render pass was recorded with props change cause + assertEquals(1, renderPassTracker.renderPassCount) + assertNotNull(renderPassTracker.renderPassInfoReceived) + + val renderPassInfo = renderPassTracker.renderPassInfoReceived!! + assertEquals(runtimeName, renderPassInfo.runnerName) + assertTrue(renderPassInfo.renderCause is RootPropsChanged) + assertTrue(renderPassInfo.durationUptime.inWholeNanoseconds > 0) + } + + @Test + fun `render pass tracker receives correct RenderPassInfo for action-triggered render`() { + val renderPassTracker = TestWorkflowRenderPassTracker() + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer), + renderPassTracker = renderPassTracker + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + val initialProps = "props" + + // Initialize session + monitor.onSessionStarted(testScope, rootSession) + monitor.onInitialState(initialProps, null, testScope, { _, _, _ -> "state" }, rootSession) + + // Simulate an action-triggered render by adding a callback render cause + monitor.renderIncomingCauses.clear() + val callbackCause = RenderCause.Callback("testAction", "TestWorkflow") + monitor.renderIncomingCauses.add(callbackCause) + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + // Reset tracker to ensure we're only measuring the action-triggered render + renderPassTracker.renderPassCount = 0 + renderPassTracker.renderPassInfoReceived = null + + // Perform render and snapshot + monitor.onRenderAndSnapshot( + renderProps = initialProps, + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + // Verify render pass was recorded with callback cause + assertEquals(1, renderPassTracker.renderPassCount) + assertNotNull(renderPassTracker.renderPassInfoReceived) + + val renderPassInfo = renderPassTracker.renderPassInfoReceived!! + assertEquals(runtimeName, renderPassInfo.runnerName) + assertTrue(renderPassInfo.renderCause is RenderCause.Callback) + assertEquals("testAction", (renderPassInfo.renderCause as RenderCause.Callback).actionName) + assertEquals("TestWorkflow", (renderPassInfo.renderCause as RenderCause.Callback).workflowName) + assertTrue(renderPassInfo.durationUptime.inWholeNanoseconds > 0) + } + + @Test + fun `render pass tracker tracks multiple render passes`() { + val renderPassTracker = TestWorkflowRenderPassTracker() + val monitor = WorkflowRuntimeMonitor( + runtimeName = runtimeName, + workflowRuntimeTracers = listOf(fakeRuntimeTracer), + renderPassTracker = renderPassTracker + ) + val testWorkflow = TestWorkflow() + val rootSession = testWorkflow.createRootSession() + val testScope = TestScope() + val initialProps = "props" + + // Initialize session + monitor.onSessionStarted(testScope, rootSession) + monitor.onInitialState(initialProps, null, testScope, { _, _, _ -> "state" }, rootSession) + + val expectedSnapshot = TreeSnapshot.forRootOnly(null) + val renderingAndSnapshot = RenderingAndSnapshot("rendering", expectedSnapshot) + + // First render pass (root creation) + monitor.onRenderAndSnapshot( + renderProps = initialProps, + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + assertEquals(1, renderPassTracker.renderPassCount) + assertTrue(renderPassTracker.renderPassInfoReceived!!.renderCause is RootCreation) + + // Second render pass (props change) + monitor.renderIncomingCauses.clear() + monitor.onRenderAndSnapshot( + renderProps = "differentProps", + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + assertEquals(2, renderPassTracker.renderPassCount) + assertTrue(renderPassTracker.renderPassInfoReceived!!.renderCause is RootPropsChanged) + + // Third render pass (action callback) + monitor.renderIncomingCauses.clear() + monitor.renderIncomingCauses.add(RenderCause.Callback("anotherAction", "TestWorkflow")) + monitor.onRenderAndSnapshot( + // Same props to avoid props change detection + renderProps = "differentProps", + proceed = { renderingAndSnapshot }, + session = rootSession + ) + + assertEquals(3, renderPassTracker.renderPassCount) + assertTrue(renderPassTracker.renderPassInfoReceived!!.renderCause is RenderCause.Callback) + } + + // Test helper classes + private class TestWorkflow : StatefulWorkflow() { + override fun initialState( + props: String, + snapshot: Snapshot? + ): String = props + + override fun render( + renderProps: String, + renderState: String, + context: RenderContext + ): String = renderState + + override fun snapshotState(state: String): Snapshot = Snapshot.of(state) + + fun createRootSession(): WorkflowSession = TestWorkflowSession( + workflow = this, + sessionId = 1L, + renderKey = "root", + parent = null + ) + + fun createChildSession(parent: WorkflowSession): WorkflowSession = TestWorkflowSession( + workflow = this, + sessionId = 2L, + renderKey = "child", + parent = parent + ) + } + + private class TestWorkflowSession( + private val workflow: TestWorkflow, + override val sessionId: Long, + override val renderKey: String, + override val parent: WorkflowSession? + ) : WorkflowSession { + override val identifier = workflow.identifier + override val runtimeConfig = TestRuntimeConfig() + override val workflowTracer = null + } + + private class TestRuntimeConfig : RuntimeConfig { + override fun contains(element: RuntimeConfigOptions): Boolean = false + override val size: Int = 0 + override fun containsAll(elements: Collection): Boolean = false + override fun isEmpty(): Boolean = true + override fun iterator(): Iterator = + emptyList().iterator() + + override fun toString(): String = "TestRuntimeConfig" + } + + private class TestBaseRenderContext : BaseRenderContext { + override val runtimeConfig = TestRuntimeConfig() + override val workflowTracer = null + override val actionSink: Sink> = TestSink() + + override fun renderChild( + child: Workflow, + props: ChildPropsT, + key: String, + handler: (ChildOutputT) -> WorkflowAction + ): ChildRenderingT { + throw NotImplementedError("Not implemented for testing") + } + + override fun runningSideEffect( + key: String, + sideEffect: suspend CoroutineScope.() -> Unit + ) { + throw NotImplementedError("Not implemented for testing") + } + + override fun remember( + key: String, + resultType: KType, + vararg inputs: Any?, + calculation: () -> ResultT + ): ResultT { + throw NotImplementedError("Not implemented for testing") + } + } + + private class TestSink : Sink> { + override fun send(value: WorkflowAction) { + // No-op for testing + } + } + + private class TestWorkflowRuntimeTracer : WorkflowRuntimeTracer() { + var onWorkflowSessionStartedCalled = false + var onWorkflowSessionStartedCallCount = 0 + var onWorkflowSessionStoppedCalled = false + var onInitialStateCalled = false + var onPropsChangedCalled = false + var onRenderAndSnapshotCalled = false + var onRenderCalled = false + var onSnapshotStateWithChildrenCalled = false + var onRuntimeUpdateEnhancedCalled = false + var onRuntimeUpdateEnhancedCallCount = 0 + var onRootPropsChangedCalled = false + + override fun onWorkflowSessionStarted( + workflowScope: CoroutineScope, + session: WorkflowSession + ) { + onWorkflowSessionStartedCalled = true + onWorkflowSessionStartedCallCount++ + } + + override fun onWorkflowSessionStopped(sessionId: Long) { + onWorkflowSessionStoppedCalled = true + } + + override fun onInitialState( + props: P, + snapshot: Snapshot?, + workflowScope: CoroutineScope, + proceed: (P, Snapshot?, CoroutineScope) -> S, + session: WorkflowSession + ): S { + onInitialStateCalled = true + return proceed(props, snapshot, workflowScope) + } + + override fun onPropsChanged( + old: P, + new: P, + state: S, + proceed: (P, P, S) -> S, + session: WorkflowSession + ): S { + onPropsChangedCalled = true + return proceed(old, new, state) + } + + override fun onRenderAndSnapshot( + renderProps: P, + proceed: (P) -> RenderingAndSnapshot, + session: WorkflowSession + ): RenderingAndSnapshot { + onRenderAndSnapshotCalled = true + return proceed(renderProps) + } + + override fun onRender( + renderProps: P, + renderState: S, + context: BaseRenderContext, + proceed: (P, S, RenderContextInterceptor?) -> R, + session: WorkflowSession + ): R { + onRenderCalled = true + return proceed(renderProps, renderState, null) + } + + override fun onSnapshotStateWithChildren( + proceed: () -> TreeSnapshot, + session: WorkflowSession + ): TreeSnapshot { + onSnapshotStateWithChildrenCalled = true + return proceed() + } + + override fun onRuntimeUpdateEnhanced( + runtimeUpdate: RuntimeUpdate, + currentActionHandlingChangedState: Boolean, + configSnapshot: ConfigSnapshot + ) { + onRuntimeUpdateEnhancedCalled = true + onRuntimeUpdateEnhancedCallCount++ + } + + override fun onRootPropsChanged(session: WorkflowSession) { + onRootPropsChangedCalled = true + } + } + + private class TestWorkflowRenderPassTracker : WorkflowRenderPassTracker { + var renderPassCount = 0 + var renderPassInfoReceived: RenderPassInfo? = null + + override fun recordRenderPass(renderPass: RenderPassInfo) { + renderPassCount++ + renderPassInfoReceived = renderPass + } + } + + private class TestWorkflowRuntimeLoopListener : WorkflowRuntimeLoopListener { + var onRuntimeLoopTickCalled = false + var runtimeUpdatesReceived: RuntimeUpdates? = null + + override fun onRuntimeLoopTick( + configSnapshot: ConfigSnapshot, + runtimeUpdates: RuntimeUpdates + ) { + onRuntimeLoopTickCalled = true + runtimeUpdatesReceived = runtimeUpdates + } + } +} diff --git a/workflow-tracing/src/test/resources/com/squareup/workflow1/diagnostic/tracing/expected_trace_file.txt b/workflow-tracing/src/test/resources/com/squareup/workflow1/diagnostic/tracing/expected_trace_file.txt deleted file mode 100644 index 0752ddbff5..0000000000 --- a/workflow-tracing/src/test/resources/com/squareup/workflow1/diagnostic/tracing/expected_trace_file.txt +++ /dev/null @@ -1,1078 +0,0 @@ -[ -{"name":"process_name","ph":"M","ts":0,"pid":0,"tid":0,"args":{"name":"TestWorkflow"}}, -{"name":"thread_name","ph":"M","ts":0,"pid":0,"tid":0,"args":{"name":"Profiling"}}, -{"name":"GC detected","cat":"system","ph":"i","ts":0,"pid":0,"tid":0,"s":"g","args":{"freeMemory":42,"totalMemory":43}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"workflow","ph":"b","ts":0,"pid":0,"tid":0,"id":"workflow","args":{"workflowId":"0","initialProps":"0","initialState":"initial","restoredFromSnapshot":false}}, -{"name":"TestWorkflow (0)","ph":"N","ts":0,"pid":0,"tid":0,"id":"0"}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"0"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"0","state":"initial"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"0","newProps":"1","oldState":"initial","newState":"changed state"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"0","newProps":"1","oldState":"initial","newState":"changed state"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"1"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"1","state":"changed state"}}, -{"name":"GC detected","cat":"system","ph":"i","ts":0,"pid":0,"tid":0,"s":"g","args":{"freeMemory":42,"totalMemory":43}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (1)","cat":"workflow","ph":"b","ts":0,"pid":0,"tid":0,"id":"workflow","args":{"workflowId":"1","initialProps":"0","initialState":"initial","restoredFromSnapshot":false,"parent":"TestWorkflow (0)"}}, -{"name":"TestWorkflow (1)","ph":"N","ts":0,"pid":0,"tid":0,"id":"1"}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"1","props":"0","state":"initial"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"1","newProps":"2","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"1","newProps":"2","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"2"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"2","state":"changed state"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"1","props":"0","state":"initial"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"Worker (2)","cat":"workflow","ph":"b","ts":0,"pid":0,"tid":0,"id":"workflow","args":{"workflowId":"2","initialProps":"TypedWorker(java.lang.String (Kotlin reflection is not available))","initialState":"0","restoredFromSnapshot":false,"parent":"TestWorkflow (0)"}}, -{"name":"Worker (2)","ph":"N","ts":0,"pid":0,"tid":0,"id":"2"}, -{"name":"Worker (2)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"2","props":"TypedWorker(java.lang.String (Kotlin reflection is not available))","state":"0"}}, -{"name":"Worker (2)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"kotlin.Unit"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"2","newProps":"3","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"2","newProps":"3","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"3"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"3","state":"changed state"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"1","props":"0","state":"initial"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"Props changed: Worker (2)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"TypedWorker(java.lang.String (Kotlin reflection is not available))","newProps":"TypedWorker(java.lang.String (Kotlin reflection is not available))","oldState":"0","newState":"{no change}"}}, -{"name":"Worker (2)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"2","props":"TypedWorker(java.lang.String (Kotlin reflection is not available))","state":"0"}}, -{"name":"Worker (2)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"kotlin.Unit"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Sink received: Worker (2)","cat":"update","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"action":"action(EmitWorkerOutputAction(worker=TypedWorker(java.lang.String (Kotlin reflection is not available)), key=))"}}, -{"name":"WorkflowAction: Worker (2)","cat":"update","ph":"i","ts":0,"pid":0,"tid":0,"s":"p","args":{"action":"action(EmitWorkerOutputAction(worker=TypedWorker(java.lang.String (Kotlin reflection is not available)), key=))","oldState":"0","newState":"{no change}","output":"fired!"}}, -{"name":"Worker (2)","ph":"O","ts":0,"pid":0,"tid":0,"id":"2","args":{"snapshot":"0"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"3"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"3","state":"changed state"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"1","props":"0","state":"initial"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"Props changed: Worker (2)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"TypedWorker(java.lang.String (Kotlin reflection is not available))","newProps":"TypedWorker(java.lang.String (Kotlin reflection is not available))","oldState":"0","newState":"{no change}"}}, -{"name":"Worker (2)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"2","props":"TypedWorker(java.lang.String (Kotlin reflection is not available))","state":"0"}}, -{"name":"Worker (2)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"kotlin.Unit"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"3","newProps":"4","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"3","newProps":"4","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"4"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"4","state":"changed state"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"1","props":"0","state":"initial"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"GC detected","cat":"system","ph":"i","ts":0,"pid":0,"tid":0,"s":"g","args":{"freeMemory":42,"totalMemory":43}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow:second (3)","cat":"workflow","ph":"b","ts":0,"pid":0,"tid":0,"id":"workflow","args":{"workflowId":"3","initialProps":"1","initialState":"initial","restoredFromSnapshot":false,"parent":"TestWorkflow (0)"}}, -{"name":"TestWorkflow:second (3)","ph":"N","ts":0,"pid":0,"tid":0,"id":"3"}, -{"name":"TestWorkflow:second (3)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"3","props":"1","state":"initial"}}, -{"name":"GC detected","cat":"system","ph":"i","ts":0,"pid":0,"tid":0,"s":"g","args":{"freeMemory":42,"totalMemory":43}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (4)","cat":"workflow","ph":"b","ts":0,"pid":0,"tid":0,"id":"workflow","args":{"workflowId":"4","initialProps":"0","initialState":"initial","restoredFromSnapshot":false,"parent":"TestWorkflow:second (3)"}}, -{"name":"TestWorkflow (4)","ph":"N","ts":0,"pid":0,"tid":0,"id":"4"}, -{"name":"TestWorkflow (4)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"4","props":"0","state":"initial"}}, -{"name":"TestWorkflow (4)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"TestWorkflow:second (3)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Worker (2)","cat":"workflow","ph":"e","ts":0,"pid":0,"tid":0,"id":"workflow"}, -{"name":"Worker (2)","ph":"D","ts":0,"pid":0,"tid":0,"id":"2"}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"4","newProps":"5","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"4","newProps":"5","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"5"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"5","state":"changed state"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"1","props":"0","state":"initial"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"TestWorkflow:second (3)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"3","props":"1","state":"initial"}}, -{"name":"TestWorkflow (4)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"4","props":"0","state":"initial"}}, -{"name":"TestWorkflow (4)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"TestWorkflow:second (3)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"5","newProps":"6","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"5","newProps":"6","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"6"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"6","state":"changed state"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"1","props":"0","state":"initial"}}, -{"name":"TestWorkflow (1)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"initial"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (4)","cat":"workflow","ph":"e","ts":0,"pid":0,"tid":0,"id":"workflow"}, -{"name":"TestWorkflow (4)","ph":"D","ts":0,"pid":0,"tid":0,"id":"4"}, -{"name":"TestWorkflow:second (3)","cat":"workflow","ph":"e","ts":0,"pid":0,"tid":0,"id":"workflow"}, -{"name":"TestWorkflow:second (3)","ph":"D","ts":0,"pid":0,"tid":0,"id":"3"}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"6","newProps":"7","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"6","newProps":"7","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"7"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"7","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (1)","cat":"workflow","ph":"e","ts":0,"pid":0,"tid":0,"id":"workflow"}, -{"name":"TestWorkflow (1)","ph":"D","ts":0,"pid":0,"tid":0,"id":"1"}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"7","newProps":"8","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"7","newProps":"8","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"8"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"8","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"8","newProps":"9","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"8","newProps":"9","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"9"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"9","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"9","newProps":"10","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"9","newProps":"10","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"10"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"10","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"rendering"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"10","newProps":"11","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"10","newProps":"11","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"11"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"11","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"11","newProps":"12","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"11","newProps":"12","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"12"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"12","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"12","newProps":"13","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"12","newProps":"13","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"13"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"13","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"13","newProps":"14","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"13","newProps":"14","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"14"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"14","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"14","newProps":"15","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"14","newProps":"15","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"15"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"15","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"15","newProps":"16","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"15","newProps":"16","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"16"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"16","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"16","newProps":"17","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"16","newProps":"17","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"17"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"17","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"17","newProps":"18","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"17","newProps":"18","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"18"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"18","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"18","newProps":"19","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"18","newProps":"19","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"19"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"19","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"19","newProps":"20","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"19","newProps":"20","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"20"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"20","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"20","newProps":"21","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"20","newProps":"21","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"21"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"21","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"21","newProps":"22","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"21","newProps":"22","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"22"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"22","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"22","newProps":"23","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"22","newProps":"23","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"23"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"23","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"23","newProps":"24","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"23","newProps":"24","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"24"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"24","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"24","newProps":"25","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"24","newProps":"25","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"25"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"25","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"25","newProps":"26","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"25","newProps":"26","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"26"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"26","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"26","newProps":"27","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"26","newProps":"27","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"27"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"27","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"27","newProps":"28","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"27","newProps":"28","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"28"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"28","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"28","newProps":"29","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"28","newProps":"29","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"29"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"29","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"29","newProps":"30","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"29","newProps":"30","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"30"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"30","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"30","newProps":"31","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"30","newProps":"31","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"31"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"31","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"31","newProps":"32","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"31","newProps":"32","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"32"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"32","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"32","newProps":"33","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"32","newProps":"33","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"33"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"33","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"33","newProps":"34","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"33","newProps":"34","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"34"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"34","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"34","newProps":"35","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"34","newProps":"35","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"35"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"35","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"35","newProps":"36","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"35","newProps":"36","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"36"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"36","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"36","newProps":"37","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"36","newProps":"37","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"37"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"37","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"37","newProps":"38","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"37","newProps":"38","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"38"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"38","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"38","newProps":"39","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"38","newProps":"39","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"39"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"39","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"39","newProps":"40","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"39","newProps":"40","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"40"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"40","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"40","newProps":"41","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"40","newProps":"41","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"41"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"41","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"41","newProps":"42","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"41","newProps":"42","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"42"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"42","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"42","newProps":"43","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"42","newProps":"43","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"43"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"43","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"43","newProps":"44","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"43","newProps":"44","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"44"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"44","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"44","newProps":"45","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"44","newProps":"45","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"45"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"45","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"45","newProps":"46","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"45","newProps":"46","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"46"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"46","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"46","newProps":"47","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"46","newProps":"47","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"47"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"47","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"47","newProps":"48","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"47","newProps":"48","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"48"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"48","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"48","newProps":"49","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"48","newProps":"49","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"49"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"49","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"49","newProps":"50","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"49","newProps":"50","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"50"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"50","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"50","newProps":"51","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"50","newProps":"51","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"51"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"51","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"51","newProps":"52","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"51","newProps":"52","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"52"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"52","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"52","newProps":"53","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"52","newProps":"53","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"53"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"53","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"53","newProps":"54","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"53","newProps":"54","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"54"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"54","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"54","newProps":"55","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"54","newProps":"55","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"55"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"55","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"55","newProps":"56","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"55","newProps":"56","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"56"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"56","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"56","newProps":"57","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"56","newProps":"57","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"57"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"57","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"57","newProps":"58","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"57","newProps":"58","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"58"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"58","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"58","newProps":"59","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"58","newProps":"59","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"59"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"59","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"59","newProps":"60","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"59","newProps":"60","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"60"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"60","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"60","newProps":"61","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"60","newProps":"61","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"61"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"61","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"61","newProps":"62","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"61","newProps":"62","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"62"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"62","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"62","newProps":"63","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"62","newProps":"63","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"63"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"63","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"63","newProps":"64","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"63","newProps":"64","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"64"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"64","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"64","newProps":"65","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"64","newProps":"65","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"65"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"65","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"65","newProps":"66","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"65","newProps":"66","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"66"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"66","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"66","newProps":"67","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"66","newProps":"67","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"67"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"67","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"67","newProps":"68","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"67","newProps":"68","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"68"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"68","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"68","newProps":"69","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"68","newProps":"69","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"69"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"69","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"69","newProps":"70","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"69","newProps":"70","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"70"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"70","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"70","newProps":"71","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"70","newProps":"71","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"71"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"71","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"71","newProps":"72","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"71","newProps":"72","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"72"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"72","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"72","newProps":"73","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"72","newProps":"73","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"73"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"73","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"73","newProps":"74","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"73","newProps":"74","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"74"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"74","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"74","newProps":"75","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"74","newProps":"75","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"75"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"75","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"75","newProps":"76","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"75","newProps":"76","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"76"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"76","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"76","newProps":"77","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"76","newProps":"77","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"77"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"77","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"77","newProps":"78","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"77","newProps":"78","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"78"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"78","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"78","newProps":"79","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"78","newProps":"79","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"79"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"79","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"79","newProps":"80","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"79","newProps":"80","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"80"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"80","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"80","newProps":"81","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"80","newProps":"81","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"81"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"81","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"81","newProps":"82","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"81","newProps":"82","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"82"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"82","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"82","newProps":"83","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"82","newProps":"83","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"83"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"83","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"83","newProps":"84","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"83","newProps":"84","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"84"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"84","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"84","newProps":"85","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"84","newProps":"85","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"85"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"85","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"85","newProps":"86","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"85","newProps":"86","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"86"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"86","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"86","newProps":"87","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"86","newProps":"87","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"87"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"87","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"87","newProps":"88","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"87","newProps":"88","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"88"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"88","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"88","newProps":"89","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"88","newProps":"89","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"89"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"89","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"89","newProps":"90","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"89","newProps":"90","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"90"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"90","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"90","newProps":"91","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"90","newProps":"91","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"91"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"91","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"91","newProps":"92","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"91","newProps":"92","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"92"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"92","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"92","newProps":"93","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"92","newProps":"93","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"93"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"93","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"93","newProps":"94","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"93","newProps":"94","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"94"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"94","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"94","newProps":"95","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"94","newProps":"95","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"95"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"95","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"95","newProps":"96","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"95","newProps":"96","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"96"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"96","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"96","newProps":"97","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"96","newProps":"97","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"97"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"97","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"97","newProps":"98","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"97","newProps":"98","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"98"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"98","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"98","newProps":"99","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"98","newProps":"99","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"99"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"99","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Props changed: {root}","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"99","newProps":"100","oldState":"changed state","newState":"{no change}"}}, -{"name":"Props changed: TestWorkflow (0)","ph":"i","ts":0,"pid":0,"tid":0,"s":"t","args":{"oldProps":"99","newProps":"100","oldState":"changed state","newState":"{no change}"}}, -{"name":"Render Pass","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"props":"100"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"B","ts":0,"pid":0,"tid":0,"args":{"workflowId":"0","props":"100","state":"changed state"}}, -{"name":"TestWorkflow (0)","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"Render Pass","cat":"rendering","ph":"E","ts":0,"pid":0,"tid":0,"args":{"rendering":"final"}}, -{"name":"used/free memory","ph":"C","ts":0,"pid":0,"tid":0,"args":{"usedMemory":1,"freeMemory":42}}, -{"name":"Snapshot","ph":"B","ts":0,"pid":0,"tid":0,"args":{}}, -{"name":"Snapshot","ph":"E","ts":0,"pid":0,"tid":0,"args":{}}, diff --git a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/NavigationMonitor.kt b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/NavigationMonitor.kt index f77cb2ce7d..0550c57a3a 100644 --- a/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/NavigationMonitor.kt +++ b/workflow-ui/core-common/src/main/java/com/squareup/workflow1/ui/navigation/NavigationMonitor.kt @@ -42,6 +42,11 @@ public class NavigationMonitor( /** * Creates a [NavigationMonitor] and [updates it][NavigationMonitor.update] * with [each element collected][Flow.onEach] by the receiving [Flow]. + * + * Note that one of the best ways to use this is with an installed + * [com.squareup.workflow1.tracing.WorkflowRuntimeMonitor] and then calling + * [com.squareup.workflow1.tracing.RuntimeTraceContext.addRuntimeUpdate] with a + * [com.squareup.workflow1.tracing.UiUpdateLogLine] in [onNavigate]. */ public fun Flow.reportNavigation( skipFirstScreen: Boolean = false,