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

Commit 824eb119 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add benchmark tests for MotionValueCollection and Spring implementation" into main

parents 9b311b1c 59abfbee
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()
        }
    }
}