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

Commit 59abfbee authored by Mike Schneider's avatar Mike Schneider
Browse files

Add benchmark tests for MotionValueCollection and Spring implementation

Test: Benchmark tests
Bug: 404975104
Flag: TEST_ONLY
Change-Id: I8e850f660fdb0746c8ebd5f42e10d3951c61b4de
parent adf303d7
Loading
Loading
Loading
Loading
+82 −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.test.ext.junit.runners.AndroidJUnit4
import com.android.mechanics.spring.SpringParameters
import com.android.mechanics.spring.SpringState
import com.android.mechanics.spring.calculateUpdatedState
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
class MechanicsSpringBenchmark {
    @get:Rule val benchmarkRule = BenchmarkRule()

    @Test
    fun calculateUpdatedState_atRest() {
        val initialState = SpringState(0f, 0f)

        benchmarkRule.measureRepeated {
            initialState.calculateUpdatedState(FrameDuration, CriticallyDamped)
        }
    }

    @Test
    fun calculateUpdatedState_underDamped() {
        val initialState = SpringState(10f, -1f)

        benchmarkRule.measureRepeated {
            initialState.calculateUpdatedState(FrameDuration, UnderDamped)
        }
    }

    @Test
    fun calculateUpdatedState_criticallyDamped() {
        val initialState = SpringState(10f, -1f)

        benchmarkRule.measureRepeated {
            initialState.calculateUpdatedState(FrameDuration, CriticallyDamped)
        }
    }

    @Test
    fun calculateUpdatedState_overDamped() {
        val initialState = SpringState(10f, -1f)

        benchmarkRule.measureRepeated {
            initialState.calculateUpdatedState(FrameDuration, OverDamped)
        }
    }

    @Test
    fun isStable() {
        val initialState = SpringState(10f, -1f)

        benchmarkRule.measureRepeated { initialState.isStable(CriticallyDamped, 0.1f) }
    }

    companion object {
        val FrameDuration = 16_000_000L
        val UnderDamped = SpringParameters(stiffness = 100f, dampingRatio = 0.5f)
        val CriticallyDamped = SpringParameters(stiffness = 100f, dampingRatio = 1f)
        val OverDamped = SpringParameters(stiffness = 100f, dampingRatio = 2f)
    }
}
+185 −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.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.Snapshot
import androidx.compose.ui.util.fastForEach
import com.android.mechanics.DistanceGestureContext
import com.android.mechanics.ManagedMotionValue
import com.android.mechanics.MotionValueCollection
import com.android.mechanics.spec.Guarantee
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.builder.MotionBuilderContext
import com.android.mechanics.spec.builder.directionalMotionSpec
import com.android.mechanics.spring.SpringParameters
import kotlinx.coroutines.launch
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import platform.test.motion.compose.MonotonicClockTestScope
import platform.test.motion.compose.runMonotonicClockTest

/** Benchmark, which will execute on an Android device. Previous results: go/mm-microbenchmarks */
@RunWith(Parameterized::class)
class MotionValueCollectionBenchmark(private val instanceCount: Int) {

    companion object {
        @JvmStatic
        @Parameterized.Parameters(name = "instanceCount={0}")
        fun instanceCount() = listOf(100)
    }

    @get:Rule val benchmarkRule = BenchmarkRule()

    private val tearDownOperations = mutableListOf<() -> Unit>()

    /**
     * Runs a test block within a [MonotonicClockTestScope] provided by the underlying
     * [platform.test.motion.compose.runMonotonicClockTest] and ensures automatic cleanup.
     *
     * This mechanism provides a convenient way to register cleanup actions (e.g., stopping
     * coroutines, resetting states) that should reliably run at the end of the test, simplifying
     * test setup and teardown.
     */
    private fun runMonotonicClockTest(block: suspend MonotonicClockTestScope.() -> Unit) {
        return platform.test.motion.compose.runMonotonicClockTest {
            try {
                block()
            } finally {
                tearDownOperations.fastForEach { it.invoke() }
            }
        }
    }

    private data class TestFixture(
        val collection: MotionValueCollection,
        val gestureContext: DistanceGestureContext,
        val instances: List<MotionValueInstance>,
    )

    private data class MotionValueInstance(
        val value: ManagedMotionValue,
        val spec: MutableState<MotionSpec>,
    )

    private fun MonotonicClockTestScope.testFixture(
        initialInput: Float = 0f,
        init: (Int) -> MotionSpec = { MotionSpec.Identity },
    ): TestFixture {
        val gestureContext = DistanceGestureContext(initialInput, InputDirection.Max, 2f)
        val collection =
            MotionValueCollection(
                { gestureContext.dragOffset },
                gestureContext,
                stableThreshold = MotionBuilderContext.StableThresholdEffects,
            )

        val instances =
            List(instanceCount) {
                val spec = mutableStateOf(init(it))
                val value = collection.create(spec::value)
                MotionValueInstance(value, spec)
            }

        val keepRunningJob = launch { collection.keepRunning() }
        tearDownOperations += { keepRunningJob.cancel() }

        return TestFixture(
            collection = collection,
            gestureContext = gestureContext,
            instances = instances,
        )
    }

    private fun MonotonicClockTestScope.nextFrame() {
        Snapshot.sendApplyNotifications()
        testScheduler.advanceTimeBy(16)
    }

    @Test
    fun noChange() = runMonotonicClockTest {
        val fixture = testFixture()

        benchmarkRule.measureRepeated {
            fixture.gestureContext.dragOffset += 0f
            nextFrame()
        }
    }

    @Test
    fun changeInput() = runMonotonicClockTest {
        val fixture = testFixture()

        benchmarkRule.measureRepeated {
            fixture.gestureContext.dragOffset += 1f
            nextFrame()
        }
    }

    @Test
    fun animateOutput() = runMonotonicClockTest {
        val spec =
            MotionSpec(
                directionalMotionSpec(
                    defaultSpring = SpringParameters(stiffness = 300f, dampingRatio = .9f),
                    initialMapping = Mapping.Zero,
                ) {
                    fixedValue(breakpoint = 5f, value = 1f)
                }
            )

        val fixture = testFixture(initialInput = 4f) { spec }
        var stepSize = 1f

        benchmarkRule.measureRepeated {
            val lastInput = fixture.gestureContext.dragOffset
            if (lastInput <= .5f) stepSize = 1f else if (lastInput >= 9.5f) stepSize = -1f
            fixture.gestureContext.dragOffset = lastInput + stepSize
            nextFrame()
        }
    }

    @Test
    fun animateWithGuarantee() = runMonotonicClockTest {
        val spec =
            MotionSpec(
                directionalMotionSpec(
                    defaultSpring = SpringParameters(stiffness = 300f, dampingRatio = .9f),
                    initialMapping = Mapping.Zero,
                ) {
                    fixedValue(breakpoint = 5f, value = 1f, guarantee = Guarantee.InputDelta(4f))
                }
            )

        val fixture = testFixture { spec }
        var stepSize = 1f

        benchmarkRule.measureRepeated {
            val lastInput = fixture.gestureContext.dragOffset
            if (lastInput <= .5f) stepSize = 1f else if (lastInput >= 9.5f) stepSize = -1f
            fixture.gestureContext.dragOffset = lastInput + stepSize
            nextFrame()
        }
    }
}