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  */
benchmarkToFirstPixelnull35 fun 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  */
benchmarkFirstComposenull64 fun 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  */
ComposeBenchmarkRulenull84 fun 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  */
benchmarkFirstLayoutnull109 fun 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  */
ComposeBenchmarkRulenull135 fun 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. */
benchmarkFirstSetContentnull161 fun 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. */
benchmarkFirstMeasurenull171 fun 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. */
benchmarkFirstLayoutnull187 fun 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. */
benchmarkFirstDrawnull204 fun 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  */
ComposeExecutionControlnull232 fun 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