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

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

Snap for 13288885 from c1b28e1d to 25Q3-release

Change-Id: Idf44cf2b431af0d2455103b5d9a8364913403a22
parents d50a7a45 c1b28e1d
Loading
Loading
Loading
Loading
+35 −1
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.app.displaylib

import android.util.Log
import android.view.Display
import android.view.Display.DEFAULT_DISPLAY
import com.android.app.tracing.coroutines.flow.stateInTraced
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.tracing.traceSection
@@ -25,6 +26,7 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
import javax.inject.Qualifier
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
@@ -103,6 +105,15 @@ interface PerDisplayRepository<T> {
    fun interface InitCallback {
        fun onInit(debugName: String, instance: Any)
    }

    /**
     * Iterate over all the available displays performing the action on each object of type T.
     *
     * @param createIfAbsent If true, create instances of T if they are not already created. If
     *   false, do not and skip calling action..
     * @param action The action to perform on each instance.
     */
    fun forEach(createIfAbsent: Boolean, action: Consumer<T>)
}

/** Qualifier for [CoroutineScope] used for displaylib background tasks. */
@@ -229,6 +240,14 @@ constructor(
        return "PerDisplayInstanceRepositoryImpl(" +
            "debugName='$debugName', instances=$perDisplayInstances)"
    }

    override fun forEach(createIfAbsent: Boolean, action: Consumer<T>) {
        if (createIfAbsent) {
            allowedDisplays.value.forEach { displayId -> get(displayId)?.let { action.accept(it) } }
        } else {
            perDisplayInstances.forEach { (_, instance) -> instance?.let { action.accept(it) } }
        }
    }
}

