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