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