1 /*
<lambda>null2  * Copyright 2023 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.baselineprofile.gradle.producer
18 
19 import androidx.baselineprofile.gradle.configuration.ConfigurationManager
20 import androidx.baselineprofile.gradle.producer.tasks.CollectBaselineProfileTask
21 import androidx.baselineprofile.gradle.producer.tasks.InstrumentationTestTaskWrapper
22 import androidx.baselineprofile.gradle.utils.AgpFeature.CONFIGURATION_CACHE_FIX_B348136774
23 import androidx.baselineprofile.gradle.utils.AgpFeature.TEST_MODULE_SUPPORTS_MULTIPLE_BUILD_TYPES
24 import androidx.baselineprofile.gradle.utils.AgpFeature.TEST_VARIANT_SUPPORTS_INSTRUMENTATION_RUNNER_ARGUMENTS
25 import androidx.baselineprofile.gradle.utils.AgpFeature.TEST_VARIANT_TESTED_APKS
26 import androidx.baselineprofile.gradle.utils.AgpPlugin
27 import androidx.baselineprofile.gradle.utils.AgpPluginId
28 import androidx.baselineprofile.gradle.utils.AndroidTestModuleWrapper
29 import androidx.baselineprofile.gradle.utils.BUILD_TYPE_BASELINE_PROFILE_PREFIX
30 import androidx.baselineprofile.gradle.utils.BUILD_TYPE_BENCHMARK_PREFIX
31 import androidx.baselineprofile.gradle.utils.CONFIGURATION_ARTIFACT_TYPE
32 import androidx.baselineprofile.gradle.utils.CONFIGURATION_NAME_BASELINE_PROFILES
33 import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_ENABLED_RULES
34 import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_ENABLED_RULES_BASELINE_PROFILE
35 import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_ENABLED_RULES_BENCHMARK
36 import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_SKIP_ON_EMULATOR
37 import androidx.baselineprofile.gradle.utils.INSTRUMENTATION_ARG_TARGET_PACKAGE_NAME
38 import androidx.baselineprofile.gradle.utils.InstrumentationTestRunnerArgumentsAgp82
39 import androidx.baselineprofile.gradle.utils.MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
40 import androidx.baselineprofile.gradle.utils.MIN_AGP_VERSION_REQUIRED_INCLUSIVE
41 import androidx.baselineprofile.gradle.utils.RELEASE
42 import androidx.baselineprofile.gradle.utils.TestedApksAgp83
43 import androidx.baselineprofile.gradle.utils.camelCase
44 import androidx.baselineprofile.gradle.utils.createBuildTypeIfNotExists
45 import androidx.baselineprofile.gradle.utils.createExtendedBuildTypes
46 import com.android.build.api.dsl.TestBuildType
47 import com.android.build.api.dsl.TestExtension
48 import com.android.build.api.variant.TestVariant
49 import com.android.build.api.variant.TestVariantBuilder
50 import com.android.build.api.variant.Variant
51 import org.gradle.api.GradleException
52 import org.gradle.api.Plugin
53 import org.gradle.api.Project
54 
55 /**
56  * This is the producer plugin for baseline profile generation. In order to generate baseline
57  * profiles three plugins are needed: one is applied to the app or the library that should consume
58  * the baseline profile when building (consumer), one is applied to the module that should supply
59  * the under test app (app target) and the last one is applied to a test module containing the ui
60  * test that generate the baseline profile on the device (producer).
61  */
62 class BaselineProfileProducerPlugin : Plugin<Project> {
63     override fun apply(project: Project) = BaselineProfileProducerAgpPlugin(project).onApply()
64 }
65 
66 private class BaselineProfileProducerAgpPlugin(private val project: Project) :
67     AgpPlugin(
68         project = project,
69         supportedAgpPlugins = setOf(AgpPluginId.ID_ANDROID_TEST_PLUGIN),
70         minAgpVersionInclusive = MIN_AGP_VERSION_REQUIRED_INCLUSIVE,
71         maxAgpVersionExclusive = MAX_AGP_VERSION_RECOMMENDED_EXCLUSIVE
72     ) {
73 
74     companion object {
75         private const val PROP_ENABLED_RULES =
76             "android.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules"
77     }
78 
79     private val baselineProfileExtension = BaselineProfileProducerExtension.register(project)
80     private val configurationManager = ConfigurationManager(project)
<lambda>null81     private val shouldSkipGeneration by lazy {
82         project.providers.gradleProperty(PROP_SKIP_GENERATION).isPresent
83     }
<lambda>null84     private val forceOnlyConnectedDevices: Boolean by lazy {
85         project.providers.gradleProperty(PROP_FORCE_ONLY_CONNECTED_DEVICES).isPresent
86     }
<lambda>null87     private val addEnabledRulesInstrumentationArgument by lazy {
88         !project.providers.gradleProperty(PROP_DONT_DISABLE_RULES).isPresent
89     }
<lambda>null90     private val addTargetPackageNameInstrumentationArgument by lazy {
91         !project.providers.gradleProperty(PROP_SEND_TARGET_PACKAGE_NAME).isPresent
92     }
93 
94     // This maps all the extended build types to the original ones. Note that release does not
95     // exist by default so we need to create nonMinifiedRelease and map it manually to `release`.
96     private val nonObfuscatedReleaseName = camelCase(BUILD_TYPE_BASELINE_PROFILE_PREFIX, RELEASE)
97     private val baselineProfileExtendedToOriginalTypeMap =
98         mutableMapOf(nonObfuscatedReleaseName to RELEASE)
99 
100     private val benchmarkReleaseName = camelCase(BUILD_TYPE_BENCHMARK_PREFIX, RELEASE)
101     private val benchmarkExtendedToOriginalTypeMap = mutableMapOf(benchmarkReleaseName to RELEASE)
102 
onAgpPluginFoundnull103     override fun onAgpPluginFound(pluginIds: Set<AgpPluginId>) {
104         project.logger.debug(
105             "[BaselineProfileProducerPlugin] afterEvaluate check: app plugin was applied"
106         )
107     }
108 
onAgpPluginNotFoundnull109     override fun onAgpPluginNotFound(pluginIds: Set<AgpPluginId>) {
110         throw IllegalStateException(
111             """
112     The module ${project.name} does not have the `com.android.test` plugin applied. Currently,
113     the `androidx.baselineprofile.producer` plugin supports only android test modules. In future this
114     plugin will also support library modules (https://issuetracker.google.com/issue?id=259737450).
115     Please review your build.gradle to ensure this plugin is applied to the correct module.
116             """
117                 .trimIndent()
118         )
119     }
120 
onBeforeFinalizeDslnull121     override fun onBeforeFinalizeDsl() {
122 
123         // We need the instrumentation apk to run as a separate process
124         AndroidTestModuleWrapper(project).setSelfInstrumenting(true)
125     }
126 
onTestFinalizeDslnull127     override fun onTestFinalizeDsl(extension: TestExtension) {
128 
129         // Creates the new build types to match the app target. All the existing build types beside
130         // `debug`, that is the default one, are added manually in the configuration so we can
131         // assume they've been added for the purpose of generating baseline profiles. We don't
132         // need to create a nonMinified build type from `debug` since there will be no matching
133         // configuration with the app target module.
134 
135         // The test build types need to be debuggable and have the same singing config key to
136         // be installed. We also disable the test coverage tracking since it's not important
137         // here.
138         val configureBlock: TestBuildType.() -> (Unit) = {
139             isDebuggable = true
140             enableAndroidTestCoverage = false
141             enableUnitTestCoverage = false
142             signingConfig = extension.buildTypes.getByName("debug").signingConfig
143 
144             // TODO: The matching fallback is causing a circular dependency when app target plugin
145             //  is not applied. Normally this is not used, but if an app defines only the consumer
146             //  plugin the `nonMinified` build won't exist. If the provider points to it, the
147             //  matching fallback will kick in and will use the `release` build instead of the
148             //  `nonMinifiedRelease`. When this happens, since  we depend on
149             //  `mergeArtReleaseProfile` to ensure that a profile is copied is the baseline profile
150             //  src sets before the build is complete, it will trigger a circular task dependency:
151             //      collectNonMinifiedReleaseBaselineProfile ->
152             //          connectedNonMinifiedReleaseAndroidTest ->
153             //              packageRelease ->
154             //                  compileReleaseArtProfile ->
155             //                      mergeReleaseArtProfile ->
156             //                          copyReleaseBaselineProfileIntoSrc ->
157             //                              mergeReleaseBaselineProfile ->
158             //                                  collectNonMinifiedReleaseBaselineProfile
159             //  Note that the error is expected but we should handle it gracefully with proper
160             //  explanation. (b/272851616)
161             matchingFallbacks += listOf(RELEASE)
162         }
163 
164         // The variant names are used by the test module to request a specific apk artifact to
165         // the under test app module (using configuration attributes). This is all handled by
166         // the com.android.test plugin, as long as both modules have the same variants.
167         // Unfortunately the test module cannot determine which variants are present in the
168         // under test app module. As a result we need to replicate the same build types and
169         // flavors, so that the same variant names are created.
170         createExtendedBuildTypes(
171             project = project,
172             extensionBuildTypes = extension.buildTypes,
173             newBuildTypePrefix = BUILD_TYPE_BASELINE_PROFILE_PREFIX,
174             extendedBuildTypeToOriginalBuildTypeMapping = baselineProfileExtendedToOriginalTypeMap,
175             newConfigureBlock = { _, ext -> configureBlock(ext) },
176             overrideConfigureBlock = { _, _ ->
177                 // Properties are not overridden if the build type already exists.
178             },
179             filterBlock = {
180                 // All the build types that have been added to the test module should be
181                 // extended. This is because we can't know here which ones are actually
182                 // release in the under test module. We can only exclude debug for sure.
183                 it.name != "debug"
184             }
185         )
186         createBuildTypeIfNotExists(
187             project = project,
188             extensionBuildTypes = extension.buildTypes,
189             buildTypeName = nonObfuscatedReleaseName,
190             configureBlock = configureBlock
191         )
192 
193         // Similarly to baseline profile build types we also create benchmark build types if this
194         // version of AGP has the support for it.
195         if (supportsFeature(TEST_MODULE_SUPPORTS_MULTIPLE_BUILD_TYPES)) {
196             createExtendedBuildTypes(
197                 project = project,
198                 extensionBuildTypes = extension.buildTypes,
199                 newBuildTypePrefix = BUILD_TYPE_BENCHMARK_PREFIX,
200                 extendedBuildTypeToOriginalBuildTypeMapping = benchmarkExtendedToOriginalTypeMap,
201                 newConfigureBlock = { _, ext -> configureBlock(ext) },
202                 overrideConfigureBlock = { _, _ ->
203                     // Properties are not overridden if the build type already exists.
204                 },
205                 filterBlock = {
206                     // Note that at this point we already have created the baseline profile build
207                     // types that we don't want to extend again.
208                     it.name != "debug" && it.name !in baselineProfileExtendedToOriginalTypeMap
209                 }
210             )
211             createBuildTypeIfNotExists(
212                 project = project,
213                 extensionBuildTypes = extension.buildTypes,
214                 buildTypeName = benchmarkReleaseName,
215                 configureBlock = configureBlock
216             )
217         }
218 
219         if (shouldSkipGeneration) {
220             logger.info(
221                 """
222                 Property `$PROP_SKIP_GENERATION` set. Baseline profile generation will be skipped.
223             """
224                     .trimIndent()
225             )
226         }
227     }
228 
onTestBeforeVariantsnull229     override fun onTestBeforeVariants(variantBuilder: TestVariantBuilder) {
230 
231         // Makes sure that only the non obfuscated build type variant selected is enabled
232         val buildType = variantBuilder.buildType
233 
234         val isBaselineProfileBuildType = buildType in baselineProfileExtendedToOriginalTypeMap.keys
235         val isBenchmarkBuildType = buildType in benchmarkExtendedToOriginalTypeMap.keys
236         variantBuilder.enable =
237             variantBuilder.enable && (isBaselineProfileBuildType || isBenchmarkBuildType)
238     }
239 
onTestVariantsnull240     override fun onTestVariants(variant: TestVariant) {
241 
242         // Creates all the configurations, one per variant for the newly created build type.
243         // Note that for this version of the plugin is not possible to rely entirely on the variant
244         // api so the actual creation of the tasks is postponed to be executed when all the
245         // agp tasks have been created, using the old api.
246 
247         // The enabled rules property is passed automatically according to the variant, if it was
248         // not set by the user.
249         val enabledRulesNotSet =
250             !project.gradle.startParameter.projectProperties.any {
251                 it.key!!.contentEquals(PROP_ENABLED_RULES)
252             }
253 
254         // If this is a benchmark variant sets the instrumentation runner argument to run only
255         // tests with MacroBenchmark rules.
256         if (
257             variant.buildType in benchmarkExtendedToOriginalTypeMap.keys &&
258                 supportsFeature(TEST_VARIANT_SUPPORTS_INSTRUMENTATION_RUNNER_ARGUMENTS)
259         ) {
260 
261             InstrumentationTestRunnerArgumentsAgp82.set(
262                 variant = variant,
263                 arguments =
264                     listOf(
265                         INSTRUMENTATION_ARG_SKIP_ON_EMULATOR to
266                             baselineProfileExtension.skipBenchmarksOnEmulator.toString()
267                     )
268             )
269 
270             if (addEnabledRulesInstrumentationArgument && enabledRulesNotSet) {
271                 InstrumentationTestRunnerArgumentsAgp82.set(
272                     variant = variant,
273                     arguments =
274                         listOf(
275                             INSTRUMENTATION_ARG_ENABLED_RULES to
276                                 INSTRUMENTATION_ARG_ENABLED_RULES_BENCHMARK,
277                         )
278                 )
279             }
280         }
281 
282         // If AGP api support it, the application id of the target app is sent to instrumentation
283         // app as an instrumentation runner argument. BaselineProfileRule and MacrobenchmarkRule
284         // can pick that up during the test execution.
285         if (
286             addTargetPackageNameInstrumentationArgument &&
287                 supportsFeature(TEST_VARIANT_TESTED_APKS) &&
288                 supportsFeature(CONFIGURATION_CACHE_FIX_B348136774)
289         ) {
290             InstrumentationTestRunnerArgumentsAgp82.set(
291                 variant = variant,
292                 key = INSTRUMENTATION_ARG_TARGET_PACKAGE_NAME,
293                 value = TestedApksAgp83.getTargetAppApplicationId(variant)
294             )
295         }
296 
297         // If this is a baseline profile variant sets the instrumentation runner argument to run
298         // only tests with BaselineProfileRule, create the consumable configurations to expose
299         // the baseline profile artifacts and the tasks to generate the baseline profile artifacts.
300         // Configuration and tasks are created only for baseline profile variants.
301         if (variant.buildType in baselineProfileExtendedToOriginalTypeMap.keys) {
302 
303             // If this is a benchmark variant sets the instrumentation runner argument to run only
304             // tests with MacroBenchmark rules.
305             if (
306                 addEnabledRulesInstrumentationArgument &&
307                     enabledRulesNotSet &&
308                     supportsFeature(TEST_VARIANT_SUPPORTS_INSTRUMENTATION_RUNNER_ARGUMENTS)
309             ) {
310                 InstrumentationTestRunnerArgumentsAgp82.set(
311                     variant = variant,
312                     arguments =
313                         listOf(
314                             INSTRUMENTATION_ARG_ENABLED_RULES to
315                                 INSTRUMENTATION_ARG_ENABLED_RULES_BASELINE_PROFILE
316                         )
317                 )
318             }
319 
320             // Creates the configuration to handle this variant. Note that in the attributes
321             // to match the configuration we use the original build type without `nonObfuscated`.
322             val configuration =
323                 createConfigurationForVariant(
324                     variant = variant,
325                     originalBuildTypeName =
326                         baselineProfileExtendedToOriginalTypeMap[variant.buildType] ?: "",
327                 )
328 
329             // Prepares a block to execute later that creates the tasks for this variant
330             afterVariants {
331                 createTasksForVariant(
332                     project = project,
333                     variant = variant,
334                     configurationName = configuration.name,
335                     baselineProfileExtension = baselineProfileExtension
336                 )
337             }
338         }
339     }
340 
createConfigurationForVariantnull341     private fun createConfigurationForVariant(variant: Variant, originalBuildTypeName: String) =
342         configurationManager.maybeCreate(
343             nameParts =
344                 listOf(
345                     variant.flavorName ?: "",
346                     originalBuildTypeName,
347                     CONFIGURATION_NAME_BASELINE_PROFILES
348                 ),
349             canBeConsumed = true,
350             canBeResolved = false,
351             buildType = originalBuildTypeName,
352             productFlavors = variant.productFlavors
353         )
354 
355     private fun createTasksForVariant(
356         project: Project,
357         variant: TestVariant,
358         configurationName: String,
359         baselineProfileExtension: BaselineProfileProducerExtension
360     ) {
361 
362         // Prepares the devices list to use to generate the baseline profile.
363         // Note that when running gradle with
364         // `androidx.baselineprofile.forceonlyconnecteddevices=false`
365         // this DSL specification is not respected. This is used by Android Studio to run
366         // baseline profile generation only on the selected devices.
367         val devices = mutableSetOf<String>()
368         if (forceOnlyConnectedDevices) {
369             devices.add("connected")
370         } else {
371             devices.addAll(baselineProfileExtension.managedDevices)
372             if (baselineProfileExtension.useConnectedDevices) devices.add("connected")
373         }
374 
375         // The test task runs the ui tests
376         val testTasks =
377             devices
378                 .map { device ->
379                     val task =
380                         InstrumentationTestTaskWrapper.getByName(
381                             project = project,
382                             device = device,
383                             variantName = variant.name
384                         )
385 
386                     // The task is null if the managed device name does not exist
387                     if (task == null) {
388 
389                         // If gradle is syncing don't throw any exception and simply stop here. This
390                         // plugin will fail at build time instead. This allows not breaking project
391                         // sync in ide.
392                         if (isGradleSyncRunning()) return
393 
394                         throw GradleException(
395                             """
396                 No managed device named `$device` was found. Please check your GMD configuration
397                 and make sure that the `baselineProfile.managedDevices` property contains only
398                 existing gradle managed devices. Example:
399 
400                 android {
401                     testOptions.managedDevices.allDevices {
402                         pixel6Api31(ManagedVirtualDevice) {
403                             device = "Pixel 6"
404                             apiLevel = 31
405                             systemImageSource = "aosp"
406                         }
407                     }
408                 }
409 
410                 baselineProfile {
411                     managedDevices = ["pixel6Api31"]
412                     useConnectedDevices = false
413                 }
414 
415                     """
416                                 .trimIndent()
417                         )
418                     }
419 
420                     task
421                 }
422                 .onEach {
423                     it.setEnableEmulatorDisplay(baselineProfileExtension.enableEmulatorDisplay)
424                     if (shouldSkipGeneration) it.setTaskEnabled(false)
425                 }
426 
427         // The collect task collects the baseline profile files from the ui test results
428         val collectTaskProvider =
429             CollectBaselineProfileTask.registerForVariant(
430                 project = project,
431                 variant = variant,
432                 testTaskDependencies = testTasks,
433                 shouldSkipGeneration = shouldSkipGeneration
434             )
435 
436         // The artifacts are added to the configuration that exposes the generated baseline profile
437         addArtifactToConfiguration(
438             configurationName = configurationName,
439             taskProvider = collectTaskProvider,
440             artifactType = CONFIGURATION_ARTIFACT_TYPE
441         )
442     }
443 }
444