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.tasks
18 
19 import androidx.baselineprofile.gradle.utils.INTERMEDIATES_BASE_FOLDER
20 import androidx.baselineprofile.gradle.utils.TASK_NAME_SUFFIX
21 import androidx.baselineprofile.gradle.utils.camelCase
22 import com.android.build.api.variant.TestVariant
23 import com.android.build.gradle.internal.tasks.BuildAnalyzer
24 import com.android.buildanalyzer.common.TaskCategory
25 import com.google.testing.platform.proto.api.core.TestSuiteResultProto
26 import java.io.File
27 import org.gradle.api.DefaultTask
28 import org.gradle.api.GradleException
29 import org.gradle.api.Project
30 import org.gradle.api.file.ConfigurableFileCollection
31 import org.gradle.api.file.DirectoryProperty
32 import org.gradle.api.provider.MapProperty
33 import org.gradle.api.tasks.Input
34 import org.gradle.api.tasks.InputFiles
35 import org.gradle.api.tasks.OutputDirectory
36 import org.gradle.api.tasks.PathSensitive
37 import org.gradle.api.tasks.PathSensitivity
38 import org.gradle.api.tasks.TaskAction
39 import org.gradle.api.tasks.TaskProvider
40 import org.gradle.work.DisableCachingByDefault
41 
42 /**
43  * Collects the generated baseline profile from the instrumentation results of a previous run of the
44  * ui tests.
45  */
46 @DisableCachingByDefault(because = "Mostly I/O bound task")
47 @BuildAnalyzer(primaryTaskCategory = TaskCategory.OPTIMIZATION)
48 abstract class CollectBaselineProfileTask : DefaultTask() {
49 
50     companion object {
51 
52         private const val COLLECT_TASK_NAME = "collect"
53 
54         private const val PROP_KEY_PREFIX_INSTRUMENTATION_RUNNER_ARG =
55             "android.testInstrumentationRunnerArguments."
56 
57         private const val PROP_KEY_INSTRUMENTATION_RUNNER_ARG_CLASS =
58             "${PROP_KEY_PREFIX_INSTRUMENTATION_RUNNER_ARG}class"
59 
60         private const val GOOGLE_STORAGE_SCHEMA = "gs:"
61 
62         private val PROFILE_NAMES = listOf("-baseline-prof-", "-startup-prof-")
63 
64         private val PROFILE_LABELS =
65             listOf("additionaltestoutput.benchmark.trace", "firebase.toolOutput")
66 
67         internal fun registerForVariant(
68             project: Project,
69             variant: TestVariant,
70             testTaskDependencies: List<InstrumentationTestTaskWrapper>,
71             shouldSkipGeneration: Boolean,
72         ): TaskProvider<CollectBaselineProfileTask> {
73 
74             val flavorName = variant.flavorName
75             val buildType = variant.buildType
76 
77             return project.tasks.register(
78                 camelCase(COLLECT_TASK_NAME, variant.name, TASK_NAME_SUFFIX),
79                 CollectBaselineProfileTask::class.java
80             ) {
81                 var outputDir = project.layout.buildDirectory.dir("$INTERMEDIATES_BASE_FOLDER/")
82                 if (!flavorName.isNullOrBlank()) {
83                     outputDir = outputDir.map { d -> d.dir(flavorName) }
84                 }
85                 if (!buildType.isNullOrBlank()) {
86                     outputDir = outputDir.map { d -> d.dir(buildType) }
87                 }
88 
89                 // Sets the baseline-prof output path.
90                 it.outputDir.set(outputDir)
91 
92                 // Sets the test results inputs
93                 it.testResultDirs.setFrom(testTaskDependencies.map { t -> t.resultsDir })
94 
95                 // Sets the project testInstrumentationRunnerArguments
96                 it.testInstrumentationRunnerArguments.set(
97                     project.providers.gradlePropertiesPrefixedBy(
98                         PROP_KEY_PREFIX_INSTRUMENTATION_RUNNER_ARG
99                     )
100                 )
101 
102                 // Disables the task if requested
103                 if (shouldSkipGeneration) it.enabled = false
104             }
105         }
106     }
107 
108     init {
109         group = "Baseline Profile"
110         description = "Collects a baseline profile previously generated through integration tests."
111     }
112 
113     @get:InputFiles
114     @get:PathSensitive(PathSensitivity.NONE)
115     abstract val testResultDirs: ConfigurableFileCollection
116 
117     @get:Input abstract val testInstrumentationRunnerArguments: MapProperty<String, Any>
118 
119     @get:OutputDirectory abstract val outputDir: DirectoryProperty
120 
121     @TaskAction
122     fun exec() {
123 
124         // Determines if this is a partial result based on whether the property
125         // `android.testInstrumentationRunnerArguments.class` is set
126         val isPartialResult =
127             testInstrumentationRunnerArguments
128                 .get()
129                 .containsKey(PROP_KEY_INSTRUMENTATION_RUNNER_ARG_CLASS)
130 
131         // Prepares list with test results to read. Note that these are the output directories
132         // from the instrumentation task. We're interested only in `test-result.pb`.
133         val testResultProtoFiles = testResultDirs.files.map { File(it, "test-result.pb") }
134 
135         // A test-result.pb file must exist as output of connected and managed device tests.
136         // If it doesn't exist it's because there were no tests to run. If there are no devices,
137         // the test task will simply fail. The following check is to give a meaningful error
138         // message if something like that happens.
139         if (testResultProtoFiles.none { it.exists() }) {
140             throw GradleException(
141                 """
142                 Expected test results were not found. This is most likely because there are no
143                 tests to run. Please check that there are ui tests to execute. You can find more
144                 information at https://d.android.com/studio/test/advanced-test-setup. To create a
145                 baseline profile test instead, please check the documentation at
146                 https://d.android.com/baselineprofiles.
147                 """
148                     .trimIndent()
149             )
150         }
151 
152         val profileFiles =
153             testResultProtoFiles
154                 .asSequence()
155                 .onEach { logger.info("Parsing test-result.pb in `${it.absolutePath}`.") }
156                 .map { TestSuiteResultProto.TestSuiteResult.parseFrom(it.readBytes()) }
157                 .flatMap {
158                     // Artifacts can be per test results (when running locally) or
159                     // global (when running on ftl).
160                     it.testResultList.flatMap { r -> r.outputArtifactList } + it.outputArtifactList
161                 }
162                 .filter {
163                     // The label for this artifact is either `additionaltestoutput.benchmark.trace`
164                     // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:utp/android-test-plugin-host-additional-test-output/src/main/java/com/android/tools/utp/plugins/host/additionaltestoutput/AndroidAdditionalTestOutputPlugin.kt;l=199?q=additionaltestoutput.benchmark.trace
165                     // or "firebase.toolOutput" when using ftl. There could be also artifacts stored
166                     // on google storage when running on ftl, so we need to skip those.
167                     it.label.label in PROFILE_LABELS &&
168                         !it.sourcePath.path.startsWith(GOOGLE_STORAGE_SCHEMA)
169                 }
170                 .map { File(it.sourcePath.path) }
171                 .filter {
172                     // NOTE: If the below logic must be changed, be sure to update
173                     // OutputsTest#sanitizeFilename_baselineProfileGradlePlugin
174                     // as that covers library -> plugin file handoff testing
175                     it.extension == "txt" && PROFILE_NAMES.any { n -> n in it.name }
176                 }
177                 .onEach { logger.info("Found profile file `$it`.") }
178                 .toSet()
179 
180         // If this is not a partial result delete the content of the output dir.
181         if (!isPartialResult) {
182             outputDir.get().asFile.apply {
183                 deleteRecursively()
184                 mkdirs()
185             }
186         }
187 
188         // Saves the merged baseline profile file in the final destination. Existing tests are
189         // overwritten, in case this is a partial result that needs to update an existing profile.
190         profileFiles.forEach { it.copyTo(outputDir.file(it.name).get().asFile, overwrite = true) }
191     }
192 }
193