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