/**
@@ -247,11 +266,22 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>(
    override val debugName: String,
    private val instanceProvider: PerDisplayInstanceProvider<T>,
) : PerDisplayRepository<T> {
    private val lazyDefaultDisplayInstance by lazy {
    private val lazyDefaultDisplayInstanceDelegate = lazy {
        instanceProvider.createInstance(Display.DEFAULT_DISPLAY)
    }
    private val lazyDefaultDisplayInstance by lazyDefaultDisplayInstanceDelegate

    override fun get(displayId: Int): T? = lazyDefaultDisplayInstance

    override fun forEach(createIfAbsent: Boolean, action: Consumer<T>) {
        if (createIfAbsent) {
            get(DEFAULT_DISPLAY)?.let { action.accept(it) }
        } else {
            if (lazyDefaultDisplayInstanceDelegate.isInitialized()) {
                lazyDefaultDisplayInstance?.let { action.accept(it) }
            }
        }
    }
}

/**
@@ -265,4 +295,8 @@ class DefaultDisplayOnlyInstanceRepositoryImpl<T>(
class SingleInstanceRepositoryImpl<T>(override val debugName: String, private val instance: T) :
    PerDisplayRepository<T> {
    override fun get(displayId: Int): T? = instance

    override fun forEach(createIfAbsent: Boolean, action: Consumer<T>) {
        action.accept(instance)
    }
}
+168 −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.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.snapshots.Snapshot
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.junit.Test
import org.junit.runner.RunWith
import platform.test.motion.compose.runMonotonicClockTest

@RunWith(AndroidJUnit4::class)
class ComposeStateTest {
    @Test
    fun mutableState_sendApplyNotifications() = runMonotonicClockTest {
        val mutableState = mutableStateOf(0f)

        var lastRead = -1f
        snapshotFlow { mutableState.value }.onEach { lastRead = it }.launchIn(backgroundScope)
        check(lastRead == -1f) { "[1] lastRead $lastRead, snapshotFlow launchIn" }

        // snapshotFlow will emit the first value (0f).
        testScheduler.advanceTimeBy(1)
        check(lastRead == 0f) { "[2] lastRead $lastRead, first advanceTimeBy()" }

        // update composeState x5.
        repeat(5) {
            mutableState.value++
            check(lastRead == 0f) { "[3 loop] lastRead $lastRead, composeState.floatValue++" }

            testScheduler.advanceTimeBy(1)
            check(lastRead == 0f) { "[4 loop] lastRead $lastRead, advanceTimeBy()" }
        }

        // Try to wait with a delay. It does nothing (lastRead == 0f).
        delay(1)
        check(mutableState.value == 5f) { "[5] mutableState ${mutableState.value}, after loop" }
        check(lastRead == 0f) { "[5] lastRead $lastRead, after loop" }

        // This should trigger the flow.
        Snapshot.sendApplyNotifications()
        check(lastRead == 0f) { "[6] lastRead $lastRead, Snapshot.sendApplyNotifications()" }

        // lastRead will be updated (5f) after advanceTimeBy (or a delay).
        testScheduler.advanceTimeBy(1)
        check(lastRead == 5f) { "[7] lastRead $lastRead, advanceTimeBy" }
    }

    @Test
    fun derivedState_readNotRequireASendApplyNotifications() = runMonotonicClockTest {
        val mutableState = mutableStateOf(0f)

        var derivedRuns = 0
        val derived = derivedStateOf {
            derivedRuns++
            mutableState.value * 2f
        }
        check(derivedRuns == 0) { "[1] derivedRuns: $derivedRuns, should be 0" }

        var lastRead = -1f
        snapshotFlow { derived.value }.onEach { lastRead = it }.launchIn(backgroundScope)
        check(lastRead == -1f) { "[2] lastRead $lastRead, snapshotFlow launchIn" }
        check(derivedRuns == 0) { "[2] derivedRuns: $derivedRuns, should be 0" }

        // snapshotFlow will emit the first value (0f * 2f = 0f).
        testScheduler.advanceTimeBy(16)
        check(lastRead == 0f) { "[3] lastRead $lastRead, first advanceTimeBy()" }
        check(derivedRuns == 1) { "[3] derivedRuns: $derivedRuns, should be 1" }

        // update composeState x5.
        repeat(5) {
            mutableState.value++
            check(lastRead == 0f) { "[4 loop] lastRead $lastRead, composeState.floatValue++" }

            testScheduler.advanceTimeBy(16)
            check(lastRead == 0f) { "[5 loop] lastRead $lastRead, advanceTimeBy()" }
        }

        // Try to wait with a delay. It does nothing (lastRead == 0f).
        delay(1)
        check(mutableState.value == 5f) { "[6] mutableState ${mutableState.value}, after loop" }
        check(lastRead == 0f) { "[6] lastRead $lastRead, after loop" }
        check(derivedRuns == 1) { "[6] derivedRuns $derivedRuns, after loop" }

        // Reading a derived state, this will trigger the flow.
        // NOTE: We are not using Snapshot.sendApplyNotifications()
        derived.value
        check(lastRead == 0f) { "[7] lastRead $lastRead, read derivedDouble" }
        check(derivedRuns == 2) { "[7] derivedRuns $derivedRuns, read derived" } // Triggered

        // lastRead will be updated (5f * 2f = 10f) after advanceTimeBy (or a delay)
        testScheduler.advanceTimeBy(16)
        check(lastRead == 5f * 2f) { "[8] lastRead $lastRead, advanceTimeBy" } // New value
        check(derivedRuns == 2) { "[8] derivedRuns $derivedRuns, read derived" }
    }

    @Test
    fun derivedState_readADerivedStateTriggerOthersDerivedState() = runMonotonicClockTest {
        val mutableState = mutableStateOf(0f)

        var derivedRuns = 0
        val derived = derivedStateOf {
            derivedRuns++
            mutableState.value
        }

        var otherRuns = 0
        repeat(100) {
            val otherState = derivedStateOf {
                otherRuns++
                mutableState.value
            }
            // Observer all otherStates.
            snapshotFlow { otherState.value }.launchIn(backgroundScope)
        }
        check(derivedRuns == 0) { "[1] derivedRuns: $derivedRuns" }
        check(otherRuns == 0) { "[1] otherRuns: $otherRuns" }

        // Wait for snapshotFlow.
        testScheduler.advanceTimeBy(16)
        check(derivedRuns == 0) { "[2] derivedRuns: $derivedRuns" }
        check(otherRuns == 100) { "[2] otherRuns: $otherRuns" }

        // This write might trigger all otherStates observed, but it does not.
        mutableState.value++
        check(derivedRuns == 0) { "[3] derivedRuns: $derivedRuns" }
        check(otherRuns == 100) { "[3] otherRuns: $otherRuns" }

        // Wait for several frames, but still doesn't trigger otherStates.
        repeat(10) { testScheduler.advanceTimeBy(16) }
        check(derivedRuns == 0) { "[4] derivedRuns: $derivedRuns" }
        check(otherRuns == 100) { "[4] otherRuns: $otherRuns" }

        // Reading derived state will trigger all otherStates.
        // This behavior is causing us some problems, because reading a derived state causes all
        // the
        // dirty derived states to be reread, and this can happen multiple times per frame,
        // making
        // derived states much more expensive than one might expect.
        derived.value
        check(derivedRuns == 1) { "[5] derivedRuns: $derivedRuns" }
        check(otherRuns == 100) { "[5] otherRuns: $otherRuns" }

        // Now we pay the cost of those derived states.
        testScheduler.advanceTimeBy(1)
        check(derivedRuns == 1) { "[6] derivedRuns: $derivedRuns" }
        check(otherRuns == 200) { "[6] otherRuns: $otherRuns" }
    }
}
+166 −23
Original line number Diff line number Diff line
@@ -18,64 +18,207 @@ package com.android.mechanics.benchmark

import androidx.benchmark.junit4.BenchmarkRule
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.test.ext.junit.runners.AndroidJUnit4
import com.android.mechanics.DistanceGestureContext
import com.android.mechanics.MotionValue
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.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
import org.junit.runner.RunWith
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(AndroidJUnit4::class)
class MotionValueBenchmark {
    @get:Rule val benchmarkRule = BenchmarkRule()

    private data class TestData(
        val motionValue: MotionValue,
        val gestureContext: DistanceGestureContext,
        val input: MutableFloatState,
        val spec: MotionSpec,
    )

    private fun testData(
        gestureContext: DistanceGestureContext = DistanceGestureContext(0f, InputDirection.Max, 2f),
        input: Float = 0f,
        spec: MotionSpec = MotionSpec.Empty,
    ): TestData {
        val inputState = mutableFloatStateOf(input)
        return TestData(
            motionValue = MotionValue(inputState::floatValue, gestureContext, spec),
            gestureContext = gestureContext,
            input = inputState,
            spec = spec,
        )
    }

    // Fundamental operations on MotionValue: create, read, update.

    @Test
    fun createMotionValue() {
        val gestureContext = DistanceGestureContext(0f, InputDirection.Max, 2f)
        val currentInput = { 0f }
        benchmarkRule.measureRepeated { MotionValue(currentInput, gestureContext) }
        val input = { 0f }

        benchmarkRule.measureRepeated { MotionValue(input, gestureContext) }
    }

    @Test
    fun changeInput_readOutput() {
        val gestureContext = DistanceGestureContext(0f, InputDirection.Max, 2f)
        val a = mutableFloatStateOf(0f)
        val motionValue = MotionValue(a::floatValue, gestureContext)
    fun stable_readOutput_noChanges() {
        val data = testData()

        // The first read may cost more than the others, it is not interesting for this test.
        data.motionValue.floatValue

        benchmarkRule.measureRepeated { data.motionValue.floatValue }
    }

    @Test
    fun stable_readOutput_afterWriteInput() {
        val data = testData()

        benchmarkRule.measureRepeated {
            runWithMeasurementDisabled { a.floatValue += 1f }
            motionValue.floatValue
            runWithMeasurementDisabled { data.input.floatValue += 1f }
            data.motionValue.floatValue
        }
    }

    @Test
    fun readOutputMultipleTimes() {
        val gestureContext = DistanceGestureContext(0f, InputDirection.Max, 2f)
        val a = mutableFloatStateOf(0f)
        val motionValue = MotionValue(a::floatValue, gestureContext)
    fun stable_writeInput_AND_readOutput() {
        val data = testData()

        benchmarkRule.measureRepeated {
            runWithMeasurementDisabled {
                a.floatValue += 1f
                motionValue.output
            data.input.floatValue += 1f
            data.motionValue.floatValue
        }
            motionValue.output
    }

    // 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 readOutputMultipleTimesMeasureAll() {
        val gestureContext = DistanceGestureContext(0f, InputDirection.Max, 2f)
        val currentInput = mutableFloatStateOf(0f)
        val motionValue = MotionValue(currentInput::floatValue, gestureContext)
    fun writeState_100snapshotFlow() = runMonotonicClockTest {
        val composeState = mutableFloatStateOf(0f)

        repeat(100) { snapshotFlow { composeState.floatValue }.launchIn(backgroundScope) }

        benchmarkRule.measureRepeated {
            composeState.floatValue++
            Snapshot.sendApplyNotifications()
            testScheduler.advanceTimeBy(16)
        }
    }

    // Animations

    private fun MonotonicClockTestScope.keepRunningDuringTest(motionValue: MotionValue) {
        val keepRunningJob = launch { motionValue.keepRunning() }
        doOnTearDown { keepRunningJob.cancel() }
    }

    private val MotionSpec.Companion.ZeroToOne_AtOne
        get() =
            MotionSpec(
                buildDirectionalMotionSpec(
                    defaultSpring = SpringParameters(stiffness = 300f, dampingRatio = .9f),
                    initialMapping = Mapping.Zero,
                ) {
                    constantValue(breakpoint = 1f, value = 1f)
                }
            )

    private val InputDirection.opposite
        get() = if (this == InputDirection.Min) InputDirection.Max else InputDirection.Min

    @Test
    fun unstable_resetGestureContext_readOutput() = runMonotonicClockTest {
        val data = testData(input = 1f, spec = MotionSpec.ZeroToOne_AtOne)
        keepRunningDuringTest(data.motionValue)

        benchmarkRule.measureRepeated {
            currentInput.floatValue += 1f
            motionValue.output
            motionValue.output
            if (data.motionValue.isStable) {
                data.gestureContext.reset(0f, data.gestureContext.direction.opposite)
            }
            data.motionValue.floatValue
            testScheduler.advanceTimeBy(16)
        }
    }

    @Test
    fun unstable_resetGestureContext_snapshotFlowOutput() = runMonotonicClockTest {
        val data = testData(input = 1f, spec = MotionSpec.ZeroToOne_AtOne)
        keepRunningDuringTest(data.motionValue)

        snapshotFlow { data.motionValue.floatValue }.launchIn(backgroundScope)

        benchmarkRule.measureRepeated {
            if (data.motionValue.isStable) {
                data.gestureContext.reset(0f, data.gestureContext.direction.opposite)
            }
            testScheduler.advanceTimeBy(16)
        }
    }

    private val MotionSpec.Companion.ZeroToOne_AtOne_WithGuarantee
        get() =
            MotionSpec(
                buildDirectionalMotionSpec(
                    defaultSpring = SpringParameters(stiffness = 300f, dampingRatio = .9f),
                    initialMapping = Mapping.Zero,
                ) {
                    constantValue(
                        breakpoint = 1f,
                        value = 1f,
                        guarantee = Guarantee.GestureDragDelta(1f),
                    )
                }
            )

    @Test
    fun unstable_resetGestureContext_guarantee_readOutput() = runMonotonicClockTest {
        val data = testData(input = 1f, spec = MotionSpec.ZeroToOne_AtOne_WithGuarantee)
        keepRunningDuringTest(data.motionValue)

        benchmarkRule.measureRepeated {
            if (data.motionValue.isStable) {
                data.gestureContext.reset(0f, data.gestureContext.direction.opposite)
            } else {
                val isMax = data.gestureContext.direction == InputDirection.Max
                data.gestureContext.dragOffset += if (isMax) 0.01f else -0.01f
            }

            data.motionValue.floatValue
            testScheduler.advanceTimeBy(16)
        }
    }
}
+22 −0
Original line number Diff line number Diff line
@@ -16,15 +16,37 @@

package com.android.mechanics

import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalViewConfiguration
import com.android.mechanics.spec.InputDirection
import kotlin.math.max
import kotlin.math.min

/**
 * Remembers [DistanceGestureContext] with the given initial distance / direction.
 *
 * Providing update [initDistance] or [initialDirection] will not re-create the
 * [DistanceGestureContext].
 *
 * The `directionChangeSlop` is derived from `ViewConfiguration.touchSlop` and kept current without
 * re-creating, should it ever change.
 */
