Loading mechanics/benchmark/tests/src/com/android/mechanics/benchmark/ComposeStateTest.kt 0 → 100644 +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" } } } mechanics/benchmark/tests/src/com/android/mechanics/benchmark/MotionValueBenchmark.kt +166 −23 Original line number Diff line number Diff line Loading @@ -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) } } } Loading
mechanics/benchmark/tests/src/com/android/mechanics/benchmark/ComposeStateTest.kt 0 → 100644 +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" } } }
mechanics/benchmark/tests/src/com/android/mechanics/benchmark/MotionValueBenchmark.kt +166 −23 Original line number Diff line number Diff line Loading @@ -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) } } }