Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 8c5e4b54 authored by Omar Miatello's avatar Omar Miatello
Browse files

MM: add benchmarks for stable and unstable values during a keepRunning

Add comparison with Compose Spring animations.

Test: MotionValueBenchmark (run on AS, results go/mm-microbenchmarks)
Test: ComposeBaselineBenchmark
Test: atest MotionValueTest
Bug: 404975090
Flag: EXEMPT update tests and add benchmarks
Change-Id: I759e86edb415936ebb2245cebcbb1bd0add72fd7
parent 118924ac
Loading
Loading
Loading
Loading
+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)
    }
}
+37 −20
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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 }
        }
    }

@@ -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 }
        }
    }

@@ -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
        }
    }
}
+3 −22
Original line number Diff line number Diff line
@@ -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
@@ -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

@@ -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()
@@ -561,7 +557,6 @@ class MotionValueTest {
            assertThat(e).hasMessageThat().contains("MotionValue(Foo) is already running")
        }
        assertThat(realJob.isActive).isTrue()
        realJob.cancel()
    }

    @Test
@@ -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>()

@@ -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,