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