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

Commit 0ce4ca80 authored by Android Build Coastguard Worker's avatar Android Build Coastguard Worker
Browse files

Snap for 14047163 from a550b4f0 to 25Q4-release

Change-Id: I62ca68e4120947599eb4a197830b268ffb3a5e6d
parents c3b637d2 a550b4f0
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()
        }
    }
}
+4 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import androidx.compose.runtime.setValue
import com.android.mechanics.MotionValue
import com.android.mechanics.impl.DiscontinuityAnimation
import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.SegmentData
import com.android.mechanics.spec.SegmentKey
import com.android.mechanics.spec.SemanticKey
@@ -88,4 +89,7 @@ internal constructor(

    val semantics: List<SemanticValue<*>>
        get() = with(segment) { spec.semantics(key) }

    val spec: MotionSpec
        get() = segment.spec
}
+10 −8
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
@@ -46,7 +47,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastCoerceAtLeast
import androidx.compose.ui.util.fastCoerceAtMost
import androidx.compose.ui.util.fastForEachIndexed
import com.android.mechanics.MotionValue
import com.android.mechanics.MotionValueState
import com.android.mechanics.spec.DirectionalMotionSpec
import com.android.mechanics.spec.Guarantee
import com.android.mechanics.spec.InputDirection
@@ -79,7 +80,7 @@ typealias OutputRangeFn =
 */
@Composable
fun DebugMotionValueVisualization(
    motionValue: MotionValue,
    motionValue: MotionValueState,
    inputRange: ClosedFloatingPointRange<Float>,
    modifier: Modifier = Modifier,
    outputRange: OutputRangeFn = DebugMotionValueVisualization.default,
@@ -87,8 +88,9 @@ fun DebugMotionValueVisualization(
) {
    val inspector = remember(motionValue) { motionValue.debugInspector() }

    val computedOutputRange =
        remember(motionValue.spec, inputRange) { outputRange(motionValue.spec, inputRange) }
    val spec = remember(motionValue) { derivedStateOf { inspector.frame.spec } }.value

    val computedOutputRange = remember(spec, inputRange) { outputRange(spec, inputRange) }
    DisposableEffect(inspector) { onDispose { inspector.dispose() } }

    val colorScheme = MaterialTheme.colorScheme
@@ -96,7 +98,7 @@ fun DebugMotionValueVisualization(
    val specColor = colorScheme.tertiary
    val valueColor = colorScheme.primary

    val primarySpec = motionValue.spec.get(inspector.frame.gestureDirection)
    val primarySpec = spec.get(inspector.frame.gestureDirection)
    val activeSegment = inspector.frame.segmentKey

    Spacer(
@@ -179,7 +181,7 @@ fun Modifier.debugMotionSpecGraph(
 */
@Composable
fun Modifier.debugMotionValueGraph(
    motionValue: MotionValue,
    motionValue: MotionValueState,
    color: Color,
    inputRange: ClosedFloatingPointRange<Float>,
    outputRange: ClosedFloatingPointRange<Float>,
@@ -241,7 +243,7 @@ fun DirectionalMotionSpec.computeOutputValueRange(
}

private data class DebugMotionValueGraphElement(
    val motionValue: MotionValue,
    val motionValue: MotionValueState,
    val color: Color,
    val inputRange: ClosedFloatingPointRange<Float>,
    val outputRange: ClosedFloatingPointRange<Float>,
@@ -269,7 +271,7 @@ private data class DebugMotionValueGraphElement(
}

private class DebugMotionValueGraphNode(
    motionValue: MotionValue,
    motionValue: MotionValueState,
    var color: Color,
    var inputRange: ClosedFloatingPointRange<Float>,
    var outputRange: ClosedFloatingPointRange<Float>,
+9 −9
Original line number Diff line number Diff line
@@ -30,25 +30,25 @@ import androidx.compose.ui.node.ObserverModifierNode
import androidx.compose.ui.node.currentValueOf
import androidx.compose.ui.node.observeReads
import androidx.compose.ui.platform.InspectorInfo
import com.android.mechanics.MotionValue
import com.android.mechanics.MotionValueState
import kotlinx.coroutines.DisposableHandle

/** Keeps track of MotionValues that are registered for debug-inspection. */
class MotionValueDebugController {
    private val observedMotionValues = mutableStateListOf<MotionValue>()
    private val observedMotionValues = mutableStateListOf<MotionValueState>()

    /**
     * Registers a [MotionValue] to be debugged.
     * Registers a [MotionValueState] to be debugged.
     *
     * Clients must call [DisposableHandle.dispose] when done.
     */
    fun register(motionValue: MotionValue): DisposableHandle {
    fun register(motionValue: MotionValueState): DisposableHandle {
        observedMotionValues.add(motionValue)
        return DisposableHandle { observedMotionValues.remove(motionValue) }
    }

    /** The currently registered `MotionValues`. */
    val observed: List<MotionValue>
    val observed: List<MotionValueState>
        get() = observedMotionValues
}

@@ -70,12 +70,12 @@ fun MotionValueDebuggerProvider(enableDebugger: Boolean = true, content: @Compos
}

/** Registers the [motionValue] with the [LocalMotionValueDebugController], if available. */
fun Modifier.debugMotionValue(motionValue: MotionValue): Modifier =
fun Modifier.debugMotionValue(motionValue: MotionValueState): Modifier =
    this.then(DebugMotionValueElement(motionValue))

/** Registers the [motionValue] with the [LocalMotionValueDebugController], if available. */
@Composable
fun DebugEffect(motionValue: MotionValue) {
fun DebugEffect(motionValue: MotionValueState) {
    val debugger = LocalMotionValueDebugController.current
    if (debugger != null) {
        DisposableEffect(debugger, motionValue) {
@@ -89,7 +89,7 @@ fun DebugEffect(motionValue: MotionValue) {
 * [DelegatableNode] to register the [motionValue] with the [LocalMotionValueDebugController], if
 * available.
 */
class DebugMotionValueNode(motionValue: MotionValue) :
class DebugMotionValueNode(motionValue: MotionValueState) :
    Modifier.Node(), DelegatableNode, CompositionLocalConsumerModifierNode, ObserverModifierNode {
    private var debugger: MotionValueDebugController? = null

@@ -118,7 +118,7 @@ class DebugMotionValueNode(motionValue: MotionValue) :
        }
}

private data class DebugMotionValueElement(val motionValue: MotionValue) :
private data class DebugMotionValueElement(val motionValue: MotionValueState) :
    ModifierNodeElement<DebugMotionValueNode>() {
    override fun create(): DebugMotionValueNode = DebugMotionValueNode(motionValue)