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\"")