Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.squareup.benchmarks.performance.complex.poetry.instrumentation
import androidx.tracing.Trace
import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemWorkflow
import com.squareup.benchmarks.performance.complex.poetry.PerformancePoemsBrowserWorkflow
import com.squareup.benchmarks.performance.complex.poetry.instrumentation.PerformanceTracingInterceptor.Companion.NODES_TO_TRACE
import com.squareup.workflow1.BaseRenderContext
import com.squareup.workflow1.WorkflowInterceptor
import com.squareup.workflow1.WorkflowInterceptor.RenderContextInterceptor
Expand All @@ -27,6 +28,13 @@ class PerformanceTracingInterceptor(
context: BaseRenderContext<P, S, O>,
proceed: (P, S, RenderContextInterceptor<P, S, O>?) -> R,
session: WorkflowSession
): R = traceRender(session) {
proceed(renderProps, renderState, null)
}

private inline fun <R> traceRender(
session: WorkflowSession,
render: () -> R
): R {
val isRoot = session.parent == null
val traceIdIndex = NODES_TO_TRACE.indexOfFirst { it.second == session.identifier }
Expand All @@ -45,7 +53,7 @@ class PerformanceTracingInterceptor(
Trace.beginSection(sectionName)
}

return proceed(renderProps, renderState, null).also {
return render().also {
if (traceIdIndex > -1 && !sample) {
Trace.endSection()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import androidx.tracing.Trace
import app.cash.burst.Burst
import com.squareup.benchmark.runtime.benchmark.BenchmarkRuntimeOptions.NoOptimizations
import com.squareup.workflow1.RuntimeConfig
import com.squareup.workflow1.RuntimeConfigOptions.Companion.RuntimeOptions
import com.squareup.workflow1.Sink
Expand All @@ -16,6 +15,7 @@ import com.squareup.workflow1.WorkflowAction
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.WorkflowTracer
import com.squareup.workflow1.action
import com.squareup.workflow1.internal.compose.runtime.setGlobalSnapshotManagerSendApplyImmediately
import com.squareup.workflow1.remember
import com.squareup.workflow1.renderChild
import com.squareup.workflow1.renderWorkflowIn
Expand All @@ -25,19 +25,24 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.job
import kotlinx.coroutines.plus
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import kotlin.math.pow
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.minutes

/** The microbenchmarks take a while to run, so we only run with a subset of runtime configs. */
@Suppress("unused")
@OptIn(WorkflowExperimentalRuntime::class)
enum class BenchmarkRuntimeOptions(
val runtimeConfig: RuntimeConfig
) {
NoOptimizations(RuntimeOptions.NONE.runtimeConfig),
// NoOptimizations(RuntimeOptions.NONE.runtimeConfig),
AllOptimizations(RuntimeOptions.ALL.runtimeConfig),
ComposeNoSkip(RuntimeOptions.COMPOSE_RUNTIME_NON_SKIPPING.runtimeConfig),
ComposeSkipping(RuntimeOptions.COMPOSE_RUNTIME_SKIPPING.runtimeConfig),
}

enum class BenchmarkTreeShape(
Expand All @@ -52,17 +57,30 @@ enum class BenchmarkTreeShape(
@Burst
class WorkflowRuntimeMicrobenchmark(
private val treeShape: BenchmarkTreeShape = BenchmarkTreeShape.ShallowBushyTree,
private val runtime: BenchmarkRuntimeOptions = NoOptimizations,
private val runtime: BenchmarkRuntimeOptions = BenchmarkRuntimeOptions.AllOptimizations,
) {

private companion object {
const val WideSiblingCount = 250
const val RememberEntryCount = 250
const val StableHandlerCount = 250

// The default 1m runTest timeout fires for the slowest combinations on physical devices,
// surfacing as UncompletedCoroutinesError instead of a real benchmark result. The benchmark
// body itself is bounded by `measureRepeated` so this just lets it finish.
val BenchmarkRunTestTimeout = 10.minutes
}

@get:Rule val benchmarkRule = BenchmarkRule()

@Before fun setUp() {
setGlobalSnapshotManagerSendApplyImmediately(true)
}

@After fun tearDown() {
setGlobalSnapshotManagerSendApplyImmediately(false)
}

@Test fun initialRenderAllChildren() = benchmarkWorkflowPropsChange(
setupProps = BenchmarkWorkflowRoot.Props(
renderFirstLeaf = false,
Expand Down Expand Up @@ -251,7 +269,7 @@ class WorkflowRuntimeMicrobenchmark(
testProps: PropsT,
expectedSetupRendering: Int,
expectedTestRendering: Int,
) = runTest {
) = runTest(timeout = BenchmarkRunTestTimeout) {
val props = MutableStateFlow(setupProps)
val workflowJob = Job(parent = coroutineContext.job)
val renderings = renderWorkflowIn(
Expand Down Expand Up @@ -284,7 +302,7 @@ class WorkflowRuntimeMicrobenchmark(
private fun benchmarkWorkflowStateChange(
testState: (setStateForChild: (index: Int, newState: Int) -> Unit) -> Unit,
expectedTestRendering: Int,
) = runTest {
) = runTest(timeout = BenchmarkRunTestTimeout) {
val actionSinks = arrayOfNulls<Sink<WorkflowAction<*, Int, Nothing>>?>(treeShape.leafCount)
val workflow = BenchmarkWorkflowRoot(
treeShape = treeShape,
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ plugins {
alias(libs.plugins.ktlint)
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.androidx.benchmark) apply false
alias(libs.plugins.jetbrains.compose) apply false
}

shardConnectedCheckTasks(project)
Expand Down
3 changes: 3 additions & 0 deletions dependencies/classpath.txt
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ org.codehaus.woodstox:stax2-api:4.2.1
org.glassfish.jaxb:jaxb-runtime:2.3.2
org.glassfish.jaxb:txw2:2.3.2
org.jdom:jdom2:2.0.6
org.jetbrains.compose.hot-reload:hot-reload-gradle-plugin:1.0.0
org.jetbrains.compose:compose-gradle-plugin:1.10.3
org.jetbrains.compose:org.jetbrains.compose.gradle.plugin:1.10.3
org.jetbrains.dokka:dokka-core:2.0.0
org.jetbrains.dokka:dokka-gradle-plugin:2.0.0
org.jetbrains.dokka:org.jetbrains.dokka.gradle.plugin:2.0.0
Expand Down
12 changes: 9 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ compileSdk = "36"
minSdk = "24"
targetSdk = "33"

jdk-target = "1.8"
jdk-target = "11"
jdk-toolchain = "17"

androidx-activity = "1.8.2"
Expand All @@ -15,7 +15,7 @@ androidx-benchmark = "1.3.4"
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-compose-bom = "2026.04.01"
androidx-constraintlayout = "2.1.4"
androidx-core = "1.13.1"
androidx-fragment = "1.8.5"
Expand Down Expand Up @@ -54,7 +54,7 @@ groovy = "3.0.9"
jUnit = "4.13.2"
java-diff-utils = "4.12"
javaParser = "3.24.0"
jetbrains-compose-plugin = "1.7.3"
jetbrains-compose-plugin = "1.10.3"
kgx = "0.1.12"
kotest = "5.1.0"
# Keep this in sync with what is hard-coded in build-logic/settings.gradle.kts as that is upstream
Expand Down Expand Up @@ -117,6 +117,8 @@ kotlinx-apiBinaryCompatibility = { id = "org.jetbrains.kotlinx.binary-compatibil
mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech-publish" }

jetbrains-compose = { id = "org.jetbrains.compose", version.ref = "jetbrains-compose-plugin" }
android-library = { id = "com.android.library", version.ref = "agpVersion" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }

[libraries]

Expand Down Expand Up @@ -209,7 +211,11 @@ hamcrest = "org.hamcrest:hamcrest-core:2.2"

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

jetbrains-annotations = "org.jetbrains:annotations:24.0.1"
jetbrains-compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "jetbrains-compose-plugin" }
jetbrains-compose-runtime-saveable = { module = "org.jetbrains.compose.runtime:runtime-saveable", version.ref = "jetbrains-compose-plugin" }
jetbrains-compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "jetbrains-compose-plugin" }

junit = { module = "junit:junit", version.ref = "jUnit" }

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION") // ContextCompat.startActivity overload deprecation; sample code.

package com.squareup.sample.compose.launcher

import android.content.Intent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import androidx.compose.ui.platform.AndroidUiDispatcher
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.squareup.workflow1.RuntimeConfigOptions.COMPOSE_RUNTIME
import com.squareup.workflow1.SimpleLoggingWorkflowInterceptor
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
Expand Down Expand Up @@ -49,7 +51,8 @@ class NestedRenderingsActivity : AppCompatActivity() {
workflow = RecursiveWorkflow.mapRendering { it.withEnvironment(viewEnvironment) },
scope = viewModelScope + AndroidUiDispatcher.Main,
savedStateHandle = savedState,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
runtimeConfig = setOf(COMPOSE_RUNTIME), // AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
interceptors = listOf(SimpleLoggingWorkflowInterceptor()),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import com.squareup.sample.container.SampleContainers
import com.squareup.sample.poetry.RealPoemWorkflow
import com.squareup.sample.poetry.RealPoemsBrowserWorkflow
import com.squareup.sample.poetry.model.Poem
import com.squareup.workflow1.RuntimeConfigOptions.COMPOSE_RUNTIME
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
Expand Down Expand Up @@ -47,7 +48,7 @@ class PoetryModel(savedState: SavedStateHandle) : ViewModel() {
scope = viewModelScope,
prop = 0 to 0 to Poem.allPoems,
savedStateHandle = savedState,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
runtimeConfig = setOf(COMPOSE_RUNTIME), // AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
).reportNavigation {
Timber.i("Navigated to %s", it)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import androidx.lifecycle.viewModelScope
import com.squareup.sample.container.SampleContainers
import com.squareup.sample.poetry.RealPoemWorkflow
import com.squareup.sample.poetry.model.Raven
import com.squareup.workflow1.RuntimeConfigOptions.COMPOSE_RUNTIME
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
Expand Down Expand Up @@ -56,7 +57,7 @@ class RavenModel(savedState: SavedStateHandle) : ViewModel() {
scope = viewModelScope,
savedStateHandle = savedState,
prop = Raven,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
runtimeConfig = setOf(COMPOSE_RUNTIME), // AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
) {
running.complete()
}.reportNavigation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewModelScope
import com.squareup.sample.container.SampleContainers
import com.squareup.workflow1.RuntimeConfigOptions.COMPOSE_RUNTIME
import com.squareup.workflow1.WorkflowExperimentalRuntime
import com.squareup.workflow1.android.renderWorkflowIn
import com.squareup.workflow1.config.AndroidRuntimeConfigTools
Expand Down Expand Up @@ -53,7 +54,7 @@ class HelloBackButtonModel(savedState: SavedStateHandle) : ViewModel() {
workflow = AreYouSureWorkflow,
scope = viewModelScope,
savedStateHandle = savedState,
runtimeConfig = AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
runtimeConfig = setOf(COMPOSE_RUNTIME), // AndroidRuntimeConfigTools.getAppWorkflowRuntimeConfig()
) {
// This workflow handles the back button itself, so the activity can't.
// Instead, the workflow emits an output to signal that it's time to shut things down.
Expand Down
4 changes: 4 additions & 0 deletions samples/dungeon/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ plugins {
id("kotlin-android")
id("android-sample-app")
id("android-ui-tests")
alias(libs.plugins.compose.compiler)
}

android {
Expand Down Expand Up @@ -43,12 +44,15 @@ dependencies {
implementation(libs.rxjava2.rxandroid)
implementation(libs.squareup.cycler)
implementation(libs.squareup.okio)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.foundation)

implementation(project(":samples:dungeon:common"))
implementation(project(":samples:dungeon:timemachine"))
implementation(project(":samples:dungeon:timemachine-shakeable"))
implementation(project(":workflow-ui:core-android"))
implementation(project(":workflow-ui:core-common"))
implementation(project(":workflow-ui:compose"))

testImplementation(libs.junit)
testImplementation(libs.truth)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,12 @@ class Component(context: AppCompatActivity) {

val appWorkflow = DungeonAppWorkflow(gameSessionWorkflow, boardLoader)

val timeMachineWorkflow = TimeMachineAppWorkflow(appWorkflow, clock, context)
val timeMachineWorkflow = TimeMachineAppWorkflow(
appWorkflow,
// SimpleWorkflow(),
clock,
context
)

val timeMachineModelFactory = TimeMachineModel.Factory(
context,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package com.squareup.sample.dungeon
import android.os.Bundle
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import com.squareup.workflow1.ui.compose.withComposeInteropSupport
import com.squareup.workflow1.ui.withEnvironment
import com.squareup.workflow1.ui.withRegistry
import com.squareup.workflow1.ui.workflowContentView
import kotlinx.coroutines.flow.map
Expand All @@ -17,6 +19,11 @@ class DungeonActivity : AppCompatActivity() {
val model: TimeMachineModel by viewModels { component.timeMachineModelFactory }

workflowContentView
.take(lifecycle, model.renderings.map { it.withRegistry(component.viewRegistry) })
.take(
lifecycle,
model.renderings.map {
it.withRegistry(component.viewRegistry)
.withEnvironment { it.withComposeInteropSupport() }
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.squareup.sample.dungeon

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import com.squareup.sample.dungeon.DungeonAppWorkflow.Props
import com.squareup.workflow1.Snapshot
import com.squareup.workflow1.StatefulWorkflow
import com.squareup.workflow1.parse
import com.squareup.workflow1.ui.Screen
import com.squareup.workflow1.ui.compose.ComposeScreen

class SimpleWorkflow : StatefulWorkflow<Props, String, Nothing, Screen>() {
override fun initialState(
props: Props,
snapshot: Snapshot?
): String = snapshot?.bytes?.parse { it.readUtf8() } ?: "initial"

override fun render(
renderProps: Props,
renderState: String,
context: RenderContext<Props, String, Nothing>
): Screen {
return MyScreen(
text = renderState,
onClick = context.eventHandler("onClick", remember = true) { state = state.reversed() }
)
}

override fun snapshotState(state: String): Snapshot = Snapshot.of(state)
}

private data class MyScreen(
val text: String,
val onClick: () -> Unit,
) : ComposeScreen {
@Composable override fun Content() {
Modifier.pointerInput(Unit) {
extendedTouchPadding
}
BasicText(
text = text,
color = { Color.White },
modifier = Modifier
.background(Color.White)
.wrapContentSize()
.padding(8.dp)
.clickable { onClick() }
.background(Color(red = 0f, green = 0f, blue = 0.9f))
.padding(48.dp)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import kotlin.time.TimeSource
@OptIn(ExperimentalTime::class)
class TimeMachineAppWorkflow(
appWorkflow: DungeonAppWorkflow,
// appWorkflow: SimpleWorkflow,
clock: TimeSource,
context: Context
) : StatelessWorkflow<BoardPath, Nothing, ShakeableTimeMachineScreen>() {
Expand Down
Loading