1 /*
<lambda>null2 * 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 android.view.View
20 import androidx.compose.runtime.Composable
21 import androidx.compose.runtime.getValue
22 import androidx.compose.runtime.mutableStateOf
23 import androidx.compose.runtime.setValue
24 import androidx.compose.testutils.ComposeExecutionControl
25 import androidx.compose.testutils.ComposeTestCase
26 import androidx.compose.testutils.ToggleableTestCase
27 import androidx.compose.testutils.assertNoPendingChanges
28 import androidx.compose.testutils.benchmark.android.AndroidTestCase
29 import androidx.compose.testutils.doFramesUntilNoChangesPending
30 import androidx.compose.testutils.recomposeAssertHadChanges
31 import androidx.compose.testutils.setupContent
32 import androidx.compose.ui.layout.SubcomposeLayout
33 import androidx.compose.ui.layout.SubcomposeLayoutState
34 import androidx.compose.ui.layout.SubcomposeSlotReusePolicy
35 import androidx.compose.ui.unit.IntOffset
36 import kotlin.math.abs
37
38 /**
39 * Measures measure and layout performance of the given test case by toggling measure constraints.
40 */
41 fun ComposeBenchmarkRule.benchmarkLayoutPerf(caseFactory: () -> ComposeTestCase) {
42 runBenchmarkFor(caseFactory) {
43 val measureSpecs = arrayOf(0, 1, 2, 3)
44
45 runOnUiThread {
46 doFramesUntilNoChangesPending()
47
48 val width = measuredWidth
49 val height = measuredHeight
50
51 measureSpecs[0] = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
52 measureSpecs[1] = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
53 measureSpecs[2] = View.MeasureSpec.makeMeasureSpec(width - 10, View.MeasureSpec.EXACTLY)
54 measureSpecs[3] =
55 View.MeasureSpec.makeMeasureSpec(height - 10, View.MeasureSpec.EXACTLY)
56
57 requestLayout()
58 measureWithSpec(measureSpecs[0], measureSpecs[1])
59 layout()
60 }
61
62 var offset = 0
63 measureRepeatedOnUiThread {
64 runWithMeasurementDisabled {
65 // toggle between 0 and 2
66 offset = abs(2 - offset)
67 requestLayout()
68 }
69 measureWithSpec(measureSpecs[offset], measureSpecs[offset + 1])
70 layout()
71 }
72 }
73 }
74
benchmarkLayoutPerfnull75 fun AndroidBenchmarkRule.benchmarkLayoutPerf(caseFactory: () -> AndroidTestCase) {
76 runBenchmarkFor(caseFactory) {
77 val measureSpecs = arrayOf(0, 1, 2, 3)
78
79 runOnUiThread {
80 doFrame()
81
82 val width = measuredWidth
83 val height = measuredHeight
84
85 measureSpecs[0] = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
86 measureSpecs[1] = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
87 measureSpecs[2] = View.MeasureSpec.makeMeasureSpec(width - 10, View.MeasureSpec.EXACTLY)
88 measureSpecs[3] =
89 View.MeasureSpec.makeMeasureSpec(height - 10, View.MeasureSpec.EXACTLY)
90
91 requestLayout()
92 measureWithSpec(measureSpecs[0], measureSpecs[1])
93 layout()
94 }
95
96 var offset = 0
97 measureRepeatedOnUiThread {
98 runWithMeasurementDisabled {
99 // toggle between 0 and 2
100 offset = abs(2 - offset)
101 requestLayout()
102 }
103 measureWithSpec(measureSpecs[offset], measureSpecs[offset + 1])
104 layout()
105 }
106 }
107 }
108
109 /** Measures draw performance of the given test case by invalidating the view hierarchy. */
benchmarkDrawPerfnull110 fun AndroidBenchmarkRule.benchmarkDrawPerf(caseFactory: () -> AndroidTestCase) {
111 runBenchmarkFor(caseFactory) {
112 runOnUiThread { doFrame() }
113
114 measureRepeatedOnUiThread {
115 runWithMeasurementDisabled {
116 invalidateViews()
117 drawPrepare()
118 }
119 draw()
120 runWithMeasurementDisabled { drawFinish() }
121 }
122 }
123 }
124
125 /** Measures draw performance of the given test case by invalidating the view hierarchy. */
ComposeBenchmarkRulenull126 fun ComposeBenchmarkRule.benchmarkDrawPerf(caseFactory: () -> ComposeTestCase) {
127 runBenchmarkFor(caseFactory) {
128 runOnUiThread { doFramesUntilNoChangesPending() }
129
130 measureRepeatedOnUiThread {
131 runWithMeasurementDisabled {
132 invalidateViews()
133 drawPrepare()
134 }
135 draw()
136 runWithMeasurementDisabled { drawFinish() }
137 }
138 }
139 }
140
141 /**
142 * Measures recomposition time of the hierarchy after changing a state.
143 *
144 * @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
145 * after the first recomposition. By default this is true to enforce correctness in the benchmark,
146 * but for components that have animations after being recomposed this can be turned off to
147 * benchmark just the first recomposition without any pending animations.
148 */
toggleStateBenchmarkRecomposenull149 fun <T> ComposeBenchmarkRule.toggleStateBenchmarkRecompose(
150 caseFactory: () -> T,
151 assertOneRecomposition: Boolean = true,
152 requireRecomposition: Boolean = true,
153 ) where T : ComposeTestCase, T : ToggleableTestCase {
154 runBenchmarkFor(caseFactory) {
155 runOnUiThread { doFramesUntilNoChangesPending() }
156 measureRepeatedOnUiThread {
157 runWithMeasurementDisabled { getTestCase().toggleState() }
158 if (requireRecomposition) {
159 recomposeAssertHadChanges()
160 } else {
161 recompose()
162 }
163 if (assertOneRecomposition) {
164 assertNoPendingChanges()
165 }
166 }
167 }
168 }
169
170 /**
171 * Measures measure time of the hierarchy after changing a state.
172 *
173 * @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
174 * after the first recomposition. By default this is true to enforce correctness in the benchmark,
175 * but for components that have animations after being recomposed this can be turned off to
176 * benchmark just the first remeasure without any pending animations.
177 */
toggleStateBenchmarkMeasurenull178 fun <T> ComposeBenchmarkRule.toggleStateBenchmarkMeasure(
179 caseFactory: () -> T,
180 toggleCausesRecompose: Boolean = true,
181 assertOneRecomposition: Boolean = true
182 ) where T : ComposeTestCase, T : ToggleableTestCase {
183 runBenchmarkFor(caseFactory) {
184 runOnUiThread { doFramesUntilNoChangesPending() }
185 measureRepeatedOnUiThread {
186 runWithMeasurementDisabled {
187 getTestCase().toggleState()
188 if (toggleCausesRecompose) {
189 recomposeAssertHadChanges()
190 }
191 requestLayout()
192 if (assertOneRecomposition) {
193 assertNoPendingChanges()
194 }
195 }
196 measure()
197 if (assertOneRecomposition) {
198 assertNoPendingChanges()
199 }
200 }
201 }
202 }
203
204 /**
205 * Measures layout time of the hierarchy after changing a state.
206 *
207 * @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
208 * after the first recomposition. By default this is true to enforce correctness in the benchmark,
209 * but for components that have animations after being recomposed this can be turned off to
210 * benchmark just the first relayout without any pending animations.
211 */
toggleStateBenchmarkLayoutnull212 fun <T> ComposeBenchmarkRule.toggleStateBenchmarkLayout(
213 caseFactory: () -> T,
214 toggleCausesRecompose: Boolean = true,
215 assertOneRecomposition: Boolean = true
216 ) where T : ComposeTestCase, T : ToggleableTestCase {
217 runBenchmarkFor(caseFactory) {
218 runOnUiThread { doFramesUntilNoChangesPending() }
219
220 measureRepeatedOnUiThread {
221 runWithMeasurementDisabled {
222 getTestCase().toggleState()
223 if (toggleCausesRecompose) {
224 recomposeAssertHadChanges()
225 }
226 requestLayout()
227 measure()
228 if (assertOneRecomposition) {
229 assertNoPendingChanges()
230 }
231 }
232 layout()
233 if (assertOneRecomposition) {
234 assertNoPendingChanges()
235 }
236 }
237 }
238 }
239
240 /**
241 * Measures draw time of the hierarchy after changing a state.
242 *
243 * @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
244 * after the first recomposition. By default this is true to enforce correctness in the benchmark,
245 * but for components that have animations after being recomposed this can be turned off to
246 * benchmark just the first redraw without any pending animations.
247 */
toggleStateBenchmarkDrawnull248 fun <T> ComposeBenchmarkRule.toggleStateBenchmarkDraw(
249 caseFactory: () -> T,
250 toggleCausesRecompose: Boolean = true,
251 assertOneRecomposition: Boolean = true
252 ) where T : ComposeTestCase, T : ToggleableTestCase {
253 runBenchmarkFor(caseFactory) {
254 runOnUiThread { doFramesUntilNoChangesPending() }
255
256 measureRepeatedOnUiThread {
257 runWithMeasurementDisabled {
258 getTestCase().toggleState()
259 if (toggleCausesRecompose) {
260 recomposeAssertHadChanges()
261 }
262 if (assertOneRecomposition) {
263 assertNoPendingChanges()
264 }
265 requestLayout()
266 measure()
267 layout()
268 drawPrepare()
269 }
270 draw()
271 runWithMeasurementDisabled { drawFinish() }
272 }
273 }
274 }
275
276 /** Measures measure time of the hierarchy after changing a state. */
toggleStateBenchmarkMeasurenull277 fun <T> AndroidBenchmarkRule.toggleStateBenchmarkMeasure(caseFactory: () -> T) where
278 T : AndroidTestCase,
279 T : ToggleableTestCase {
280 runBenchmarkFor(caseFactory) {
281 runOnUiThread { doFrame() }
282
283 measureRepeatedOnUiThread {
284 runWithMeasurementDisabled { getTestCase().toggleState() }
285 measure()
286 }
287 }
288 }
289
290 /** Measures layout time of the hierarchy after changing a state. */
toggleStateBenchmarkLayoutnull291 fun <T> AndroidBenchmarkRule.toggleStateBenchmarkLayout(caseFactory: () -> T) where
292 T : AndroidTestCase,
293 T : ToggleableTestCase {
294 runBenchmarkFor(caseFactory) {
295 runOnUiThread { doFrame() }
296
297 measureRepeatedOnUiThread {
298 runWithMeasurementDisabled {
299 getTestCase().toggleState()
300 measure()
301 }
302 layout()
303 }
304 }
305 }
306
307 /** Measures draw time of the hierarchy after changing a state. */
toggleStateBenchmarkDrawnull308 fun <T> AndroidBenchmarkRule.toggleStateBenchmarkDraw(caseFactory: () -> T) where
309 T : AndroidTestCase,
310 T : ToggleableTestCase {
311 runBenchmarkFor(caseFactory) {
312 runOnUiThread { doFrame() }
313
314 measureRepeatedOnUiThread {
315 runWithMeasurementDisabled {
316 getTestCase().toggleState()
317 measure()
318 layout()
319 drawPrepare()
320 }
321 draw()
322 runWithMeasurementDisabled { drawFinish() }
323 }
324 }
325 }
326
327 /**
328 * Measures recompose, measure and layout time after changing a state.
329 *
330 * @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
331 * after the first recomposition. By default this is true to enforce correctness in the benchmark,
332 * but for components that have animations after being recomposed this can be turned off to
333 * benchmark just the first recompose, remeasure and relayout without any pending animations.
334 */
toggleStateBenchmarkComposeMeasureLayoutnull335 fun <T> ComposeBenchmarkRule.toggleStateBenchmarkComposeMeasureLayout(
336 caseFactory: () -> T,
337 assertOneRecomposition: Boolean = true,
338 requireRecomposition: Boolean = true
339 ) where T : ComposeTestCase, T : ToggleableTestCase {
340 runBenchmarkFor(caseFactory) {
341 runOnUiThread { doFramesUntilNoChangesPending() }
342 measureRepeatedOnUiThread {
343 getTestCase().toggleState()
344 if (requireRecomposition) {
345 recomposeAssertHadChanges()
346 } else {
347 recompose()
348 }
349 if (assertOneRecomposition) {
350 assertNoPendingChanges()
351 }
352 measure()
353 layout()
354 runWithMeasurementDisabled {
355 drawPrepare()
356 draw()
357 drawFinish()
358 }
359 }
360 }
361 }
362
363 /**
364 * Measures recompose time after changing a state.
365 *
366 * @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
367 * after the first recomposition. By default this is true to enforce correctness in the benchmark,
368 * but for components that have animations after being recomposed this can be turned off to
369 * benchmark just the first recompose, remeasure and relayout without any pending animations.
370 */
toggleStateBenchmarkComposenull371 fun <T> ComposeBenchmarkRule.toggleStateBenchmarkCompose(
372 caseFactory: () -> T,
373 assertOneRecomposition: Boolean = true,
374 requireRecomposition: Boolean = true
375 ) where T : ComposeTestCase, T : ToggleableTestCase {
376 runBenchmarkFor(caseFactory) {
377 runOnUiThread { doFramesUntilNoChangesPending() }
378 measureRepeatedOnUiThread {
379 getTestCase().toggleState()
380 if (requireRecomposition) {
381 recomposeAssertHadChanges()
382 } else {
383 recompose()
384 }
385 if (assertOneRecomposition) {
386 assertNoPendingChanges()
387 }
388
389 runWithMeasurementDisabled {
390 measure()
391 layout()
392 drawPrepare()
393 draw()
394 drawFinish()
395 }
396 }
397 }
398 }
399
400 /**
401 * Measures measure and layout time after changing a state.
402 *
403 * @param assertOneRecomposition whether the benchmark will fail if there are pending recompositions
404 * after the first recomposition. By default this is true to enforce correctness in the benchmark,
405 * but for components that have animations after being recomposed this can be turned off to
406 * benchmark just the first remeasure and relayout without any pending animations.
407 */
toggleStateBenchmarkMeasureLayoutnull408 fun <T> ComposeBenchmarkRule.toggleStateBenchmarkMeasureLayout(
409 caseFactory: () -> T,
410 assertOneRecomposition: Boolean = true
411 ) where T : ComposeTestCase, T : ToggleableTestCase {
412 runBenchmarkFor(caseFactory) {
413 runOnUiThread { doFramesUntilNoChangesPending() }
414
415 measureRepeatedOnUiThread {
416 runWithMeasurementDisabled {
417 getTestCase().toggleState()
418 if (assertOneRecomposition) {
419 assertNoPendingChanges()
420 }
421 }
422 measure()
423 if (assertOneRecomposition) {
424 assertNoPendingChanges()
425 }
426 }
427 }
428 }
429
430 /**
431 * Runs a reuse benchmark for the given [content].
432 *
433 * @param content The Content to be benchmarked.
434 */
ComposeBenchmarkRulenull435 fun ComposeBenchmarkRule.benchmarkReuseFor(content: @Composable () -> Unit) {
436 val testCase = { SubcomposeLayoutReuseTestCase(reusableSlots = 1, content) }
437 runBenchmarkFor(testCase) {
438 runOnUiThread {
439 setupContent()
440 doFramesUntilIdle()
441 }
442
443 measureRepeatedOnUiThread {
444 runWithMeasurementDisabled {
445 assertNoPendingChanges()
446 getTestCase().clearContent()
447 doFramesUntilIdle()
448 assertNoPendingChanges()
449 }
450
451 getTestCase().initContent()
452 doFramesUntilIdle()
453 }
454 }
455 }
456
ComposeExecutionControlnull457 private fun ComposeExecutionControl.doFramesUntilIdle() {
458 do {
459 doFrame()
460 } while (hasPendingChanges() || hasPendingMeasureOrLayout())
461 }
462
463 /**
464 * A [ComposeTestCase] to emulate content reuse.
465 *
466 * @param reusableSlots The max number of slots that will be kept for use. For instance, if
467 * reusableSlots=0 the content will be always disposed.
468 * @param content The composable content that will be benchmarked
469 */
470 class SubcomposeLayoutReuseTestCase(
471 private val reusableSlots: Int = 0,
472 private val content: @Composable () -> Unit
473 ) : ComposeTestCase {
474 private var active by mutableStateOf(true)
475
476 @Composable
Contentnull477 override fun Content() {
478 SubcomposeLayout(SubcomposeLayoutState(SubcomposeSlotReusePolicy(reusableSlots))) {
479 constraints ->
480 val measurables =
481 if (active) {
482 subcompose(Unit) { content() }
483 } else {
484 null
485 }
486
487 val placeable = measurables?.single()?.measure(constraints)
488 layout(placeable?.width ?: 0, placeable?.height ?: 0) {
489 placeable?.place(IntOffset.Zero)
490 }
491 }
492 }
493
clearContentnull494 fun clearContent() {
495 active = false
496 }
497
initContentnull498 fun initContent() {
499 active = true
500 }
501 }
502