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.consumer.task 18 19 import androidx.baselineprofile.gradle.consumer.RuleType 20 import androidx.baselineprofile.gradle.utils.BaselineProfilePluginLogger 21 import androidx.baselineprofile.gradle.utils.TASK_NAME_SUFFIX 22 import androidx.baselineprofile.gradle.utils.Warnings 23 import androidx.baselineprofile.gradle.utils.maybeRegister 24 import com.android.build.gradle.internal.tasks.BuildAnalyzer 25 import com.android.buildanalyzer.common.TaskCategory 26 import java.io.File 27 import kotlin.io.path.Path 28 import org.gradle.api.DefaultTask 29 import org.gradle.api.GradleException 30 import org.gradle.api.Project 31 import org.gradle.api.file.ConfigurableFileCollection 32 import org.gradle.api.file.Directory 33 import org.gradle.api.file.DirectoryProperty 34 import org.gradle.api.file.FileCollection 35 import org.gradle.api.provider.ListProperty 36 import org.gradle.api.provider.Property 37 import org.gradle.api.provider.Provider 38 import org.gradle.api.tasks.CacheableTask 39 import org.gradle.api.tasks.Input 40 import org.gradle.api.tasks.InputFiles 41 import org.gradle.api.tasks.Optional 42 import org.gradle.api.tasks.OutputDirectory 43 import org.gradle.api.tasks.PathSensitive 44 import org.gradle.api.tasks.PathSensitivity 45 import org.gradle.api.tasks.TaskAction 46 import org.gradle.api.tasks.TaskProvider 47 48 /** 49 * Collects all the baseline profile artifacts generated by all the producer configurations and 50 * merges them into one, sorting and ensuring that there are no duplicated lines. 51 * 52 * The format of the profile is a simple list of classes and methods loaded in memory when executing 53 * a test, expressed in JVM format. Duplicates can arise when multiple tests cover the same code: 54 * for example when having 2 tests both covering the startup path and then doing something else, 55 * both will have startup classes and methods. There is no harm in having this duplication but 56 * mostly the profile file will be unnecessarily larger. 57 */ 58 @CacheableTask 59 @BuildAnalyzer(primaryTaskCategory = TaskCategory.OPTIMIZATION) 60 abstract class MergeBaselineProfileTask : DefaultTask() { 61 62 companion object { 63 64 private const val MERGE_TASK_NAME = "merge" 65 private const val COPY_TASK_NAME = "copy" 66 67 // Filename parts to differentiate how to use the profile rules 68 private const val FILENAME_MATCHER_BASELINE_PROFILE = "baseline-prof" 69 private const val FILENAME_MATCHER_STARTUP_PROFILE = "startup-prof" 70 71 // The output file for the HRF baseline profile file in `src/main` 72 private const val BASELINE_PROFILE_FILENAME = "baseline-prof.txt" 73 private const val STARTUP_PROFILE_FILENAME = "startup-prof.txt" 74 75 internal fun maybeRegisterForMerge( 76 project: Project, 77 variantName: String, 78 mergeAwareTaskName: String, 79 hasDependencies: Boolean, 80 library: Boolean, 81 sourceProfilesFileCollection: FileCollection, 82 outputDir: Provider<Directory>, 83 filterRules: List<Pair<RuleType, String>> = listOf(), 84 isLastTask: Boolean, 85 warnings: Warnings 86 ): TaskProvider<MergeBaselineProfileTask> { 87 return project.tasks.maybeRegister( 88 MERGE_TASK_NAME, 89 mergeAwareTaskName, 90 TASK_NAME_SUFFIX 91 ) { task -> 92 93 // Sets whether or not baseline profile dependencies have been set. 94 // If they haven't, the task will fail at execution time. 95 task.hasDependencies.set(hasDependencies) 96 97 // Sets the name of this variant to print it in error messages. 98 task.variantName.set(variantName) 99 100 // These are all the configurations this task depends on, 101 // in order to consume their artifacts. Note that if this task already 102 // exist (for example if `merge` is `all`) the new artifact will be 103 // added to the existing list. 104 task.baselineProfileFileCollection.from.add(sourceProfilesFileCollection) 105 106 // This is the task output for the generated baseline profile. Output 107 // is always stored in the intermediates 108 task.baselineProfileDir.set(outputDir) 109 110 // Sets the package filter rules. Note that if this task already exists 111 // because of a mergeIntoMain rule, rules are added to the existing ones. 112 task.filterRules.addAll(filterRules) 113 114 // Sets whether this task has been configured for a library. In this case, 115 // startup profiles are not handled. 116 task.library.set(library) 117 118 // Determines whether this is the last task to be executed. This flag is used 119 // exclusively for logging purposes. 120 task.lastTask.set(isLastTask) 121 122 // Determines whether this task should print warnings. Note that warnings used 123 // by Android Studio cannot be suppressed. 124 task.printWarningNoBaselineProfileRulesGenerated.set( 125 warnings.noBaselineProfileRulesGenerated 126 ) 127 task.printWarningNoStartupProfileRulesGenerated.set( 128 warnings.noStartupProfileRulesGenerated 129 ) 130 task.printWarningVariantHasNoBaselineProfileDependency.set( 131 warnings.variantHasNoBaselineProfileDependency 132 ) 133 } 134 } 135 136 internal fun maybeRegisterForCopy( 137 project: Project, 138 variantName: String, 139 mergeAwareTaskName: String, 140 library: Boolean, 141 sourceDir: Provider<Directory>, 142 outputDir: Provider<Directory>, 143 isLastTask: Boolean, 144 hasDependencies: Boolean, 145 warnings: Warnings 146 ): TaskProvider<MergeBaselineProfileTask> { 147 return project.tasks.maybeRegister( 148 COPY_TASK_NAME, 149 mergeAwareTaskName, 150 "baselineProfileIntoSrc" 151 ) { task -> 152 // For explanation about each of these properties, see above function named 153 // `maybeRegisterForMerge`. 154 task.baselineProfileFileCollection.from.add(sourceDir) 155 task.baselineProfileDir.set(outputDir) 156 task.library.set(library) 157 task.variantName.set(variantName) 158 task.lastTask.set(isLastTask) 159 task.hasDependencies.set(hasDependencies) 160 task.printWarningNoBaselineProfileRulesGenerated.set( 161 warnings.noBaselineProfileRulesGenerated 162 ) 163 task.printWarningNoStartupProfileRulesGenerated.set( 164 warnings.noStartupProfileRulesGenerated 165 ) 166 task.printWarningVariantHasNoBaselineProfileDependency.set( 167 warnings.variantHasNoBaselineProfileDependency 168 ) 169 } 170 } 171 } 172 173 @get:Input abstract val variantName: Property<String> 174 175 @get:Input abstract val lastTask: Property<Boolean> 176 177 @get:Input @get:Optional abstract val hasDependencies: Property<Boolean> 178 179 @get:Input abstract val library: Property<Boolean> 180 181 @get:InputFiles 182 @get:PathSensitive(PathSensitivity.NONE) 183 abstract val baselineProfileFileCollection: ConfigurableFileCollection 184 185 @get:Input abstract val filterRules: ListProperty<Pair<RuleType, String>> 186 187 @get:OutputDirectory abstract val baselineProfileDir: DirectoryProperty 188 189 @get:Input abstract val printWarningNoBaselineProfileRulesGenerated: Property<Boolean> 190 191 @get:Input abstract val printWarningNoStartupProfileRulesGenerated: Property<Boolean> 192 193 @get:Input abstract val printWarningVariantHasNoBaselineProfileDependency: Property<Boolean> 194 195 private val logger by lazy { BaselineProfilePluginLogger(this.getLogger()) } 196 197 private val variantHasDependencies by lazy { 198 hasDependencies.isPresent && hasDependencies.get() 199 } 200 201 @TaskAction 202 fun exec() { 203 204 // This warning should be printed only if no dependency has been set for the processed 205 // variant. 206 if (lastTask.get() && !variantHasDependencies) { 207 logger.warn( 208 property = { printWarningVariantHasNoBaselineProfileDependency.get() }, 209 propertyName = "variantHasNoBaselineProfileDependency", 210 message = 211 """ 212 The baseline profile consumer plugin is applied to this module but no dependency 213 has been set for variant `${variantName.get()}`, so no baseline profile will be 214 generated for it. 215 216 A dependency for all the variants can be added in the dependency block using 217 `baselineProfile` configuration: 218 219 dependencies { 220 ... 221 baselineProfile(project(":baselineprofile")) 222 } 223 224 Or for a specific variant in the baseline profile block: 225 226 baselineProfile { 227 variants { 228 freeRelease { 229 from(project(":baselineprofile")) 230 } 231 } 232 } 233 """ 234 .trimIndent() 235 ) 236 } 237 238 // Rules are sorted for package depth and excludes are always evaluated first. 239 val rules = 240 filterRules 241 .get() 242 .sortedWith( 243 compareBy<Pair<RuleType, String>> { r -> r.second.split(".").size } 244 .thenComparing { r -> if (r.first == RuleType.INCLUDE) 0 else 1 } 245 .reversed() 246 ) 247 248 // Read the profile rules from the file collection that contains the profile artifacts from 249 // all the configurations for this variant and merge them in a single list. 250 val baselineProfileRules = 251 baselineProfileFileCollection.files.readLines { 252 FILENAME_MATCHER_BASELINE_PROFILE in it.name || 253 FILENAME_MATCHER_STARTUP_PROFILE in it.name 254 } 255 256 // This warning should be printed only if the variant has dependencies but there are no 257 // baseline profile rules. 258 if (lastTask.get() && variantHasDependencies && baselineProfileRules.isEmpty()) { 259 logger.warn( 260 property = { printWarningNoBaselineProfileRulesGenerated.get() }, 261 propertyName = "noBaselineProfileRulesGenerated", 262 message = 263 """ 264 No baseline profile rules were generated for the variant `${variantName.get()}`. 265 This is most likely because there are no instrumentation test for it. If this 266 is not intentional check that tests for this variant exist in the `baselineProfile` 267 dependency module. 268 """ 269 .trimIndent() 270 ) 271 } 272 273 // The profile rules here are: 274 // - sorted (since we group by class later, we want the input to the group by operation not 275 // to be influenced by reading order) 276 // - group by class and method (ignoring flag) and for each group keep only the first value 277 // - apply the filters 278 // - sort with comparator 279 val filteredBaselineProfileRules = 280 baselineProfileRules 281 .sorted() 282 .asSequence() 283 .mapNotNull { ProfileRule.parse(it) } 284 .groupBy { it.classDescriptor + it.methodDescriptor } 285 .map { it.value[0] } 286 .filter { 287 288 // If no rules are specified, always include this line. 289 if (rules.isEmpty()) return@filter true 290 291 // Otherwise rules are evaluated in the order they've been sorted previously. 292 for (r in rules) { 293 if (r.matches(it.fullClassName)) { 294 return@filter r.isInclude() 295 } 296 } 297 298 // If the rules were all excludes and nothing matched, we can include this line 299 // otherwise exclude it. 300 return@filter !rules.any { r -> r.isInclude() } 301 } 302 .sortedWith(ProfileRule.comparator) 303 304 // Check if the filters filtered out all the rules. 305 if ( 306 baselineProfileRules.isNotEmpty() && 307 filteredBaselineProfileRules.isEmpty() && 308 rules.isNotEmpty() 309 ) { 310 throw GradleException( 311 """ 312 The baseline profile consumer plugin is configured with filters that exclude all 313 the profile rules. Please review your build.gradle configuration and make sure your 314 filters don't exclude all the baseline profile rules. 315 """ 316 .trimIndent() 317 ) 318 } 319 320 writeProfile( 321 filename = BASELINE_PROFILE_FILENAME, 322 rules = filteredBaselineProfileRules, 323 profileType = "baseline" 324 ) 325 326 // If this is a library we can stop here and don't manage the startup profiles. 327 if (library.get()) { 328 return 329 } 330 331 // Same process with startup profiles. 332 val startupRules = 333 baselineProfileFileCollection.files.readLines { 334 FILENAME_MATCHER_STARTUP_PROFILE in it.name 335 } 336 337 // This warning should be printed only if the variant has dependencies but there are no 338 // startup profile rules. 339 if (lastTask.get() && variantHasDependencies && startupRules.isEmpty()) { 340 logger.warn( 341 property = { printWarningNoStartupProfileRulesGenerated.get() }, 342 propertyName = "noBaselineProfileRulesGenerated", 343 message = 344 """ 345 No startup profile rules were generated for the variant `${variantName.get()}`. 346 This is most likely because there are no instrumentation test with baseline profile 347 rule, which specify `includeInStartupProfile = true`. If this is not intentional 348 check that tests for this variant exist in the `baselineProfile` dependency module. 349 """ 350 .trimIndent() 351 ) 352 } 353 354 // Use same sorting without filter for startup profiles. 355 val sortedStartupProfileRules = 356 startupRules 357 .asSequence() 358 .sorted() 359 .mapNotNull { ProfileRule.parse(it) } 360 .groupBy { it.classDescriptor + it.methodDescriptor } 361 .map { it.value[0] } 362 .sortedWith(ProfileRule.comparator) 363 364 writeProfile( 365 filename = STARTUP_PROFILE_FILENAME, 366 profileType = "startup", 367 rules = sortedStartupProfileRules, 368 ) 369 } 370 371 private fun writeProfile(filename: String, rules: List<ProfileRule>, profileType: String) { 372 baselineProfileDir.file(filename).get().asFile.apply { 373 374 // If an old profile file already exists calculate stats. 375 val stats = 376 if (exists()) 377 ProfileStats.from( 378 existingRules = readLines().mapNotNull { ProfileRule.parse(it) }, 379 newRules = rules 380 ) 381 else null 382 383 delete() 384 if (rules.isEmpty()) return 385 writeText(rules.joinToString(System.lineSeparator()) { it.underlying }) 386 387 // If this is the last task display a success message (depending on the flag 388 // `saveInSrc` this task may be configured as a merge or copy task). 389 if (!lastTask.get()) { 390 return 391 } 392 393 // This log should not be suppressed because it's used by Android Studio to 394 // open the generated HRF file. 395 logger.warn( 396 property = { true }, 397 propertyName = null, 398 message = 399 """ 400 401 A $profileType profile was generated for the variant `${variantName.get()}`: 402 ${Path(absolutePath).toUri()} 403 """ 404 .trimIndent() 405 ) 406 407 // Print stats if was previously calculated 408 stats?.apply { 409 logger.warn( 410 property = { true }, 411 propertyName = null, 412 message = 413 """ 414 415 Comparison with previous $profileType profile: 416 $existing Old rules 417 $new New rules 418 $added Added rules (${"%.2f".format(addedRatio * 100)}%) 419 $removed Removed rules (${"%.2f".format(removedRatio * 100)}%) 420 $unmodified Unmodified rules (${"%.2f".format(unmodifiedRatio * 100)}%) 421 422 """ 423 .trimIndent() 424 ) 425 } 426 } 427 } 428 429 private fun Pair<RuleType, String>.isInclude(): Boolean = first == RuleType.INCLUDE 430 431 private fun Pair<RuleType, String>.matches(fullClassName: String): Boolean { 432 val rule = second 433 return when { 434 rule.endsWith(".**") -> { 435 // This matches package and subpackages 436 val pkg = fullClassName.split(".").dropLast(1).joinToString(".") 437 val rulePkg = rule.dropLast(3) 438 pkg.startsWith(rulePkg) 439 } 440 rule.endsWith(".*") -> { 441 // This matches only the package 442 val pkgParts = fullClassName.split(".").dropLast(1) 443 val pkg = pkgParts.joinToString(".") 444 val rulePkg = rule.dropLast(2) 445 val ruleParts = rulePkg.split(".") 446 pkg.startsWith(rulePkg) && ruleParts.size == pkgParts.size 447 } 448 else -> { 449 // This matches only the specific class name 450 fullClassName == rule 451 } 452 } 453 } 454 455 private fun Iterable<File>.readLines(filterBlock: (File) -> (Boolean)): List<String> = 456 this.flatMap { 457 if (it.isFile) { 458 listOf(it) 459 } else { 460 listOf(*(it.listFiles() ?: arrayOf())) 461 } 462 } 463 .filter(filterBlock) 464 .flatMap { it.readLines() } 465 } 466 467 internal data class ProfileStats( 468 val existing: Int, 469 val new: Int, 470 val added: Int, 471 val removed: Int, 472 val unmodified: Int, 473 val addedRatio: Float, 474 val removedRatio: Float, 475 val unmodifiedRatio: Float, 476 ) { 477 companion object { fromnull478 fun from(existingRules: List<ProfileRule>, newRules: List<ProfileRule>): ProfileStats { 479 val existingRulesSet = 480 existingRules.map { "${it.classDescriptor}:${it.methodDescriptor}" }.toHashSet() 481 val newRulesSet = 482 newRules.map { "${it.classDescriptor}:${it.methodDescriptor}" }.toHashSet() 483 val allUniqueRules = existingRulesSet.union(newRulesSet).size 484 485 var unmodified = 0 486 var added = 0 487 var removed = 0 488 489 for (x in existingRulesSet) { 490 if (x in newRulesSet) { 491 unmodified++ 492 } else { 493 removed++ 494 } 495 } 496 for (x in newRulesSet) { 497 if (x !in existingRulesSet) { 498 added++ 499 } 500 } 501 502 return ProfileStats( 503 existing = existingRulesSet.size, 504 new = newRulesSet.size, 505 unmodified = unmodified, 506 added = added, 507 removed = removed, 508 addedRatio = added.toFloat() / allUniqueRules, 509 removedRatio = removed.toFloat() / allUniqueRules, 510 unmodifiedRatio = unmodified.toFloat() / allUniqueRules 511 ) 512 } 513 } 514 } 515