1 /*
2  * Copyright 2019 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 package androidx.compose.ui.text.benchmark
17 
18 import android.graphics.Canvas
19 import android.util.Log
20 import kotlin.random.Random
21 import org.junit.rules.RuleChain
22 import org.junit.rules.TestRule
23 import org.junit.runner.Description
24 import org.junit.runners.model.Statement
25 
26 /**
27  * Collection of text benchmark utilities. It tries to
28  * - trigger garbage collection
29  * - free text layout caches before each test run.
30  *
31  * It also provides random text generation capabilities.
32  */
33 class TextBenchmarkTestRule(alphabet: Alphabet = Alphabet.Latin) : TestRule {
34     private val textGeneratorTestRule = RandomTextGeneratorTestRule(alphabet)
35 
applynull36     override fun apply(base: Statement, description: Description): Statement {
37         return RuleChain.outerRule(GarbageCollectTestRule())
38             .around(TextLayoutCacheTestRule())
39             .around(textGeneratorTestRule)
40             .apply(base, description)
41     }
42 
generatornull43     fun <T> generator(block: (generator: RandomTextGenerator) -> T): T {
44         return textGeneratorTestRule.generator(block)
45     }
46 
47     // Width and fontSize used for Layout measurement should be the same for all text benchmarks.
48     // It is helpful when we compare the performance of different layers.
49     // Notice that different test cases accept different length units. The unit of width and
50     // fontSize here are dp and sp, which should be converted into needed unit in the test case.
51     val widthDp: Float = 160f
52     val fontSizeSp: Float = 8f
53 
54     // We noticed that benchmark a single composable Text will lead to inaccurate result. To fix
55     // this problem, we benchmark a column of Texts with its length equal to [repeatTimes].
56     val repeatTimes: Int = 10
57 }
58 
59 /**
60  * At the beginning of each test calls Canvas.freeTextLayoutCaches in order to clear the native text
61  * layout cache.
62  */
63 private class TextLayoutCacheTestRule : TestRule {
64     private val TAG = "TextLayoutCacheTestRule"
65 
applynull66     override fun apply(base: Statement, description: Description): Statement =
67         object : Statement() {
68             override fun evaluate() {
69                 tryFreeTextLayoutCache()
70                 base.evaluate()
71             }
72         }
73 
tryFreeTextLayoutCachenull74     fun tryFreeTextLayoutCache() {
75         try {
76             val freeCaches = Canvas::class.java.getDeclaredMethod("freeTextLayoutCaches")
77             freeCaches.isAccessible = true
78             freeCaches.invoke(null)
79         } catch (e: Exception) {
80             Log.w(TAG, "Cannot fre text layout cache", e)
81             // ignore
82         }
83     }
84 }
85 
86 /**
87  * At the beginning of each test calls Runtime.getRuntime().gc() in order to free memory and
88  * possibly prevent GC during measurement.
89  */
90 private class GarbageCollectTestRule : TestRule {
applynull91     override fun apply(base: Statement, description: Description): Statement =
92         object : Statement() {
93             override fun evaluate() {
94                 Runtime.getRuntime().gc()
95                 base.evaluate()
96             }
97         }
98 }
99 
100 /**
101  * Test rule that initiates a [RandomTextGenerator] using a different seed based on the class and
102  * function name. This way each function will have a different text generated, but at each run the
103  * same function will get the same text.
104  *
105  * This will ensure that the execution order of a test class or functions in a test class does not
106  * affect others because of the native text layout cache.
107  */
108 private class RandomTextGeneratorTestRule(private val alphabet: Alphabet = Alphabet.Latin) :
109     TestRule {
110     private lateinit var textGenerator: RandomTextGenerator
111 
applynull112     override fun apply(base: Statement, description: Description): Statement =
113         object : Statement() {
114             override fun evaluate() {
115                 // gives the full class and function name including the parameters
116                 val fullName = "${description.className}#${description.methodName}"
117 
118                 textGenerator = RandomTextGenerator(alphabet, Random(fullName.hashCode()))
119 
120                 base.evaluate()
121             }
122         }
123 
generatornull124     fun <T> generator(block: (generator: RandomTextGenerator) -> T): T {
125         return block(textGenerator)
126     }
127 }
128