blob: c4289380890f5b6fc0ff00376c4de2017ae5386d [file] [log] [blame]
/*
* Copyright 2020 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 androidx.compose.testutils.benchmark
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.testutils.ComposeExecutionControl
import androidx.compose.testutils.ComposeTestCase
import androidx.compose.testutils.ToggleableTestCase
import androidx.compose.testutils.assertNoPendingChanges
import androidx.compose.testutils.benchmark.android.AndroidTestCase
import androidx.compose.testutils.doFramesUntilNoChangesPending
import androidx.compose.testutils.recomposeAssertHadChanges
import androidx.compose.testutils.setupContent
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.layout.SubcomposeLayoutState
import androidx.compose.ui.layout.SubcomposeSlotReusePolicy
import androidx.compose.ui.unit.IntOffset
import kotlin.math.abs
/**
* Measures measure and layout performance of the given test case by toggling measure constraints.
*/
fun ComposeBenchmarkRule.benchmarkLayoutPerf(caseFactory: () -> ComposeTestCase) {
runBenchmarkFor(caseFactory) {
val measureSpecs = arrayOf(0, 1, 2, 3)
runOnUiThread {
doFramesUntilNoChangesPending()
val width = measuredWidth
val height = measuredHeight
measureSpecs[0] = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
measureSpecs[1] = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
measureSpecs[2] = View.MeasureSpec.makeMeasureSpec(width - 10, View.MeasureSpec.EXACTLY)
measureSpecs[3] =
View.MeasureSpec.makeMeasureSpec(height - 10, View.MeasureSpec.EXACTLY)
requestLayout()
measureWithSpec(measureSpecs[0], measureSpecs[1])
layout()
}
var offset = 0
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
// toggle between 0 and 2
offset = abs(2 - offset)
requestLayout()
}
measureWithSpec(measureSpecs[offset], measureSpecs[offset + 1])
layout()
}
}
}
fun AndroidBenchmarkRule.benchmarkLayoutPerf(caseFactory: () -> AndroidTestCase) {
runBenchmarkFor(caseFactory) {
val measureSpecs = arrayOf(0, 1, 2, 3)
runOnUiThread {
doFrame()
val width = measuredWidth
val height = measuredHeight
measureSpecs[0] = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
measureSpecs[1] = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
measureSpecs[2] = View.MeasureSpec.makeMeasureSpec(width - 10, View.MeasureSpec.EXACTLY)
measureSpecs[3] =
View.MeasureSpec.makeMeasureSpec(height - 10, View.MeasureSpec.EXACTLY)
requestLayout()
measureWithSpec(measureSpecs[0], measureSpecs[1])
layout()
}
var offset = 0
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
// toggle between 0 and 2
offset = abs(2 - offset)
requestLayout()
}
measureWithSpec(measureSpecs[offset], measureSpecs[offset + 1])
layout()
}
}
}
/** Measures draw performance of the given test case by invalidating the view hierarchy. */
fun AndroidBenchmarkRule.benchmarkDrawPerf(caseFactory: () -> AndroidTestCase) {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFrame() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
invalidateViews()
drawPrepare()
}
draw()
runWithMeasurementDisabled { drawFinish() }
}
}
}
/** Measures draw performance of the given test case by invalidating the view hierarchy. */
fun ComposeBenchmarkRule.benchmarkDrawPerf(caseFactory: () -> ComposeTestCase) {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFramesUntilNoChangesPending() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
invalidateViews()
drawPrepare()
}
draw()
runWithMeasurementDisabled { drawFinish() }
}
}
}
/**
* Measures recomposition time of the hierarchy after changing a state.
*
* @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
* after the first recomposition. By default this is true to enforce correctness in the benchmark,
* but for components that have animations after being recomposed this can be turned off to
* benchmark just the first recomposition without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkRecompose(
caseFactory: () -> T,
assertOneRecomposition: Boolean = true,
requireRecomposition: Boolean = true,
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFramesUntilNoChangesPending() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled { getTestCase().toggleState() }
if (requireRecomposition) {
recomposeAssertHadChanges()
} else {
recompose()
}
if (assertOneRecomposition) {
assertNoPendingChanges()
}
}
}
}
/**
* Measures measure time of the hierarchy after changing a state.
*
* @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
* after the first recomposition. By default this is true to enforce correctness in the benchmark,
* but for components that have animations after being recomposed this can be turned off to
* benchmark just the first remeasure without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkMeasure(
caseFactory: () -> T,
toggleCausesRecompose: Boolean = true,
assertOneRecomposition: Boolean = true,
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFramesUntilNoChangesPending() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
getTestCase().toggleState()
if (toggleCausesRecompose) {
recomposeAssertHadChanges()
}
requestLayout()
if (assertOneRecomposition) {
assertNoPendingChanges()
}
}
measure()
if (assertOneRecomposition) {
assertNoPendingChanges()
}
}
}
}
/**
* Measures layout time of the hierarchy after changing a state.
*
* @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
* after the first recomposition. By default this is true to enforce correctness in the benchmark,
* but for components that have animations after being recomposed this can be turned off to
* benchmark just the first relayout without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkLayout(
caseFactory: () -> T,
toggleCausesRecompose: Boolean = true,
assertOneRecomposition: Boolean = true,
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFramesUntilNoChangesPending() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
getTestCase().toggleState()
if (toggleCausesRecompose) {
recomposeAssertHadChanges()
}
requestLayout()
measure()
if (assertOneRecomposition) {
assertNoPendingChanges()
}
}
layout()
if (assertOneRecomposition) {
assertNoPendingChanges()
}
}
}
}
/**
* Measures draw time of the hierarchy after changing a state.
*
* @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
* after the first recomposition. By default this is true to enforce correctness in the benchmark,
* but for components that have animations after being recomposed this can be turned off to
* benchmark just the first redraw without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkDraw(
caseFactory: () -> T,
toggleCausesRecompose: Boolean = true,
assertOneRecomposition: Boolean = true,
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFramesUntilNoChangesPending() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
getTestCase().toggleState()
if (toggleCausesRecompose) {
recomposeAssertHadChanges()
}
if (assertOneRecomposition) {
assertNoPendingChanges()
}
requestLayout()
measure()
layout()
drawPrepare()
}
draw()
runWithMeasurementDisabled { drawFinish() }
}
}
}
/** Measures measure time of the hierarchy after changing a state. */
fun <T> AndroidBenchmarkRule.toggleStateBenchmarkMeasure(caseFactory: () -> T)
where T : AndroidTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFrame() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled { getTestCase().toggleState() }
measure()
}
}
}
/** Measures layout time of the hierarchy after changing a state. */
fun <T> AndroidBenchmarkRule.toggleStateBenchmarkLayout(caseFactory: () -> T)
where T : AndroidTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFrame() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
getTestCase().toggleState()
measure()
}
layout()
}
}
}
/** Measures draw time of the hierarchy after changing a state. */
fun <T> AndroidBenchmarkRule.toggleStateBenchmarkDraw(caseFactory: () -> T)
where T : AndroidTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFrame() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
getTestCase().toggleState()
measure()
layout()
drawPrepare()
}
draw()
runWithMeasurementDisabled { drawFinish() }
}
}
}
/**
* Measures recompose, measure and layout time after changing a state.
*
* @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
* after the first recomposition. By default this is true to enforce correctness in the benchmark,
* but for components that have animations after being recomposed this can be turned off to
* benchmark just the first recompose, remeasure and relayout without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
caseFactory: () -> T,
assertOneRecomposition: Boolean = true,
requireRecomposition: Boolean = true,
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFramesUntilNoChangesPending() }
measureRepeatedOnUiThread {
getTestCase().toggleState()
if (requireRecomposition) {
recomposeAssertHadChanges()
} else {
recompose()
}
if (assertOneRecomposition) {
assertNoPendingChanges()
}
measure()
layout()
runWithMeasurementDisabled {
drawPrepare()
draw()
drawFinish()
}
}
}
}
/**
* Measures recompose time after changing a state.
*
* @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
* after the first recomposition. By default this is true to enforce correctness in the benchmark,
* but for components that have animations after being recomposed this can be turned off to
* benchmark just the first recompose, remeasure and relayout without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkCompose(
caseFactory: () -> T,
assertOneRecomposition: Boolean = true,
requireRecomposition: Boolean = true,
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFramesUntilNoChangesPending() }
measureRepeatedOnUiThread {
getTestCase().toggleState()
if (requireRecomposition) {
recomposeAssertHadChanges()
} else {
recompose()
}
if (assertOneRecomposition) {
assertNoPendingChanges()
}
runWithMeasurementDisabled {
measure()
layout()
drawPrepare()
draw()
drawFinish()
}
}
}
}
/**
* Measures measure and layout time after changing a state.
*
* @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
* after the first recomposition. By default this is true to enforce correctness in the benchmark,
* but for components that have animations after being recomposed this can be turned off to
* benchmark just the first remeasure and relayout without any pending animations.
*/
fun <T> ComposeBenchmarkRule.toggleStateBenchmarkMeasureLayout(
caseFactory: () -> T,
assertOneRecomposition: Boolean = true,
) where T : ComposeTestCase, T : ToggleableTestCase {
runBenchmarkFor(caseFactory) {
runOnUiThread { doFramesUntilNoChangesPending() }
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
getTestCase().toggleState()
if (assertOneRecomposition) {
assertNoPendingChanges()
}
}
measure()
if (assertOneRecomposition) {
assertNoPendingChanges()
}
}
}
}
/**
* Runs a reuse benchmark for the given [content].
*
* @param content The Content to be benchmarked.
*/
fun ComposeBenchmarkRule.benchmarkReuseFor(content: @Composable () -> Unit) {
val testCase = { SubcomposeLayoutReuseTestCase(reusableSlots = 1, content) }
runBenchmarkFor(testCase) {
runOnUiThread {
setupContent()
doFramesUntilIdle()
}
measureRepeatedOnUiThread {
runWithMeasurementDisabled {
assertNoPendingChanges()
getTestCase().clearContent()
doFramesUntilIdle()
assertNoPendingChanges()
}
getTestCase().initContent()
doFramesUntilIdle()
}
}
}
private fun ComposeExecutionControl.doFramesUntilIdle() {
do {
doFrame()
} while (hasPendingChanges() || hasPendingMeasureOrLayout())
}
/**
* A [ComposeTestCase] to emulate content reuse.
*
* @param reusableSlots The max number of slots that will be kept for use. For instance, if
* reusableSlots=0 the content will be always disposed.
* @param content The composable content that will be benchmarked
*/
class SubcomposeLayoutReuseTestCase(
private val reusableSlots: Int = 0,
private val content: @Composable () -> Unit,
) : ComposeTestCase {
private var active by mutableStateOf(true)
@Composable
override fun Content() {
SubcomposeLayout(SubcomposeLayoutState(SubcomposeSlotReusePolicy(reusableSlots))) {
constraints ->
val measurables =
if (active) {
subcompose(Unit, content)
} else {
null
}
val placeable = measurables?.single()?.measure(constraints)
layout(placeable?.width ?: 0, placeable?.height ?: 0) {
placeable?.place(IntOffset.Zero)
}
}
}
fun clearContent() {
active = false
}
fun initContent() {
active = true
}
}