diff --git a/algorithm-benchmark/README.md b/algorithm-benchmark/README.md new file mode 100644 index 0000000000..c0879418a3 --- /dev/null +++ b/algorithm-benchmark/README.md @@ -0,0 +1,3 @@ +# algorithm-benchmark + +A benchmark module for [algorithm](../algorithm) diff --git a/algorithm-benchmark/build.gradle.kts b/algorithm-benchmark/build.gradle.kts new file mode 100644 index 0000000000..0d607bbde3 --- /dev/null +++ b/algorithm-benchmark/build.gradle.kts @@ -0,0 +1,75 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import com.alexvanyo.composelife.buildlogic.FormFactor +import com.alexvanyo.composelife.buildlogic.configureGradleManagedDevices +import com.slack.keeper.KeeperExtension +import com.slack.keeper.optInToKeeper + +plugins { + alias(libs.plugins.convention.kotlinMultiplatform) + alias(libs.plugins.convention.androidApplication) + alias(libs.plugins.convention.androidApplicationCompose) + alias(libs.plugins.convention.androidApplicationTesting) + alias(libs.plugins.convention.detekt) + alias(libs.plugins.convention.kotlinMultiplatformCompose) + kotlin("plugin.serialization") version libs.versions.kotlin + alias(libs.plugins.gradleDependenciesSorter) + alias(libs.plugins.keeper) +} + +android { + namespace = "com.alexvanyo.composelife.algorithm.benchmark" + defaultConfig { + applicationId = "com.alexvanyo.composelife.algorithm.benchmark" + minSdk = 21 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner" + testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] = "EMULATOR" + } + configureGradleManagedDevices(enumValues().toSet(), this) +} + +keeper { + automaticR8RepoManagement.set(false) + traceReferences {} +} + +kotlin { + androidTarget() + + sourceSets { + val androidMain by getting { + dependencies { + implementation(libs.androidx.benchmark.micro.junit4) + } + } + val commonTest by getting { + dependencies { + implementation(projects.algorithm) + implementation(projects.dispatchersTest) + implementation(projects.patterns) + } + } + val androidInstrumentedTest by getting { + dependencies { + implementation(libs.testParameterInjector.junit4) + } + } + } +} diff --git a/algorithm-benchmark/consumer-rules.pro b/algorithm-benchmark/consumer-rules.pro new file mode 100644 index 0000000000..ff59496d81 --- /dev/null +++ b/algorithm-benchmark/consumer-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# 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 \ No newline at end of file diff --git a/algorithm-benchmark/proguard-rules.pro b/algorithm-benchmark/proguard-rules.pro new file mode 100644 index 0000000000..2f9dc5a47e --- /dev/null +++ b/algorithm-benchmark/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# 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 diff --git a/algorithm-benchmark/release-badging.txt b/algorithm-benchmark/release-badging.txt new file mode 100644 index 0000000000..2b36e84498 --- /dev/null +++ b/algorithm-benchmark/release-badging.txt @@ -0,0 +1,15 @@ +package: name='com.alexvanyo.composelife.algorithm.benchmark' versionCode='1' versionName='1.0' platformBuildVersionName='15' platformBuildVersionCode='35' compileSdkVersion='35' compileSdkVersionCodename='15' +sdkVersion:'21' +targetSdkVersion:'34' +uses-permission: name='android.permission.WRITE_EXTERNAL_STORAGE' +uses-permission: name='android.permission.READ_EXTERNAL_STORAGE' +application: label='' icon='' +feature-group: label='' + uses-feature: name='android.hardware.faketouch' + uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' +other-activities +supports-screens: 'small' 'normal' 'large' 'xlarge' +supports-any-density: 'true' +locales: '--_--' +densities: '160' '65535' +native-code: 'arm64-v8a' 'armeabi-v7a' 'x86' 'x86_64' diff --git a/algorithm-benchmark/src/androidInstrumentedTest/kotlin/com/alexvanyo/composelife/algorithm/GameOfLifeAlgorithmBenchmarks.kt b/algorithm-benchmark/src/androidInstrumentedTest/kotlin/com/alexvanyo/composelife/algorithm/GameOfLifeAlgorithmBenchmarks.kt new file mode 100644 index 0000000000..c466540e0c --- /dev/null +++ b/algorithm-benchmark/src/androidInstrumentedTest/kotlin/com/alexvanyo/composelife/algorithm/GameOfLifeAlgorithmBenchmarks.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alexvanyo.composelife.algorithm + +import androidx.benchmark.junit4.BenchmarkRule +import androidx.benchmark.junit4.measureRepeated +import androidx.compose.ui.unit.IntOffset +import com.alexvanyo.composelife.dispatchers.ComposeLifeDispatchers +import com.alexvanyo.composelife.dispatchers.TestComposeLifeDispatchers +import com.alexvanyo.composelife.model.CellState +import com.alexvanyo.composelife.patterns.GameOfLifeTestPattern +import com.alexvanyo.composelife.patterns.GosperGliderGunPattern +import com.google.testing.junit.testparameterinjector.TestParameter +import com.google.testing.junit.testparameterinjector.TestParameterInjector +import com.google.testing.junit.testparameterinjector.TestParameterValuesProvider +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.junit.Rule +import org.junit.runner.RunWith +import kotlin.test.Test + +@RunWith(TestParameterInjector::class) +class GameOfLifeAlgorithmBenchmarks { + + @get:Rule + val benchmarkRule = BenchmarkRule() + + class GameOfLifeAlgorithmFactory( + private val algorithmName: String, + val factory: (dispatchers: ComposeLifeDispatchers) -> GameOfLifeAlgorithm, + ) { + override fun toString(): String = algorithmName + + class Provider : TestParameterValuesProvider() { + override fun provideValues(context: Context?) = + listOf( + GameOfLifeAlgorithmFactory("Naive Algorithm") { + NaiveGameOfLifeAlgorithm(it) + }, + GameOfLifeAlgorithmFactory("HashLife Algorithm") { + HashLifeAlgorithm(it) + }, + ) + } + } + + class CellStateMapper( + private val name: String, + val mapper: (CellState) -> CellState, + ) { + override fun toString(): String = name + + class Provider : TestParameterValuesProvider() { + override fun provideValues(context: Context?) = + listOf( + CellStateMapper("Identity") { cellState -> + cellState + }, + CellStateMapper("Flip across x-axis") { cellState -> + CellState(cellState.aliveCells.map { cell -> IntOffset(cell.x, -cell.y) }.toSet()) + }, + CellStateMapper("Flip across y-axis") { cellState -> + CellState(cellState.aliveCells.map { cell -> IntOffset(-cell.x, cell.y) }.toSet()) + }, + CellStateMapper("Flip across x = y") { cellState -> + CellState(cellState.aliveCells.map { cell -> IntOffset(cell.y, cell.x) }.toSet()) + }, + CellStateMapper("Translate by an arbitrary amount") { cellState -> + CellState(cellState.aliveCells.map { cell -> cell + IntOffset(157, 72) }.toSet()) + }, + ) + } + } + + private val testPattern: GameOfLifeTestPattern = GosperGliderGunPattern + + @TestParameter(valuesProvider = GameOfLifeAlgorithmFactory.Provider::class) + lateinit var algorithmFactory: GameOfLifeAlgorithmFactory + + @TestParameter(valuesProvider = CellStateMapper.Provider::class) + lateinit var cellStateMapper: CellStateMapper + + @Test + fun generations_100() { + @OptIn(ExperimentalCoroutinesApi::class) + val testDispatcher = UnconfinedTestDispatcher() + + benchmarkRule.measureRepeated { + runBlocking { + val algorithm = runWithTimingDisabled { + algorithmFactory.factory( + TestComposeLifeDispatchers( + generalTestDispatcher = testDispatcher, + cellTickerTestDispatcher = testDispatcher, + ), + ) + } + val originalCellState = runWithTimingDisabled { + cellStateMapper.mapper(testPattern.seedCellState) + } + + algorithm.computeGenerationsWithStep( + originalCellState = originalCellState, + step = 1, + ) + .take(100) + .collect {} + } + } + } +} diff --git a/algorithm-benchmark/src/androidMain/AndroidManifest.xml b/algorithm-benchmark/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000000..c334bbd357 --- /dev/null +++ b/algorithm-benchmark/src/androidMain/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + diff --git a/algorithm-benchmark/src/androidUnitTest/resources/com/alexvanyo/robolectric.properties b/algorithm-benchmark/src/androidUnitTest/resources/com/alexvanyo/robolectric.properties new file mode 100644 index 0000000000..27f0bf468e --- /dev/null +++ b/algorithm-benchmark/src/androidUnitTest/resources/com/alexvanyo/robolectric.properties @@ -0,0 +1,3 @@ +# Local API version for Robolectric. This will be overridden in CI for parameterization. +sdk=35 +application=com.alexvanyo.composelife.test.TestInjectApplication diff --git a/algorithm-benchmark/staging-proguard-rules.pro b/algorithm-benchmark/staging-proguard-rules.pro new file mode 100644 index 0000000000..d53350658d --- /dev/null +++ b/algorithm-benchmark/staging-proguard-rules.pro @@ -0,0 +1,28 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# 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 + +-keep,allowobfuscation class androidx.compose.runtime.MonotonicFrameClock { + *; +} +-keep,allowobfuscation class androidx.compose.ui.platform.InfiniteAnimationPolicy { + *; +} diff --git a/algorithm-benchmark/staging-test-proguard-rules.pro b/algorithm-benchmark/staging-test-proguard-rules.pro new file mode 100644 index 0000000000..1e4edb378b --- /dev/null +++ b/algorithm-benchmark/staging-test-proguard-rules.pro @@ -0,0 +1,49 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle.kts. +# +# 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 + +# Added due to dependency on Material via accessibility-test-framework +-dontwarn androidx.appcompat.graphics.drawable.DrawableWrapper + +-dontwarn androidx.test.platform.app.AppComponentFactoryRegistry +-dontwarn androidx.test.platform.concurrent.DirectExecutor +-dontwarn androidx.work.Data$Builder +-dontwarn androidx.work.Data +-dontwarn androidx.work.ForegroundInfo +-dontwarn androidx.work.ListenableWorker$Result +-dontwarn androidx.work.OneTimeWorkRequest$Builder +-dontwarn androidx.work.OneTimeWorkRequest +-dontwarn androidx.work.Operation +-dontwarn androidx.work.OutOfQuotaPolicy +-dontwarn androidx.work.WorkManager +-dontwarn androidx.work.WorkRequest$Builder +-dontwarn androidx.work.WorkRequest +-dontwarn androidx.work.Worker +-dontwarn androidx.work.WorkerParameters +-dontwarn androidx.work.impl.utils.futures.SettableFuture +-dontwarn androidx.work.multiprocess.RemoteListenableWorker +-dontwarn java.beans.BeanInfo +-dontwarn java.beans.FeatureDescriptor +-dontwarn java.beans.IntrospectionException +-dontwarn java.beans.Introspector +-dontwarn java.beans.PropertyDescriptor +-dontwarn java.lang.reflect.AnnotatedType +-dontwarn javax.lang.model.element.Modifier diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index afe631b703..5c4080e295 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -20,6 +20,7 @@ pluginManagement { repositories { gradlePluginPortal() google() + mavenCentral() } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index af42883a52..c7653de87e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -133,6 +133,7 @@ androidx-activityCompose = { group = "androidx.activity", name = "activity-compo androidx-annotation = { group = "androidx.annotation", name = "annotation", version.ref = "androidxAnnotation" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "androidxAppcompat" } androidx-benchmark-macro-junit4 = { group = "androidx.benchmark", name = "benchmark-macro-junit4", version.ref = "androidxBenchmark" } +androidx-benchmark-micro-junit4 = { group = "androidx.benchmark", name = "benchmark-junit4", version.ref = "androidxBenchmark" } androidx-compose-animation = { group = "androidx.compose.animation", name = "animation" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom-alpha", version.ref = "androidxComposeBomAlpha" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } @@ -260,9 +261,11 @@ convention-kotlinMultiplatformCompose = { id = "com.alexvanyo.composelife.kotlin convention-mergeJacoco = { id = "com.alexvanyo.composelife.mergeJacoco" } android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } androidx-baselineProfile = { id = "androidx.baselineprofile", version.ref = "androidxBenchmark" } +androidx-benchmark = { id = "androidx.benchmark", version.ref = "androidxBenchmark" } compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } gradleDependenciesSorter = { id = "com.squareup.sort-dependencies", version.ref = "gradleDependenciesSorter" } +keeper = { id = "com.slack.keeper" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } diff --git a/patterns/src/jvmMain/kotlin/com/alexvanyo/composelife/patterns/OtherPatterns.kt b/patterns/src/jvmMain/kotlin/com/alexvanyo/composelife/patterns/OtherPatterns.kt index 4fbffe3c1a..9ad7b7fec5 100644 --- a/patterns/src/jvmMain/kotlin/com/alexvanyo/composelife/patterns/OtherPatterns.kt +++ b/patterns/src/jvmMain/kotlin/com/alexvanyo/composelife/patterns/OtherPatterns.kt @@ -16,6 +16,7 @@ package com.alexvanyo.composelife.patterns +import com.alexvanyo.composelife.model.RunLengthEncodedCellStateSerializer import com.alexvanyo.composelife.model.emptyCellState import com.alexvanyo.composelife.model.toCellState @@ -116,3 +117,30 @@ data object SixLongLinePattern : GameOfLifeTestPattern( """.toCellState(), ) + List(50) { emptyCellState() }, ) + +data object GosperGliderGunPattern : GameOfLifeTestPattern( + patternName = "Gosper glider gun", + """ + |........................O........... + |......................O.O........... + |............OO......OO............OO + |...........O...O....OO............OO + |OO........O.....O...OO.............. + |OO........O...O.OO....O.O........... + |..........O.....O.......O........... + |...........O...O.................... + |............OO...................... + """.toCellState(), + listOf( + """ + x = 36, y = 9, rule = B3/S23 + 23bo${'$'}21bobo${'$'}12bo7bobo11b2o${'$'}11b2o6bo2bo11b2o${'$'}2o8b2o4b2o2bobo${'$'}2o7b3o4b2o + 3bobo${'$'}10b2o4b2o5bo${'$'}11b2o${'$'}12bo! + """.trimIndent().toCellState(fixedFormatCellStateSerializer = RunLengthEncodedCellStateSerializer), + """ + x = 36, y = 9, rule = B3/S23 + 22bo${'$'}21bobo${'$'}11b2o7bob2o10b2o${'$'}10bobo6b2ob2o10b2o${'$'}2o7bo6b3obob2o${'$'}2o7bo2b + o2bo2bo2bobo${'$'}9bo6b2o4bo${'$'}10bobo${'$'}11b2o! + """.trimIndent().toCellState(fixedFormatCellStateSerializer = RunLengthEncodedCellStateSerializer), + ), +) diff --git a/settings.gradle.kts b/settings.gradle.kts index bc711441c1..7560f9a7e9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -67,6 +67,7 @@ develocity { rootProject.name = "ComposeLife" include(":algorithm") +include(":algorithm-benchmark") include(":app") include(":app-baseline-profile-generator") include(":app-benchmark")