1 /* 2 * 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 java.io.File 20 import org.gradle.api.DefaultTask 21 import org.gradle.api.file.DirectoryProperty 22 import org.gradle.api.provider.Property 23 import org.gradle.api.tasks.Input 24 import org.gradle.api.tasks.OutputDirectory 25 import org.gradle.api.tasks.StopExecutionException 26 import org.gradle.api.tasks.TaskAction 27 import org.gradle.work.DisableCachingByDefault 28 29 @Suppress("UnstableApiUsage") 30 @DisableCachingByDefault(because = "Benchmark measurements are performed each task execution.") 31 abstract class BenchmarkReportTask : DefaultTask() { 32 33 init { 34 group = "Android" 35 description = 36 "Run benchmarks found in the current project and output reports to the " + 37 "benchmark_reports folder under the project's build directory." 38 39 // This task should mirror the upToDate behavior of connectedAndroidTest as we always want 40 // this task to run after connectedAndroidTest is run to pull the most recent benchmark 41 // report data, even when tests are triggered multiple times in a row without source 42 // changes. <lambda>null43 outputs.upToDateWhen { false } 44 } 45 46 @get:OutputDirectory abstract val benchmarkReportDir: DirectoryProperty 47 48 @get:Input abstract val adbPath: Property<String> 49 50 @TaskAction execnull51 fun exec() { 52 // Fetch reports from all available devices as the default behaviour of connectedAndroidTest 53 // is to run on all available devices. 54 getReportsForDevices(Adb(adbPath.get(), logger)) 55 } 56 getReportsForDevicesnull57 private fun getReportsForDevices(adb: Adb) { 58 val reportDir = benchmarkReportDir.asFile.get() 59 if (reportDir.exists()) { 60 reportDir.deleteRecursively() 61 } 62 reportDir.mkdirs() 63 64 val deviceIds = 65 adb.execSync("devices -l") 66 .stdout 67 .split("\n") 68 .drop(1) 69 .filter { !it.contains("unauthorized") } 70 .map { it.split(Regex("\\s+")).first().trim() } 71 .filter { !it.isBlank() } 72 73 for (deviceId in deviceIds) { 74 val dataDir = getReportDirForDevice(adb, deviceId) 75 if (dataDir.isBlank()) { 76 throw StopExecutionException("Failed to find benchmark report on device: $deviceId") 77 } 78 79 val outDir = File(reportDir, deviceId) 80 outDir.mkdirs() 81 getReportsForDevice(adb, outDir, dataDir, deviceId) 82 logger.info( 83 "Benchmark", 84 "Benchmark report files generated at ${reportDir.absolutePath}" 85 ) 86 } 87 } 88 getReportsForDevicenull89 private fun getReportsForDevice( 90 adb: Adb, 91 benchmarkReportDir: File, 92 dataDir: String, 93 deviceId: String 94 ) { 95 adb.execSync("shell ls $dataDir", deviceId) 96 .stdout 97 .split("\n") 98 .map { it.trim() } 99 .filter { it.matches(Regex(".*benchmarkData[.](?:xml|json)$")) } 100 .forEach { 101 val src = "$dataDir/$it" 102 adb.execSync("pull $src $benchmarkReportDir/$it", deviceId) 103 adb.execSync("shell rm $src", deviceId) 104 } 105 } 106 107 /** 108 * Query for test runner user's Download dir on shared public external storage via content 109 * provider APIs. 110 * 111 * This folder is typically accessed in Android code via 112 * Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) 113 */ getReportDirForDevicenull114 private fun getReportDirForDevice(adb: Adb, deviceId: String): String { 115 116 val cmd = "shell content query --uri content://media/external/file --projection _data" 117 118 // With Android >= 10 `LIKE` is no longer supported when specifying a `WHERE` clause so we 119 // need to manually filter the output here. 120 // Note that stdout of the above command is of the form: 121 // Row: 0 _data=/storage/emulated 122 // Row: 1 _data=/storage/emulated/0 123 // Row: 2 _data=/storage/emulated/0/Music 124 // Row: 3 _data=/storage/emulated/0/Podcasts 125 // Row: 4 _data=/storage/emulated/0/Ringtones 126 // Row: 5 _data=/storage/emulated/0/Alarms 127 // Row: 5 _data=/storage/emulated/0/Download 128 // etc 129 130 // There are 2 filters: the first filters all the rows ending with `Download`, while 131 // the second excludes app-scoped shared external storage. 132 return adb.execSync(cmd, deviceId) 133 .stdout 134 .split("\n") 135 .filter { it.matches(regex = Regex(".*/Download")) } 136 .first { !it.matches(regex = Regex(".*files/Download")) } 137 .trim() 138 .split(Regex("\\s+")) 139 .last() 140 .split("=") 141 .last() 142 .trim() 143 } 144 } 145