@Composable
fun rememberDistanceGestureContext(
    initDistance: Float = 0f,
    initialDirection: InputDirection = InputDirection.Max,
): DistanceGestureContext {
    val touchSlop = LocalViewConfiguration.current.touchSlop
    return remember { DistanceGestureContext(initDistance, initialDirection, touchSlop) }
        .also { it.directionChangeSlop = touchSlop }
}

/**
 * Gesture-specific context to augment [MotionValue.currentInput].
 *
+10 −0
Original line number Diff line number Diff line
@@ -37,6 +37,7 @@ import com.android.mechanics.spec.InputDirection
import com.android.mechanics.spec.Mapping
import com.android.mechanics.spec.MotionSpec
import com.android.mechanics.spec.SegmentData
import com.android.mechanics.spec.SemanticKey
import com.android.mechanics.spring.SpringState
import java.util.concurrent.atomic.AtomicInteger
import kotlinx.coroutines.CoroutineName
@@ -140,6 +141,15 @@ class MotionValue(
    /** Whether an animation is currently running. */
    val isStable: Boolean by impl::isStable

    /**
     * The current value for the [SemanticKey].
     *
     * `null` if not defined in the spec.
     */
    operator fun <T> get(key: SemanticKey<T>): T? {
        return impl.semanticState(key)
    }

    /**
     * Keeps the [MotionValue]'s animated output running.
     *
Loading