Loading mechanics/benchmark/tests/src/com/android/mechanics/benchmark/ComposeBaselineBenchmark.kt 0 → 100644 +105 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 * * http://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.android.mechanics.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.compose.animation.core.Animatable import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.util.fastForEach import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import platform.test.motion.compose.runMonotonicClockTest /** Benchmark, which will execute on an Android device. Previous results: go/mm-microbenchmarks */ @RunWith(AndroidJUnit4::class) class ComposeBaselineBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() // Compose specific @Test fun writeState_1snapshotFlow() = runMonotonicClockTest { val composeState = mutableFloatStateOf(0f) var lastRead = 0f snapshotFlow { composeState.floatValue }.onEach { lastRead = it }.launchIn(backgroundScope) benchmarkRule.measureRepeated { composeState.floatValue++ Snapshot.sendApplyNotifications() testScheduler.advanceTimeBy(16) } check(lastRead == composeState.floatValue) { "snapshotFlow lastRead $lastRead != ${composeState.floatValue} (current composeState)" } } @Test fun writeState_100snapshotFlow() = runMonotonicClockTest { val composeState = mutableFloatStateOf(0f) repeat(100) { snapshotFlow { composeState.floatValue }.launchIn(backgroundScope) } benchmarkRule.measureRepeated { composeState.floatValue++ Snapshot.sendApplyNotifications() testScheduler.advanceTimeBy(16) } } @Test fun readAnimatableValue_100animatables_keepRunning() = runMonotonicClockTest { val anim = List(100) { Animatable(0f) } benchmarkRule.measureRepeated { testScheduler.advanceTimeBy(16) anim.fastForEach { it.value if (!it.isRunning) { launch { it.animateTo(if (it.targetValue != 0f) 0f else 1f) } } } } testScheduler.advanceTimeBy(2000) } @Test fun readAnimatableValue_100animatables_restartEveryFrame() = runMonotonicClockTest { val animatables = List(100) { Animatable(0f) } benchmarkRule.measureRepeated { testScheduler.advanceTimeBy(16) animatables.fastForEach { animatable -> animatable.value launch { animatable.animateTo(if (animatable.targetValue != 0f) 0f else 1f) } } } testScheduler.advanceTimeBy(2000) } } mechanics/benchmark/tests/src/com/android/mechanics/benchmark/MotionValueBenchmark.kt +37 −20 Original line number Diff line number Diff line Loading @@ -21,7 +21,7 @@ import androidx.benchmark.junit4.measureRepeated import androidx.compose.runtime.MutableFloatState import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.util.fastForEach import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.mechanics.DistanceGestureContext import com.android.mechanics.MotionValue Loading @@ -32,7 +32,6 @@ import com.android.mechanics.spec.MotionSpec import com.android.mechanics.spec.buildDirectionalMotionSpec import com.android.mechanics.spring.SpringParameters import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.junit.Rule import org.junit.Test Loading Loading @@ -106,36 +105,38 @@ class MotionValueBenchmark { } } // Compose specific @Test fun writeState_1snapshotFlow() = runMonotonicClockTest { val composeState = mutableFloatStateOf(0f) var lastRead = 0f snapshotFlow { composeState.floatValue }.onEach { lastRead = it }.launchIn(backgroundScope) fun stable_writeInput_AND_readOutput_keepRunning() = runMonotonicClockTest { val data = testData() keepRunningDuringTest(data.motionValue) benchmarkRule.measureRepeated { composeState.floatValue++ Snapshot.sendApplyNotifications() data.input.floatValue += 1f testScheduler.advanceTimeBy(16) data.motionValue.floatValue } } check(lastRead == composeState.floatValue) { "snapshotFlow lastRead $lastRead != ${composeState.floatValue} (current composeState)" @Test fun stable_writeInput_AND_readOutput_100motionValues_keepRunning() = runMonotonicClockTest { val dataList = List(100) { testData() } dataList.forEach { keepRunningDuringTest(it.motionValue) } benchmarkRule.measureRepeated { dataList.fastForEach { it.input.floatValue += 1f } testScheduler.advanceTimeBy(16) dataList.fastForEach { it.motionValue.floatValue } } } @Test fun writeState_100snapshotFlow() = runMonotonicClockTest { val composeState = mutableFloatStateOf(0f) repeat(100) { snapshotFlow { composeState.floatValue }.launchIn(backgroundScope) } fun stable_readOutput_100motionValues_keepRunning() = runMonotonicClockTest { val dataList = List(100) { testData() } dataList.forEach { keepRunningDuringTest(it.motionValue) } benchmarkRule.measureRepeated { composeState.floatValue++ Snapshot.sendApplyNotifications() testScheduler.advanceTimeBy(16) dataList.fastForEach { it.motionValue.floatValue } } } Loading Loading @@ -169,8 +170,24 @@ class MotionValueBenchmark { if (data.motionValue.isStable) { data.gestureContext.reset(0f, data.gestureContext.direction.opposite) } testScheduler.advanceTimeBy(16) data.motionValue.floatValue } } @Test fun unstable_resetGestureContext_readOutput_100motionValues() = runMonotonicClockTest { val dataList = List(100) { testData(input = 1f, spec = MotionSpec.ZeroToOne_AtOne) } dataList.forEach { keepRunningDuringTest(it.motionValue) } benchmarkRule.measureRepeated { dataList.fastForEach { data -> if (data.motionValue.isStable) { data.gestureContext.reset(0f, data.gestureContext.direction.opposite) } } testScheduler.advanceTimeBy(16) dataList.fastForEach { it.motionValue.floatValue } } } Loading Loading @@ -217,8 +234,8 @@ class MotionValueBenchmark { data.gestureContext.dragOffset += if (isMax) 0.01f else -0.01f } data.motionValue.floatValue testScheduler.advanceTimeBy(16) data.motionValue.floatValue } } } mechanics/tests/src/com/android/mechanics/MotionValueTest.kt +3 −22 Original line number Diff line number Diff line Loading @@ -24,8 +24,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.TestMonotonicFrameClock import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.mechanics.spec.Breakpoint Loading Loading @@ -57,18 +55,15 @@ import com.android.mechanics.testing.isStable import com.android.mechanics.testing.output import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import org.junit.Rule import org.junit.Test import org.junit.rules.ExternalResource import org.junit.runner.RunWith import platform.test.motion.MotionTestRule import platform.test.motion.compose.runMonotonicClockTest import platform.test.motion.golden.DataPointTypes.string import platform.test.motion.testing.createGoldenPathManager Loading Loading @@ -547,9 +542,10 @@ class MotionValueTest { } @Test fun keepRunning_concurrentInvocationThrows() = runTestWithFrameClock { testScheduler, _ -> fun keepRunning_concurrentInvocationThrows() = runMonotonicClockTest { val underTest = MotionValue({ 1f }, FakeGestureContext, label = "Foo") val realJob = launch { underTest.keepRunning() } doOnTearDown { realJob.cancel() } testScheduler.runCurrent() assertThat(realJob.isActive).isTrue() Loading @@ -561,7 +557,6 @@ class MotionValueTest { assertThat(e).hasMessageThat().contains("MotionValue(Foo) is already running") } assertThat(realJob.isActive).isTrue() realJob.cancel() } @Test Loading Loading @@ -717,19 +712,6 @@ class MotionValueTest { assertThat(underTest.debugInspector()).isNotSameInstanceAs(originalInspector) } @OptIn(ExperimentalTestApi::class) private fun runTestWithFrameClock( testBody: suspend CoroutineScope.( testScheduler: TestCoroutineScheduler, backgroundScope: CoroutineScope, ) -> Unit ) = runTest { val testScope: TestScope = this withContext(TestMonotonicFrameClock(testScope, FrameDelayNanos)) { testBody(testScope.testScheduler, testScope.backgroundScope) } } class WtfLogRule : ExternalResource() { val loggedFailures = mutableListOf<String>() Loading Loading @@ -760,7 +742,6 @@ class MotionValueTest { override val dragOffset: Float get() = 0f } private val FrameDelayNanos: Long = 16_000_000L fun specBuilder( initialMapping: Mapping = Mapping.Identity, Loading Loading
mechanics/benchmark/tests/src/com/android/mechanics/benchmark/ComposeBaselineBenchmark.kt 0 → 100644 +105 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 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 * * http://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.android.mechanics.benchmark import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.compose.animation.core.Animatable import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.util.fastForEach import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import platform.test.motion.compose.runMonotonicClockTest /** Benchmark, which will execute on an Android device. Previous results: go/mm-microbenchmarks */ @RunWith(AndroidJUnit4::class) class ComposeBaselineBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() // Compose specific @Test fun writeState_1snapshotFlow() = runMonotonicClockTest { val composeState = mutableFloatStateOf(0f) var lastRead = 0f snapshotFlow { composeState.floatValue }.onEach { lastRead = it }.launchIn(backgroundScope) benchmarkRule.measureRepeated { composeState.floatValue++ Snapshot.sendApplyNotifications() testScheduler.advanceTimeBy(16) } check(lastRead == composeState.floatValue) { "snapshotFlow lastRead $lastRead != ${composeState.floatValue} (current composeState)" } } @Test fun writeState_100snapshotFlow() = runMonotonicClockTest { val composeState = mutableFloatStateOf(0f) repeat(100) { snapshotFlow { composeState.floatValue }.launchIn(backgroundScope) } benchmarkRule.measureRepeated { composeState.floatValue++ Snapshot.sendApplyNotifications() testScheduler.advanceTimeBy(16) } } @Test fun readAnimatableValue_100animatables_keepRunning() = runMonotonicClockTest { val anim = List(100) { Animatable(0f) } benchmarkRule.measureRepeated { testScheduler.advanceTimeBy(16) anim.fastForEach { it.value if (!it.isRunning) { launch { it.animateTo(if (it.targetValue != 0f) 0f else 1f) } } } } testScheduler.advanceTimeBy(2000) } @Test fun readAnimatableValue_100animatables_restartEveryFrame() = runMonotonicClockTest { val animatables = List(100) { Animatable(0f) } benchmarkRule.measureRepeated { testScheduler.advanceTimeBy(16) animatables.fastForEach { animatable -> animatable.value launch { animatable.animateTo(if (animatable.targetValue != 0f) 0f else 1f) } } } testScheduler.advanceTimeBy(2000) } }
mechanics/benchmark/tests/src/com/android/mechanics/benchmark/MotionValueBenchmark.kt +37 −20 Original line number Diff line number Diff line Loading @@ -21,7 +21,7 @@ import androidx.benchmark.junit4.measureRepeated import androidx.compose.runtime.MutableFloatState import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshots.Snapshot import androidx.compose.ui.util.fastForEach import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.mechanics.DistanceGestureContext import com.android.mechanics.MotionValue Loading @@ -32,7 +32,6 @@ import com.android.mechanics.spec.MotionSpec import com.android.mechanics.spec.buildDirectionalMotionSpec import com.android.mechanics.spring.SpringParameters import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.junit.Rule import org.junit.Test Loading Loading @@ -106,36 +105,38 @@ class MotionValueBenchmark { } } // Compose specific @Test fun writeState_1snapshotFlow() = runMonotonicClockTest { val composeState = mutableFloatStateOf(0f) var lastRead = 0f snapshotFlow { composeState.floatValue }.onEach { lastRead = it }.launchIn(backgroundScope) fun stable_writeInput_AND_readOutput_keepRunning() = runMonotonicClockTest { val data = testData() keepRunningDuringTest(data.motionValue) benchmarkRule.measureRepeated { composeState.floatValue++ Snapshot.sendApplyNotifications() data.input.floatValue += 1f testScheduler.advanceTimeBy(16) data.motionValue.floatValue } } check(lastRead == composeState.floatValue) { "snapshotFlow lastRead $lastRead != ${composeState.floatValue} (current composeState)" @Test fun stable_writeInput_AND_readOutput_100motionValues_keepRunning() = runMonotonicClockTest { val dataList = List(100) { testData() } dataList.forEach { keepRunningDuringTest(it.motionValue) } benchmarkRule.measureRepeated { dataList.fastForEach { it.input.floatValue += 1f } testScheduler.advanceTimeBy(16) dataList.fastForEach { it.motionValue.floatValue } } } @Test fun writeState_100snapshotFlow() = runMonotonicClockTest { val composeState = mutableFloatStateOf(0f) repeat(100) { snapshotFlow { composeState.floatValue }.launchIn(backgroundScope) } fun stable_readOutput_100motionValues_keepRunning() = runMonotonicClockTest { val dataList = List(100) { testData() } dataList.forEach { keepRunningDuringTest(it.motionValue) } benchmarkRule.measureRepeated { composeState.floatValue++ Snapshot.sendApplyNotifications() testScheduler.advanceTimeBy(16) dataList.fastForEach { it.motionValue.floatValue } } } Loading Loading @@ -169,8 +170,24 @@ class MotionValueBenchmark { if (data.motionValue.isStable) { data.gestureContext.reset(0f, data.gestureContext.direction.opposite) } testScheduler.advanceTimeBy(16) data.motionValue.floatValue } } @Test fun unstable_resetGestureContext_readOutput_100motionValues() = runMonotonicClockTest { val dataList = List(100) { testData(input = 1f, spec = MotionSpec.ZeroToOne_AtOne) } dataList.forEach { keepRunningDuringTest(it.motionValue) } benchmarkRule.measureRepeated { dataList.fastForEach { data -> if (data.motionValue.isStable) { data.gestureContext.reset(0f, data.gestureContext.direction.opposite) } } testScheduler.advanceTimeBy(16) dataList.fastForEach { it.motionValue.floatValue } } } Loading Loading @@ -217,8 +234,8 @@ class MotionValueBenchmark { data.gestureContext.dragOffset += if (isMax) 0.01f else -0.01f } data.motionValue.floatValue testScheduler.advanceTimeBy(16) data.motionValue.floatValue } } }
mechanics/tests/src/com/android/mechanics/MotionValueTest.kt +3 −22 Original line number Diff line number Diff line Loading @@ -24,8 +24,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.TestMonotonicFrameClock import androidx.compose.ui.test.junit4.createComposeRule import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.mechanics.spec.Breakpoint Loading Loading @@ -57,18 +55,15 @@ import com.android.mechanics.testing.isStable import com.android.mechanics.testing.output import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withContext import org.junit.Rule import org.junit.Test import org.junit.rules.ExternalResource import org.junit.runner.RunWith import platform.test.motion.MotionTestRule import platform.test.motion.compose.runMonotonicClockTest import platform.test.motion.golden.DataPointTypes.string import platform.test.motion.testing.createGoldenPathManager Loading Loading @@ -547,9 +542,10 @@ class MotionValueTest { } @Test fun keepRunning_concurrentInvocationThrows() = runTestWithFrameClock { testScheduler, _ -> fun keepRunning_concurrentInvocationThrows() = runMonotonicClockTest { val underTest = MotionValue({ 1f }, FakeGestureContext, label = "Foo") val realJob = launch { underTest.keepRunning() } doOnTearDown { realJob.cancel() } testScheduler.runCurrent() assertThat(realJob.isActive).isTrue() Loading @@ -561,7 +557,6 @@ class MotionValueTest { assertThat(e).hasMessageThat().contains("MotionValue(Foo) is already running") } assertThat(realJob.isActive).isTrue() realJob.cancel() } @Test Loading Loading @@ -717,19 +712,6 @@ class MotionValueTest { assertThat(underTest.debugInspector()).isNotSameInstanceAs(originalInspector) } @OptIn(ExperimentalTestApi::class) private fun runTestWithFrameClock( testBody: suspend CoroutineScope.( testScheduler: TestCoroutineScheduler, backgroundScope: CoroutineScope, ) -> Unit ) = runTest { val testScope: TestScope = this withContext(TestMonotonicFrameClock(testScope, FrameDelayNanos)) { testBody(testScope.testScheduler, testScope.backgroundScope) } } class WtfLogRule : ExternalResource() { val loggedFailures = mutableListOf<String>() Loading Loading @@ -760,7 +742,6 @@ class MotionValueTest { override val dragOffset: Float get() = 0f } private val FrameDelayNanos: Long = 16_000_000L fun specBuilder( initialMapping: Mapping = Mapping.Identity, Loading