1 /* 2 * Copyright 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.compose.testutils.benchmark 18 19 import androidx.compose.runtime.Composable 20 import androidx.compose.runtime.getValue 21 import androidx.compose.runtime.mutableStateOf 22 import androidx.compose.runtime.setValue 23 import androidx.compose.testutils.ComposeExecutionControl 24 import androidx.compose.testutils.ComposeTestCase 25 import androidx.compose.testutils.LayeredComposeTestCase 26 import androidx.compose.testutils.assertNoPendingChanges 27 import androidx.compose.testutils.benchmark.android.AndroidTestCase 28 import androidx.compose.testutils.doFramesUntilNoChangesPending 29 import org.junit.Assert.assertTrue 30 31 /** 32 * Measures the time to draw the first pixel right after the given test case is added to an already 33 * existing hierarchy. This benchmarks the full compose -> measure -> layout -> draw cycle. 34 */ benchmarkToFirstPixelnull35fun ComposeBenchmarkRule.benchmarkToFirstPixel(caseFactory: () -> LayeredComposeTestCase) { 36 runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) { 37 measureRepeatedOnUiThread { 38 runWithMeasurementDisabled { 39 doFramesUntilNoChangesPending() 40 // Add the content to benchmark 41 getTestCase().addMeasuredContent() 42 } 43 44 recomposeUntilNoChangesPending() 45 requestLayout() 46 measure() 47 layout() 48 drawPrepare() 49 draw() 50 51 runWithMeasurementDisabled { 52 drawFinish() 53 assertNoPendingChanges() 54 disposeContent() 55 } 56 } 57 } 58 } 59 60 /** 61 * Measures the time of the first composition right after the given test case is added to an already 62 * existing hierarchy. 63 */ benchmarkFirstComposenull64fun ComposeBenchmarkRule.benchmarkFirstCompose(caseFactory: () -> LayeredComposeTestCase) { 65 runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) { 66 measureRepeatedOnUiThread { 67 runWithMeasurementDisabled { 68 doFramesUntilNoChangesPending() 69 // Add the content to benchmark 70 getTestCase().addMeasuredContent() 71 } 72 73 recomposeUntilNoChangesPending() 74 75 runWithMeasurementDisabled { disposeContent() } 76 } 77 } 78 } 79 80 /** 81 * Measures the time of the first measure right after the given test case is added to an already 82 * existing hierarchy. 83 */ ComposeBenchmarkRulenull84fun ComposeBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> LayeredComposeTestCase) { 85 runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) { 86 measureRepeatedOnUiThread { 87 runWithMeasurementDisabled { 88 doFramesUntilNoChangesPending() 89 // Add the content to benchmark 90 getTestCase().addMeasuredContent() 91 recomposeUntilNoChangesPending() 92 requestLayout() 93 } 94 95 measure() 96 97 runWithMeasurementDisabled { 98 assertNoPendingChanges() 99 disposeContent() 100 } 101 } 102 } 103 } 104 105 /** 106 * Measures the time of the first layout right after the given test case is added to an already 107 * existing hierarchy. 108 */ benchmarkFirstLayoutnull109fun ComposeBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> LayeredComposeTestCase) { 110 runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) { 111 measureRepeatedOnUiThread { 112 runWithMeasurementDisabled { 113 doFramesUntilNoChangesPending() 114 // Add the content to benchmark 115 getTestCase().addMeasuredContent() 116 recomposeUntilNoChangesPending() 117 requestLayout() 118 measure() 119 } 120 121 layout() 122 123 runWithMeasurementDisabled { 124 assertNoPendingChanges() 125 disposeContent() 126 } 127 } 128 } 129 } 130 131 /** 132 * Measures the time of the first draw right after the given test case is added to an already 133 * existing hierarchy. 134 */ ComposeBenchmarkRulenull135fun ComposeBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> LayeredComposeTestCase) { 136 runBenchmarkFor(LayeredCaseAdapter.of(caseFactory)) { 137 measureRepeatedOnUiThread { 138 runWithMeasurementDisabled { 139 doFramesUntilNoChangesPending() 140 // Add the content to benchmark 141 getTestCase().addMeasuredContent() 142 recomposeUntilNoChangesPending() 143 requestLayout() 144 measure() 145 layout() 146 drawPrepare() 147 } 148 149 draw() 150 151 runWithMeasurementDisabled { 152 drawFinish() 153 assertNoPendingChanges() 154 disposeContent() 155 } 156 } 157 } 158 } 159 160 /** Measures the time of the first set content of the given Android test case. */ benchmarkFirstSetContentnull161fun AndroidBenchmarkRule.benchmarkFirstSetContent(caseFactory: () -> AndroidTestCase) { 162 runBenchmarkFor(caseFactory) { 163 measureRepeatedOnUiThread { 164 setupContent() 165 runWithMeasurementDisabled { disposeContent() } 166 } 167 } 168 } 169 170 /** Measures the time of the first measure of the given test case. */ benchmarkFirstMeasurenull171fun AndroidBenchmarkRule.benchmarkFirstMeasure(caseFactory: () -> AndroidTestCase) { 172 runBenchmarkFor(caseFactory) { 173 measureRepeatedOnUiThread { 174 runWithMeasurementDisabled { 175 setupContent() 176 requestLayout() 177 } 178 179 measure() 180 181 runWithMeasurementDisabled { disposeContent() } 182 } 183 } 184 } 185 186 /** Measures the time of the first layout of the given test case. */ benchmarkFirstLayoutnull187fun AndroidBenchmarkRule.benchmarkFirstLayout(caseFactory: () -> AndroidTestCase) { 188 runBenchmarkFor(caseFactory) { 189 measureRepeatedOnUiThread { 190 runWithMeasurementDisabled { 191 setupContent() 192 requestLayout() 193 measure() 194 } 195 196 layout() 197 198 runWithMeasurementDisabled { disposeContent() } 199 } 200 } 201 } 202 203 /** Measures the time of the first draw of the given test case. */ benchmarkFirstDrawnull204fun AndroidBenchmarkRule.benchmarkFirstDraw(caseFactory: () -> AndroidTestCase) { 205 runBenchmarkFor(caseFactory) { 206 measureRepeatedOnUiThread { 207 runWithMeasurementDisabled { 208 setupContent() 209 requestLayout() 210 measure() 211 layout() 212 drawPrepare() 213 } 214 215 draw() 216 217 runWithMeasurementDisabled { 218 drawFinish() 219 disposeContent() 220 } 221 } 222 } 223 } 224 225 /** 226 * Runs recompositions until there are no changes pending. 227 * 228 * @param maxAmountOfStep Max amount of recomposition to perform before giving up and throwing 229 * exception. 230 * @throws AssertionError if there are still pending changes after [maxAmountOfStep] executed. 231 */ ComposeExecutionControlnull232fun ComposeExecutionControl.recomposeUntilNoChangesPending(maxAmountOfStep: Int = 10): Int { 233 var stepsDone = 0 234 while (stepsDone < maxAmountOfStep) { 235 recompose() 236 stepsDone++ 237 if (!hasPendingChanges()) { 238 // We are stable! 239 return stepsDone 240 } 241 } 242 243 // Still not stable 244 throw AssertionError("Changes are still pending after '$maxAmountOfStep' " + "frames.") 245 } 246 247 private class LayeredCaseAdapter(private val innerCase: LayeredComposeTestCase) : ComposeTestCase { 248 249 companion object { <lambda>null250 fun of(caseFactory: () -> LayeredComposeTestCase): () -> LayeredCaseAdapter = { 251 LayeredCaseAdapter(caseFactory()) 252 } 253 } 254 255 var isComposed by mutableStateOf(false) 256 257 @Composable Contentnull258 override fun Content() { 259 innerCase.ContentWrappers { 260 if (isComposed) { 261 innerCase.MeasuredContent() 262 } 263 } 264 } 265 addMeasuredContentnull266 fun addMeasuredContent() { 267 assertTrue(!isComposed) 268 isComposed = true 269 } 270 } 271