diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml
index 51d9df206d..84017cafe2 100644
--- a/.github/workflows/kotlin.yml
+++ b/.github/workflows/kotlin.yml
@@ -610,8 +610,8 @@ jobs:
report_paths: '**/build/test-results/*[tT]est/TEST-*.xml'
annotate_only: true # no write check perms on this workflow right now.
- performance-tests:
- name: Performance tests
+ macrobenchmarks:
+ name: Macrobenchmarks
runs-on: workflow-kotlin-test-runner-ubuntu-4core
timeout-minutes: 45
strategy:
@@ -627,13 +627,43 @@ jobs:
- name: Instrumented tests
uses: ./.github/actions/gradle-tasks-with-emulator
with:
- tests-name: perf-tests-results
+ tests-name: macrobenchmarks-results
api-level: ${{ matrix.api-level }}
prepare-task: :benchmarks:performance-poetry:complex-poetry:prepareDebugAndroidTestArtifacts
test-task: :benchmarks:performance-poetry:complex-poetry:connectedCheck --continue
restore-cache-key: androidTest-build-artifacts
failure-path-upload: '**/build/reports/androidTests/connected'
- failure-upload-name: 'perf-tests-report'
+ failure-upload-name: 'macrobenchmarks-report'
+
+ microbenchmarks:
+ name: Microbenchmarks
+ runs-on: workflow-kotlin-test-runner-ubuntu-4core
+ timeout-minutes: 45
+ strategy:
+ # Allow tests to continue on other devices if they fail on one device.
+ fail-fast: false
+ matrix:
+ api-level:
+ - 31
+ steps:
+ - name: Checkout
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+
+ - name: Instrumented tests
+ uses: ./.github/actions/gradle-tasks-with-emulator
+ with:
+ tests-name: microbenchmarks-results
+ api-level: ${{ matrix.api-level }}
+ # Microbenchmarks only run as release builds so we don't use the debug task like most
+ # builds.
+ prepare-task: :benchmarks:runtime-microbenchmark:prepareReleaseAndroidTestArtifacts -Pandroidx.benchmark.dryRunMode.enable=true -Pkotlin.native.ignoreDisabledTargets=true
+ # Run in dry run mode so they only run once, without warmup, since we don't care about measurements.
+ # See https://developer.android.com/topic/performance/benchmarking/microbenchmark-instrumentation-args#dryrunmode-enable
+ # Ignore iOS and other native targets being disabled since this is specifically and Android task.
+ test-task: :benchmarks:runtime-microbenchmark:connectedReleaseAndroidTest -Pandroidx.benchmark.dryRunMode.enable=true -Pkotlin.native.ignoreDisabledTargets=true --continue
+ restore-cache-key: androidTest-build-artifacts
+ failure-path-upload: 'benchmarks/runtime-microbenchmark/build/reports/androidTests/connected'
+ failure-upload-name: 'microbenchmarks-report'
instrumentation-tests:
name: Instrumentation tests
@@ -659,7 +689,7 @@ jobs:
api-level: ${{ matrix.api-level }}
prepare-task: prepareConnectedCheckShard${{matrix.shardNum}}
# -x excludes tasks
- test-task: connectedCheckShard${{matrix.shardNum}} -x :benchmarks:dungeon-benchmark:connectedAndroidTest -x :benchmarks:performance-poetry:complex-benchmark:connectedAndroidTest -x :benchmarks:performance-poetry:complex-poetry:connectedAndroidTest
+ test-task: connectedCheckShard${{matrix.shardNum}} -x :benchmarks:dungeon-benchmark:connectedAndroidTest -x :benchmarks:performance-poetry:complex-benchmark:connectedAndroidTest -x :benchmarks:performance-poetry:complex-poetry:connectedAndroidTest -x :benchmarks:runtime-microbenchmark:connectedAndroidTest
write-cache-key: androidTest-build-artifacts-${{matrix.shardNum}}
restore-cache-key: main-build-artifacts
failure-path-upload: '**/build/reports/androidTests/connected'
@@ -690,7 +720,7 @@ jobs:
api-level: ${{ matrix.api-level }}
prepare-task: prepareConnectedCheckShard${{matrix.shardNum}} -Pworkflow.runtime=${{matrix.runtime}}
# -x excludes tasks
- test-task: connectedCheckShard${{matrix.shardNum}} -Pworkflow.runtime=${{matrix.runtime}} -x :benchmarks:dungeon-benchmark:connectedAndroidTest -x :benchmarks:performance-poetry:complex-benchmark:connectedAndroidTest -x :benchmarks:performance-poetry:complex-poetry:connectedAndroidTest
+ test-task: connectedCheckShard${{matrix.shardNum}} -Pworkflow.runtime=${{matrix.runtime}} -x :benchmarks:dungeon-benchmark:connectedAndroidTest -x :benchmarks:performance-poetry:complex-benchmark:connectedAndroidTest -x :benchmarks:performance-poetry:complex-poetry:connectedAndroidTest -x :benchmarks:runtime-microbenchmark:connectedAndroidTest
write-cache-key: androidTest-build-artifacts-${{matrix.shardNum}}-${{matrix.runtime}}
restore-cache-key: main-build-artifacts
failure-path-upload: '**/build/reports/androidTests/connected'
@@ -724,7 +754,8 @@ jobs:
- unit-conflate-partial-drainExclusive-runtime-tests
- unit-all-runtime-tests
- ktlint
- - performance-tests
+ - macrobenchmarks
+ - microbenchmarks
- runtime-instrumentation-tests
- shards-and-version
- tutorials
diff --git a/benchmarks/README.md b/benchmarks/README.md
index 13869c1eae..3566544f27 100644
--- a/benchmarks/README.md
+++ b/benchmarks/README.md
@@ -5,7 +5,18 @@ deterministic tests - such as those for render passes - can be run on any device
otherwise), but the benchmarks should be run on physical devices, or the closest approximation
to physical devices you can get.
-## Baseline Profiles
+There are two types of benchmarks:
+
+- **Macro benchmarks** measure a particular flow in an entire app and are used to generate baseline
+ profiles. They're analogous to integration tests.
+- **Micro benchmarks** measure a specific operation or set of small operations but running them
+ many times and collecting aggregate statistics. They're used to compare detailed performance of
+ competing implementations, detect specific performance regressions, and inform optimizations.
+ They're analogous to unit tests.
+
+## Macro benchmarks
+
+### Baseline Profiles
The sample apps can be used to extract 'baseline profiles' to improve code loading time after first
install. See [baseline profiles](https://developer.android.com/studio/profile/baselineprofiles).
@@ -36,7 +47,7 @@ This will create an output file separated by module and then also by package as
profile for each module can be added into its /src/main directory as `baseline-prof.txt`. Then on a
release build this will be included with the resulting APK/binary.
-## dungeon-benchmark
+### dungeon-benchmark
These are benchmarks for the [../samples/dungeon] app. Please instead use performance-poetry where
possible.
@@ -57,11 +68,11 @@ same scenario with and without forcing the use of the profiles. To force the use
`libs.androidx.profileinstaller` dependency is included into the app under profile (dungeon in this
case) for side-loading the profiles.
-## performance-poetry
+### performance-poetry
Module of code for performance testing related to poetry applications.
-### complex-poetry
+#### complex-poetry
This application is a modified version of the samples/containers/app-poetry app which also uses the
common components in samples/containers/common and samples/containers/poetry. It modifies this
@@ -92,9 +103,23 @@ This module also includes a [robots package](performance-poetry/complex-poetry/s
that provides some utility helper 'robots' for the UiAutomator [androidx.test.uiautomator.UiDevice]
as well as scenarios specific to this Complex Poetry application.
-### complex-benchmark
+#### complex-benchmark
This is an Android Test module which hosts an application that can run androidx.macrobenchmarks.
See the kdoc on [ComplexPoetryBenchmarks.](performance-poetry/complex-benchmark/src/main/java/com/squareup/benchmarks/performance/complex/poetry/benchmark/ComplexPoetryBenchmarks.kt)
The results for this are stored in the same folder at [ComplexPoetryResults.txt.](performance-poetry/complex-benchmark/src/main/java/com/squareup/benchmarks/performance/complex/poetry/benchmark/ComplexPoetryResults.txt)
+
+## Micro benchmarks
+
+### runtime-microbenchmark
+
+This module contains benchmarks that measure the performance of the raw workflow runtime. To get
+accurate results, these benchmarks must be ran on real physical devices, not emulators.
+
+You can run them from the IDE like any other test (recommended), or with this command:
+```bash
+./gradlew :benchmarks:runtime-microbenchmark:connectedReleaseAndroidTest
+```
+
+We do not run benchmarks in CI, but we do validate that they compile by running them in "dry mode".
diff --git a/benchmarks/dungeon-benchmark/build.gradle.kts b/benchmarks/dungeon-benchmark/build.gradle.kts
index 6046db24a5..cd0e35fc5a 100644
--- a/benchmarks/dungeon-benchmark/build.gradle.kts
+++ b/benchmarks/dungeon-benchmark/build.gradle.kts
@@ -48,7 +48,7 @@ kotlin {
}
dependencies {
- implementation(libs.androidx.macro.benchmark)
+ implementation(libs.androidx.benchmark.macro)
implementation(libs.androidx.test.espresso.core)
implementation(libs.androidx.test.junit)
implementation(libs.androidx.test.uiautomator)
diff --git a/benchmarks/performance-poetry/complex-benchmark/build.gradle.kts b/benchmarks/performance-poetry/complex-benchmark/build.gradle.kts
index e60250f41e..3804863d85 100644
--- a/benchmarks/performance-poetry/complex-benchmark/build.gradle.kts
+++ b/benchmarks/performance-poetry/complex-benchmark/build.gradle.kts
@@ -57,7 +57,7 @@ kotlin {
}
dependencies {
- implementation(libs.androidx.macro.benchmark)
+ implementation(libs.androidx.benchmark.macro)
implementation(libs.androidx.test.espresso.core)
implementation(libs.androidx.test.junit)
implementation(libs.androidx.test.uiautomator)
diff --git a/benchmarks/runtime-microbenchmark/benchmark-proguard-rules.pro b/benchmarks/runtime-microbenchmark/benchmark-proguard-rules.pro
new file mode 100644
index 0000000000..e4061d2224
--- /dev/null
+++ b/benchmarks/runtime-microbenchmark/benchmark-proguard-rules.pro
@@ -0,0 +1,37 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
+-dontobfuscate
+
+-ignorewarnings
+
+-keepattributes *Annotation*
+
+-dontnote junit.framework.**
+-dontnote junit.runner.**
+
+-dontwarn androidx.test.**
+-dontwarn org.junit.**
+-dontwarn org.hamcrest.**
+-dontwarn com.squareup.javawriter.JavaWriter
+
+-keepclasseswithmembers @org.junit.runner.RunWith public class *
\ No newline at end of file
diff --git a/benchmarks/runtime-microbenchmark/build.gradle.kts b/benchmarks/runtime-microbenchmark/build.gradle.kts
new file mode 100644
index 0000000000..f2530457c5
--- /dev/null
+++ b/benchmarks/runtime-microbenchmark/build.gradle.kts
@@ -0,0 +1,77 @@
+import com.rickbusarow.kgx.libsCatalog
+import com.rickbusarow.kgx.version
+import com.squareup.workflow1.buildsrc.internal.javaTarget
+import com.squareup.workflow1.buildsrc.internal.javaTargetVersion
+
+plugins {
+ // Must be applied before kotlin-android so the convention can detect that this is a benchmark.
+ alias(libs.plugins.androidx.benchmark)
+ id("com.android.library")
+ id("kotlin-android")
+ id("app.cash.burst")
+}
+
+// Note: We are not including our defaults from .buildscript as we do not need the base Workflow
+// dependencies that those include.
+
+android {
+ compileSdk = libsCatalog.version("compileSdk").toInt()
+
+ compileOptions {
+ sourceCompatibility = javaTargetVersion
+ targetCompatibility = javaTargetVersion
+ }
+
+ kotlinOptions {
+ jvmTarget = javaTarget
+ freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn"
+ }
+
+ defaultConfig {
+ minSdk = 28
+ targetSdk = libsCatalog.version("targetSdk").toInt()
+
+ testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
+
+ // This flag is supposed to enable dry run in the test runner, and it does so locally, but for
+ // some reason it doesn't seem to be working in CI.
+ val benchmarkDryRunEnabled = project.findProperty("androidx.benchmark.dryRunMode.enable")
+ if (benchmarkDryRunEnabled == "true") {
+ println("Running benchmarks in dry mode: emulator allowed, no measurements taken, no warmup.")
+ testInstrumentationRunnerArguments["androidx.benchmark.dryRunMode.enable"] = "true"
+ } else {
+ // must be one of: 'None', 'StackSampling', or 'MethodTracing'
+ testInstrumentationRunnerArguments["androidx.benchmark.profiling.mode"] = "MethodTracing"
+ testInstrumentationRunnerArguments["androidx.benchmark.output.enable"] = "true"
+ }
+ }
+
+ testBuildType = "release"
+ buildTypes {
+ debug {
+ // Since isDebuggable can"t be modified by gradle for library modules,
+ // it must be done in a manifest - see src/androidTest/AndroidManifest.xml
+ isMinifyEnabled = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"),
+ "benchmark-proguard-rules.pro"
+ )
+ }
+ release {
+ isDefault = true
+ }
+ }
+
+ namespace = "com.squareup.benchmark.runtime.benchmark"
+ testNamespace = "$namespace.test"
+}
+
+dependencies {
+ androidTestImplementation(project(":workflow-runtime"))
+ androidTestImplementation(libs.androidx.benchmark)
+ androidTestImplementation(libs.androidx.test.espresso.core)
+ androidTestImplementation(libs.androidx.test.junit)
+ androidTestImplementation(libs.androidx.test.uiautomator)
+ androidTestImplementation(libs.kotlin.test.jdk)
+ androidTestImplementation(libs.kotlinx.coroutines.test)
+}
diff --git a/benchmarks/runtime-microbenchmark/src/androidTest/AndroidManifest.xml b/benchmarks/runtime-microbenchmark/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000000..6acd09fa34
--- /dev/null
+++ b/benchmarks/runtime-microbenchmark/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
diff --git a/benchmarks/runtime-microbenchmark/src/androidTest/kotlin/com/squareup/benchmark/runtime/benchmark/ComposeRuntimeMicrobenchmark.kt b/benchmarks/runtime-microbenchmark/src/androidTest/kotlin/com/squareup/benchmark/runtime/benchmark/ComposeRuntimeMicrobenchmark.kt
new file mode 100644
index 0000000000..8bc95ff6f8
--- /dev/null
+++ b/benchmarks/runtime-microbenchmark/src/androidTest/kotlin/com/squareup/benchmark/runtime/benchmark/ComposeRuntimeMicrobenchmark.kt
@@ -0,0 +1,314 @@
+package com.squareup.benchmark.runtime.benchmark
+
+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.NONE
+import com.squareup.workflow1.RuntimeConfig
+import com.squareup.workflow1.RuntimeConfigOptions.Companion.RuntimeOptions
+import com.squareup.workflow1.Sink
+import com.squareup.workflow1.Snapshot
+import com.squareup.workflow1.StatefulWorkflow
+import com.squareup.workflow1.StatelessWorkflow
+import com.squareup.workflow1.WorkflowAction
+import com.squareup.workflow1.WorkflowExperimentalRuntime
+import com.squareup.workflow1.WorkflowTracer
+import com.squareup.workflow1.action
+import com.squareup.workflow1.renderChild
+import com.squareup.workflow1.renderWorkflowIn
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.job
+import kotlinx.coroutines.plus
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import kotlin.test.assertEquals
+
+private const val MaxChildCount = 100
+
+/** 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
+) {
+ NONE(RuntimeOptions.NONE.runtimeConfig),
+ ALL(RuntimeOptions.ALL.runtimeConfig),
+}
+
+@OptIn(WorkflowExperimentalRuntime::class)
+@Burst
+class ComposeRuntimeMicrobenchmark(
+ private val runtime: BenchmarkRuntimeOptions = NONE,
+) {
+
+ @get:Rule val benchmarkRule = BenchmarkRule()
+
+ @Test fun initialRenderAllChildren() = benchmarkShallowWideWorkflowPropsChange(
+ childCount = MaxChildCount,
+ setupProps = ShallowWideWorkflowRoot.Props(
+ renderFirstChild = false,
+ renderOtherChildren = false,
+ ),
+ testProps = ShallowWideWorkflowRoot.Props(
+ renderFirstChild = true,
+ renderOtherChildren = true,
+ firstChildProps = 1,
+ otherChildrenProps = 1,
+ ),
+ expectedSetupRendering = 0,
+ expectedTestRendering = MaxChildCount
+ )
+
+ @Test fun initialRenderNewSibling() = benchmarkShallowWideWorkflowPropsChange(
+ childCount = MaxChildCount,
+ setupProps = ShallowWideWorkflowRoot.Props(
+ renderFirstChild = false,
+ renderOtherChildren = true,
+ otherChildrenProps = 1,
+ ),
+ testProps = ShallowWideWorkflowRoot.Props(
+ renderFirstChild = true,
+ renderOtherChildren = true,
+ firstChildProps = 1,
+ otherChildrenProps = 1,
+ ),
+ expectedSetupRendering = MaxChildCount - 1,
+ expectedTestRendering = MaxChildCount
+ )
+
+ @Test fun tearDownAllChildren() = benchmarkShallowWideWorkflowPropsChange(
+ childCount = MaxChildCount,
+ setupProps = ShallowWideWorkflowRoot.Props(
+ renderFirstChild = true,
+ renderOtherChildren = true,
+ firstChildProps = 1,
+ otherChildrenProps = 1,
+ ),
+ testProps = ShallowWideWorkflowRoot.Props(
+ renderFirstChild = false,
+ renderOtherChildren = false,
+ ),
+ expectedSetupRendering = MaxChildCount,
+ expectedTestRendering = 0
+ )
+
+ @Test fun tearDownSingleSibling() = benchmarkShallowWideWorkflowPropsChange(
+ childCount = MaxChildCount,
+ setupProps = ShallowWideWorkflowRoot.Props(
+ renderFirstChild = true,
+ renderOtherChildren = true,
+ firstChildProps = 1,
+ otherChildrenProps = 1,
+ ),
+ testProps = ShallowWideWorkflowRoot.Props(
+ renderFirstChild = false,
+ renderOtherChildren = true,
+ otherChildrenProps = 1,
+ ),
+ expectedSetupRendering = MaxChildCount,
+ expectedTestRendering = MaxChildCount - 1
+ )
+
+ @Test fun rerenderAllChildrenByPropsChange() = benchmarkShallowWideWorkflowPropsChange(
+ childCount = MaxChildCount,
+ setupProps = ShallowWideWorkflowRoot.Props(
+ firstChildProps = 1,
+ otherChildrenProps = 1,
+ ),
+ testProps = ShallowWideWorkflowRoot.Props(
+ firstChildProps = 2,
+ otherChildrenProps = 2,
+ ),
+ expectedSetupRendering = MaxChildCount,
+ expectedTestRendering = MaxChildCount * 2
+ )
+
+ @Test fun rerenderSingleSiblingByPropsChange() = benchmarkShallowWideWorkflowPropsChange(
+ childCount = MaxChildCount,
+ setupProps = ShallowWideWorkflowRoot.Props(
+ firstChildProps = 1,
+ otherChildrenProps = 1,
+ ),
+ testProps = ShallowWideWorkflowRoot.Props(
+ firstChildProps = 2,
+ otherChildrenProps = 1,
+ ),
+ expectedSetupRendering = MaxChildCount,
+ expectedTestRendering = MaxChildCount + 1
+ )
+
+ private fun benchmarkShallowWideWorkflowPropsChange(
+ childCount: Int,
+ setupProps: ShallowWideWorkflowRoot.Props,
+ testProps: ShallowWideWorkflowRoot.Props,
+ expectedSetupRendering: Int,
+ expectedTestRendering: Int,
+ ) = runTest {
+ val workflow = ShallowWideWorkflowRoot(childCount = childCount)
+ val props = MutableStateFlow(setupProps)
+ val workflowJob = Job(parent = coroutineContext.job)
+ val renderings = renderWorkflowIn(
+ workflow = workflow,
+ props = props,
+ scope = this + workflowJob,
+ runtimeConfig = runtime.runtimeConfig,
+ workflowTracer = Tracer,
+ onOutput = {}
+ )
+
+ benchmarkRule.measureRepeated {
+ runWithTimingDisabled {
+ // Clear the workflow tree.
+ props.value = setupProps
+ testScheduler.runCurrent()
+ assertEquals(expectedSetupRendering, renderings.value.rendering)
+ }
+
+ // Writing a new props value schedules the render pass that will set everything up.
+ props.value = testProps
+ // Run the render pass.
+ testScheduler.runCurrent()
+
+ assertEquals(expectedTestRendering, renderings.value.rendering)
+ }
+ workflowJob.cancel()
+ }
+
+ private fun benchmarkShallowWideWorkflowStateChange(
+ childCount: Int,
+ testState: (setStateForChild: (Int, Int) -> Unit) -> Unit,
+ expectedTestRendering: Int,
+ ) = runTest {
+ val actionSinks = arrayOfNulls>?>(childCount)
+ val workflow = ShallowWideWorkflowRoot(
+ childCount = childCount,
+ actionSinks = actionSinks
+ )
+ val props = MutableStateFlow(ShallowWideWorkflowRoot.Props())
+ val workflowJob = Job(parent = coroutineContext.job)
+ val renderings = renderWorkflowIn(
+ workflow = workflow,
+ props = props,
+ scope = this + workflowJob,
+ runtimeConfig = runtime.runtimeConfig,
+ workflowTracer = Tracer,
+ onOutput = {}
+ )
+
+ val resetStateAction = action("resetState") { this.state = 0 }
+
+ benchmarkRule.measureRepeated {
+ runWithTimingDisabled {
+ // Clear the workflow tree.
+ actionSinks.forEach { sink ->
+ sink!!.send(resetStateAction)
+ }
+ testScheduler.runCurrent()
+ assertEquals(0, renderings.value.rendering)
+
+ // We don't care about measuring the actual sending of actions into the sinks. The render
+ // won't actually happen until we advance the test scheduler below.
+ testState { index, newState ->
+ actionSinks[index]!!.send(
+ action("setState") {
+ this.state = newState
+ }
+ )
+ }
+ }
+
+ // Run the render pass.
+ testScheduler.runCurrent()
+
+ assertEquals(expectedTestRendering, renderings.value.rendering)
+ }
+ workflowJob.cancel()
+ }
+
+ @Test fun rerenderSingleSiblingViaStateChange() = benchmarkShallowWideWorkflowStateChange(
+ childCount = MaxChildCount,
+ testState = { setStateForChild -> setStateForChild(MaxChildCount / 2, 1) },
+ expectedTestRendering = 1,
+ )
+
+ @Test fun rerenderAllChildrenViaStateChange() = benchmarkShallowWideWorkflowStateChange(
+ childCount = MaxChildCount,
+ testState = { setStateForChild ->
+ repeat(MaxChildCount) {
+ setStateForChild(it, 1)
+ }
+ },
+ expectedTestRendering = MaxChildCount,
+ )
+}
+
+private object Tracer : WorkflowTracer {
+ override fun beginSection(label: String) = Trace.beginSection(label)
+ override fun endSection() = Trace.endSection()
+}
+
+private class ShallowWideWorkflowRoot(
+ private val childCount: Int,
+ private val actionSinks: Array>?>? = null,
+) : StatelessWorkflow() {
+ data class Props(
+ val renderFirstChild: Boolean = true,
+ val renderOtherChildren: Boolean = true,
+ val firstChildProps: Int = 0,
+ val otherChildrenProps: Int = 0,
+ )
+
+ private val child = Child()
+
+ override fun render(
+ renderProps: Props,
+ context: RenderContext
+ ): Int {
+ var rendering = 0
+ repeat(childCount) { childIndex ->
+ if (childIndex == 0 && renderProps.renderFirstChild) {
+ rendering += context.renderChild(
+ child = child,
+ key = childIndex.toString(),
+ props = ChildProps(index = childIndex, propsValue = renderProps.firstChildProps)
+ )
+ } else if (childIndex > 0 && renderProps.renderOtherChildren) {
+ rendering += context.renderChild(
+ child = child,
+ key = childIndex.toString(),
+ props = ChildProps(index = childIndex, propsValue = renderProps.otherChildrenProps)
+ )
+ }
+ }
+ return rendering
+ }
+
+ private data class ChildProps(
+ val index: Int,
+ val propsValue: Int,
+ )
+
+ private inner class Child : StatefulWorkflow() {
+ override fun initialState(
+ props: ChildProps,
+ snapshot: Snapshot?
+ ): Int = 0
+
+ override fun render(
+ renderProps: ChildProps,
+ renderState: Int,
+ context: RenderContext
+ ): Int {
+ if (actionSinks != null) {
+ @Suppress("UNCHECKED_CAST")
+ actionSinks[renderProps.index] = context.actionSink as Sink>
+ }
+ return renderProps.propsValue + renderState
+ }
+
+ override fun snapshotState(state: Int): Snapshot? = null
+ }
+}
diff --git a/benchmarks/runtime-microbenchmark/src/main/AndroidManifest.xml b/benchmarks/runtime-microbenchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..699668bcfd
--- /dev/null
+++ b/benchmarks/runtime-microbenchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/KotlinAndroidConventionPlugin.kt b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/KotlinAndroidConventionPlugin.kt
index 2e3226c5e0..7b58fb9ff1 100644
--- a/build-logic/src/main/java/com/squareup/workflow1/buildsrc/KotlinAndroidConventionPlugin.kt
+++ b/build-logic/src/main/java/com/squareup/workflow1/buildsrc/KotlinAndroidConventionPlugin.kt
@@ -3,7 +3,6 @@ package com.squareup.workflow1.buildsrc
import com.android.build.api.variant.AndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
-import org.gradle.configurationcache.extensions.capitalized
class KotlinAndroidConventionPlugin : Plugin {
@@ -13,16 +12,24 @@ class KotlinAndroidConventionPlugin : Plugin {
target.kotlinCommonSettings(bomConfigurationName = "implementation")
target.extensions.configure>("androidComponents") { components ->
+ val isMicrobenchmarkProject = target.plugins.hasPlugin("androidx.benchmark")
+ val buildType = if (isMicrobenchmarkProject) {
+ // Microbenchmarks are special, they only run as release.
+ "release"
+ } else {
+ "debug"
+ }
+
components.onVariants(
- selector = components.selector().withBuildType("debug")
+ selector = components.selector().withBuildType(buildType)
) { variant ->
val nameCaps = variant.name
.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
- val testTask = "connected${nameCaps}AndroidTest"
+ val testTaskName = "connected${nameCaps}AndroidTest"
target.tasks.register("prepare${nameCaps}AndroidTestArtifacts") { task ->
task.description =
- "Creates all artifacts used in `$testTask` without trying to execute tests."
- task.dependsOn(target.tasks.getByName(testTask).taskDependencies)
+ "Creates all artifacts used in `$testTaskName` without trying to execute tests."
+ task.dependsOn(target.tasks.getByName(testTaskName).taskDependencies)
}
}
}
diff --git a/build.gradle.kts b/build.gradle.kts
index 37ad3a500f..3a031f53fe 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -28,6 +28,7 @@ plugins {
alias(libs.plugins.dokka)
alias(libs.plugins.ktlint)
alias(libs.plugins.compose.compiler) apply false
+ alias(libs.plugins.androidx.benchmark) apply false
}
shardConnectedCheckTasks(project)
diff --git a/dependencies/classpath.txt b/dependencies/classpath.txt
index 0a29f2e0f6..60859c234b 100644
--- a/dependencies/classpath.txt
+++ b/dependencies/classpath.txt
@@ -1,3 +1,5 @@
+androidx.benchmark:androidx.benchmark.gradle.plugin:1.3.4
+androidx.benchmark:benchmark-gradle-plugin:1.3.4
androidx.databinding:databinding-common:8.11.1
androidx.databinding:databinding-compiler-common:8.11.1
app.cash.burst:burst-gradle-plugin:2.5.0
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8176a8cbf6..e27f7b0d0d 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,7 +11,7 @@ jdk-toolchain = "17"
androidx-activity = "1.8.2"
androidx-appcompat = "1.7.0"
-androidx-benchmark = "1.3.3"
+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
@@ -103,6 +103,7 @@ vanniktech-publish = "0.32.0"
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+androidx-benchmark = { id = "androidx.benchmark", version.ref = "androidx-benchmark" }
dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
@@ -167,7 +168,8 @@ androidx-lifecycle-viewmodel-core = { module = "androidx.lifecycle:lifecycle-vie
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" }
androidx-lifecycle-viewmodel-savedstate = { module = "androidx.lifecycle:lifecycle-viewmodel-savedstate", version.ref = "androidx-lifecycle" }
-androidx-macro-benchmark = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" }
+androidx-benchmark = { module = "androidx.benchmark:benchmark-junit4", version.ref = "androidx-benchmark" }
+androidx-benchmark-macro = { module = "androidx.benchmark:benchmark-macro-junit4", version.ref = "androidx-benchmark" }
androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstaller", version.ref = "androidx-profileinstaller" }
@@ -271,7 +273,7 @@ squareup-moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.re
squareup-okio = { module = "com.squareup.okio:okio", version.ref = "squareup-okio" }
-squareup-papa = { module = "com.squareup.papa:papa", version.ref = "squareup-papa"}
+squareup-papa = { module = "com.squareup.papa:papa", version.ref = "squareup-papa" }
squareup-radiography = { module = "com.squareup.radiography:radiography", version.ref = "squareup-radiography" }
diff --git a/lint_docs.sh b/lint_docs.sh
index c4025e26e3..6f5a1b105a 100755
--- a/lint_docs.sh
+++ b/lint_docs.sh
@@ -12,12 +12,15 @@ TUTORIALS_DIR='./samples/tutorial'
TUTORIALS_STYLE=.markdownlint-tutorials.rb
# CHANGELOG is an mkdocs redirect pointer, not valid markdown.
+# The benchmarks markdown file started failing on existing markdown that doesn't voilate the failed
+# check, might be an mdl bug.
find . \
-name '*.md' \
-not -name 'CHANGELOG.md' \
-not -path './.github/*' \
-not -path $TUTORIALS_DIR/'*' \
-not -path './compose/*' \
+ -not -path './benchmarks/*' \
| xargs mdl --style $STYLE --ignore-front-matter \
find $TUTORIALS_DIR \
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 4888917495..44ad37e0a8 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -34,6 +34,7 @@ dependencyResolutionManagement {
}
include(
+ ":benchmarks:runtime-microbenchmark",
":benchmarks:dungeon-benchmark",
":benchmarks:performance-poetry:complex-benchmark",
":benchmarks:performance-poetry:complex-poetry",
diff --git a/workflow-config/config-android/build.gradle.kts b/workflow-config/config-android/build.gradle.kts
index b263e453fd..134247ccda 100644
--- a/workflow-config/config-android/build.gradle.kts
+++ b/workflow-config/config-android/build.gradle.kts
@@ -10,6 +10,7 @@ android {
buildFeatures.buildConfig = true
val runtimeConfig = project.findProperty("workflow.runtime") ?: "baseline"
+ println("Workflow Runtime Configuration via test: 'workflow.runtime': '$runtimeConfig'")
defaultConfig {
buildConfigField("String", "WORKFLOW_RUNTIME", "\"$runtimeConfig\"")