1 /*
<lambda>null2  * 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 
17 package androidx.benchmark.gradle
18 
19 import com.android.build.api.AndroidPluginVersion
20 import com.android.build.api.variant.AndroidComponentsExtension
21 import com.android.build.api.variant.LibraryAndroidComponentsExtension
22 import com.android.build.gradle.AppExtension
23 import com.android.build.gradle.LibraryExtension
24 import com.android.build.gradle.TestedExtension
25 import org.gradle.api.Plugin
26 import org.gradle.api.Project
27 import org.gradle.api.Task
28 import org.gradle.api.tasks.StopExecutionException
29 import org.gradle.api.tasks.TaskContainer
30 
31 private const val ADDITIONAL_TEST_OUTPUT_KEY = "android.enableAdditionalTestOutput"
32 
33 class BenchmarkPlugin : Plugin<Project> {
34 
35     companion object {
36 
37         private const val PROP_FORCE_AOT_COMPILATION = "androidx.benchmark.forceaotcompilation"
38     }
39 
40     private var foundAndroidPlugin = false
41 
42     override fun apply(project: Project) {
43         // NOTE: Although none of the configuration code depends on a reference to the Android
44         // plugin here, there is some implicit coupling behind the scenes, which ensures that the
45         // required BaseExtension from AGP can be found by registering project configuration as a
46         // PluginManager callback.
47 
48         project.pluginManager.withPlugin("com.android.library") {
49             configureWithAndroidPlugin(project)
50         }
51 
52         // Verify that the configuration from this plugin dependent on AGP was successfully applied.
53         project.afterEvaluate {
54             if (!foundAndroidPlugin) {
55                 throw StopExecutionException(
56                     """
57                         The androidx.benchmark plugin currently supports only android library
58                         modules. Ensure that `com.android.library` is applied in the project
59                         build.gradle file. Note that to run macrobenchmarks, this plugin is not
60                         required.
61                         """
62                         .trimIndent()
63                 )
64             }
65         }
66     }
67 
68     private fun configureWithAndroidPlugin(project: Project) {
69         if (!foundAndroidPlugin) {
70             foundAndroidPlugin = true
71             val extension = project.extensions.getByType(TestedExtension::class.java)
72             val componentsExtension =
73                 project.extensions.getByType(AndroidComponentsExtension::class.java)
74             configureWithAndroidExtension(project, extension, componentsExtension)
75         }
76     }
77 
78     private fun configureWithAndroidExtension(
79         project: Project,
80         extension: TestedExtension,
81         componentsExtension: AndroidComponentsExtension<*, *, *>
82     ) {
83         val defaultConfig = extension.defaultConfig
84         val testBuildType = "release"
85         val testInstrumentationArgs = defaultConfig.testInstrumentationRunnerArguments
86 
87         defaultConfig.testInstrumentationRunner = "androidx.benchmark.junit4.AndroidBenchmarkRunner"
88 
89         extension.buildTypes.configureEach {
90             // Disable overhead from test coverage by default, even if we use a debug variant.
91             it.isTestCoverageEnabled = false
92 
93             // Reduce setup friction by setting signingConfig to debug for buildType benchmarks
94             // will run in.
95             if (it.name == testBuildType) {
96                 it.signingConfig = extension.signingConfigs.getByName("debug")
97             }
98         }
99 
100         extension.testBuildType = testBuildType
101         extension.buildTypes.named(testBuildType).configure { it.isDefault = true }
102 
103         if (
104             !project.providers.gradleProperty("android.injected.invoked.from.ide").isPresent &&
105                 !testInstrumentationArgs.containsKey("androidx.benchmark.output.enable")
106         ) {
107             // NOTE: This argument is checked by ResultWriter to enable CI reports.
108             defaultConfig.testInstrumentationRunnerArguments["androidx.benchmark.output.enable"] =
109                 "true"
110 
111             if (
112                 !project.providers
113                     .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
114                     .getOrElse("false")
115                     .toBoolean()
116             ) {
117                 defaultConfig.testInstrumentationRunnerArguments["no-isolated-storage"] = "1"
118             }
119         }
120 
121         val adbPathProvider = componentsExtension.sdkComponents.adb.map { it.asFile.absolutePath }
122 
123         project.tasks.maybeRegister("lockClocks", LockClocksTask::class.java).configure {
124             it.adbPath.set(adbPathProvider)
125             it.coresArg.set(
126                 project.providers.gradleProperty("androidx.benchmark.lockClocks.cores").orElse("")
127             )
128         }
129 
130         project.tasks.maybeRegister("unlockClocks", UnlockClocksTask::class.java).configure {
131             it.adbPath.set(adbPathProvider)
132         }
133 
134         val extensionVariants =
135             when (extension) {
136                 is AppExtension -> extension.applicationVariants
137                 is LibraryExtension -> extension.libraryVariants
138                 else ->
139                     throw StopExecutionException(
140                         """Missing required Android extension in project ${project.name}, this typically
141                     means you are missing the required com.android.application or
142                     com.android.library plugins or they could not be found. The
143                     androidx.benchmark plugin currently only supports android application or
144                     library modules. Ensure that the required plugin is applied in the project
145                     build.gradle file.
146                 """
147                             .trimIndent()
148                     )
149             }
150 
151         // NOTE: .configureEach here is a Gradle API, which will run the callback passed to it after
152         // the extension variants have been resolved.
153         var applied = false
154         extensionVariants.configureEach {
155             if (!applied) {
156                 applied = true
157 
158                 // Note, this directory is hard-coded in AGP
159                 val outputDir =
160                     project.layout.buildDirectory.dir(
161                         "outputs/connected_android_test_additional_output"
162                     )
163                 if (
164                     !project.providers
165                         .gradleProperty(ADDITIONAL_TEST_OUTPUT_KEY)
166                         .getOrElse("false")
167                         .toBoolean()
168                 ) {
169                     // Only enable pulling benchmark data through this plugin on older versions of
170                     // AGP that do not yet enable this flag.
171                     project.tasks
172                         .register("benchmarkReport", BenchmarkReportTask::class.java)
173                         .configure { reportTask ->
174                             reportTask.benchmarkReportDir.set(outputDir)
175                             reportTask.adbPath.set(adbPathProvider)
176                             reportTask.dependsOn(project.tasks.named("connectedAndroidTest"))
177                         }
178 
179                     project.tasks.named("connectedAndroidTest").configure {
180                         // The task benchmarkReport must be registered by this point, and is
181                         // responsible for pulling report data from all connected devices onto host
182                         // machine through adb.
183                         it.finalizedBy("benchmarkReport")
184                     }
185                 } else {
186                     project.tasks.named("connectedAndroidTest").configure {
187                         it.doLast {
188                             it.logger.info(
189                                 "Benchmark",
190                                 "Benchmark report files generated at " +
191                                     outputDir.get().asFile.absolutePath
192                             )
193                         }
194                     }
195                 }
196 
197                 // Check for legacy runner to provide a more helpful error message as it would
198                 // normally print "No tests found" otherwise.
199                 val legacyRunner = "androidx.benchmark.AndroidBenchmarkRunner"
200                 if (defaultConfig.testInstrumentationRunner == legacyRunner) {
201                     throw StopExecutionException(
202                         """Detected usage of the testInstrumentationRunner,
203                             androidx.benchmark.AndroidBenchmarkRunner, in project ${project.name},
204                             which is no longer valid as it has been moved to
205                             androidx.benchmark.junit4.AndroidBenchmarkRunner."""
206                             .trimIndent()
207                     )
208                 }
209             }
210         }
211 
212         // Enables experimental property `android.experimental.force-aot-compilation` if AGP
213         // version is at least 8.4.0. and `androidx.benchmark.forceaotcompilation` is `true`.
214         // By default this property is `true`.
215         val forceAotCompilation =
216             project.providers
217                 .gradleProperty(PROP_FORCE_AOT_COMPILATION)
218                 .map { it.toBoolean() }
219                 .getOrElse(true)
220         if (forceAotCompilation) {
221             project.extensions.findByType(LibraryAndroidComponentsExtension::class.java)?.let {
222                 if (it.pluginVersion < AndroidPluginVersion(8, 4, 0)) {
223                     return@let
224                 }
225                 it.onVariants { v ->
226                     @Suppress("UnstableApiUsage") // usage of experimentalProperties
227                     v.experimentalProperties.put("android.experimental.force-aot-compilation", true)
228                 }
229             }
230         }
231     }
232 
233     private fun <T : Task> TaskContainer.maybeRegister(taskName: String, type: Class<T>) =
234         try {
235             named(taskName, type)
236         } catch (e: Exception) {
237             register(taskName, type)
238         }
239 }
240