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 import java.io.BufferedReader
18 import java.io.File
19 import java.util.concurrent.TimeUnit
20 
21 val currentDir = File(".").absolutePath
22 check(currentDir.endsWith("/frameworks/support/.")) {
23     "Script needs to be executed from '<check-out>/frameworks/support', was '$currentDir'."
24 }
25 val scriptDir = File(currentDir, "room/scripts")
26 
<lambda>null27 check(args.size >= 6) { "Expected at least 6 args. See usage instructions."}
<lambda>null28 val taskIds = args.count { it == "-t" }
29 if (taskIds != 2) {
30     error("Exactly two tags are required per invocation. Found $taskIds")
31 }
32 
<lambda>null33 val firstTagIndex = args.indexOfFirst { it == "-t" } + 1
34 val firstTag = args[firstTagIndex]
35 val firstTasks = extractTasks(firstTagIndex, args)
<lambda>null36 check(firstTasks.isNotEmpty()) { "Task list for a tag must not be empty." }
37 
<lambda>null38 val secondTagIndex = args.indexOfLast { it == "-t" } + 1
39 val secondTag = args[secondTagIndex]
40 val secondTasks = extractTasks(secondTagIndex, args)
<lambda>null41 check(secondTasks.isNotEmpty()) { "Task list for a tag must not be empty." }
42 
43 println("Comparing tasks groups!")
44 println("First tag: $firstTag")
45 println("Task list:\n${firstTasks.joinToString(separator = "\n")}")
46 println("Second tag: $secondTag")
47 println("Task list\n${secondTasks.joinToString(separator = "\n")}")
48 
49 cleanBuild(firstTasks)
50 val firstResult = profile(firstTag, firstTasks)
51 
52 cleanBuild(secondTasks)
53 val secondResult = profile(secondTag, secondTasks)
54 
55 crunchNumbers(firstResult)
56 crunchNumbers(secondResult)
57 
extractTasksnull58 fun extractTasks(tagIndex: Int, args: Array<String>): List<String> {
59    return buildList {
60        for (i in (tagIndex + 1) until args.size) {
61            if (args[i] == "-t") {
62                break
63            }
64            add(args[i])
65        }
66    }
67 }
68 
cleanBuildnull69 fun cleanBuild(tasks: List<String>) {
70     println("Running initial build to cook cache...")
71     runCommand("./gradlew --stop")
72     runCommand("./gradlew ${tasks.joinToString(separator = " ")}")
73 }
74 
profilenull75 fun profile(
76     tag: String,
77     tasks: List<String>,
78     amount: Int = 10
79 ): ProfileResult {
80     println("Profiling tasks for '$tag'...")
81     val allRunTimes = List(amount) { runNumber ->
82         val profileCmd = buildString {
83             append("./gradlew ")
84             append("--init-script $scriptDir/rerun-requested-task-init-script.gradle ")
85             append("--no-configuration-cache ")
86             append("--profile ")
87             append(tasks.joinToString(separator = " "))
88         }
89         val reportPath = runCommand(profileCmd, returnOutputStream = true)?.use { stream ->
90             stream.lineSequence().forEach { line ->
91                 if (line.startsWith("See the profiling report at:")) {
92                     val scheme = "file://"
93                     return@use line.substring(
94                         line.indexOf(scheme) + scheme.length
95                     )
96                 }
97             }
98             return@use null
99         }
100         checkNotNull(reportPath) { "Couldn't get report path!" }
101         println("Result at: $reportPath")
102         val taskTimes = mutableMapOf<String, Float>()
103         File(reportPath).bufferedReader().use { reader ->
104             while (true) {
105                 val line = reader.readLine()
106                 if (line == null) {
107                     return@use
108                 }
109                 tasks.forEach { taskName ->
110                     if (line.contains(">$taskName<")) {
111                         val timeValue = checkNotNull(reader.readLine())
112                             .drop("<td class=\"numeric\">".length)
113                             .let { it.substring(0, it.indexOf("s</td>")) }
114                             .toFloat()
115                         taskTimes[taskName] = taskTimes.getOrDefault(taskName, 0.0f) + timeValue
116                     }
117                 }
118             }
119         }
120         println("Result of run #${runNumber + 1} of '$tag':")
121         taskTimes.forEach { taskName, time ->
122             println("$time - $taskName")
123         }
124         return@List taskTimes
125     }
126     return ProfileResult(tag, allRunTimes)
127 }
128 
crunchNumbersnull129 fun crunchNumbers(result: ProfileResult) {
130     println("--------------------")
131     println("Summary of profile for '${result.tag}'")
132     println("--------------------")
133     println("Total time (${result.numOfRuns} runs):")
134     println("  Min: ${result.minTotal()}")
135     println("  Avg: ${result.avgTotal()}")
136     println("  Max: ${result.maxTotal()}")
137     println("Per task times:")
138     result.tasks.forEach { taskName ->
139         println("  $taskName")
140         println(buildString {
141             append("  Min: ${result.minTask(taskName)}")
142             append("  Avg: ${result.avgTask(taskName)}")
143             append("  Max: ${result.maxTask(taskName)}")
144         })
145     }
146 }
147 
runCommandnull148 fun runCommand(
149     command: String,
150     workingDir: File = File("."),
151     timeoutAmount: Long = 60,
152     timeoutUnit: TimeUnit = TimeUnit.SECONDS,
153     returnOutputStream: Boolean = false
154 ): BufferedReader? = runCatching {
155     println("Executing: $command")
156     val proc = ProcessBuilder("\\s".toRegex().split(command))
157         .directory(workingDir)
158         .apply {
159             if (returnOutputStream) {
160                 redirectOutput(ProcessBuilder.Redirect.PIPE)
161             } else {
162                 redirectOutput(ProcessBuilder.Redirect.INHERIT)
163             }
164         }
165         .redirectError(ProcessBuilder.Redirect.INHERIT)
166         .start()
167     proc.waitFor(timeoutAmount, timeoutUnit)
168     if (proc.exitValue() != 0) {
169         error("Non-zero exit code received: ${proc.exitValue()}")
170     }
171     return if (returnOutputStream) {
172         proc.inputStream.bufferedReader()
173     } else {
174         null
175     }
176 }.onFailure { it.printStackTrace() }.getOrNull()
177 
178 data class ProfileResult(
179     val tag: String,
180     private val taskTimes: List<Map<String, Float>>
181 ) {
182     val numOfRuns = taskTimes.size
183     val tasks = taskTimes.first().keys
184 
<lambda>null185     fun minTotal(): Float = taskTimes.minOf { it.values.sum() }
186 
<lambda>null187     fun avgTotal(): Float = taskTimes.map { it.values.sum() }.sum() / taskTimes.size
188 
<lambda>null189     fun maxTotal(): Float = taskTimes.maxOf { it.values.sum() }
190 
<lambda>null191     fun minTask(name: String): Float = taskTimes.minOf { it.getValue(name) }
192 
<lambda>null193     fun avgTask(name: String): Float = taskTimes.map { it.getValue(name) }.sum() / taskTimes.size
194 
<lambda>null195     fun maxTask(name: String): Float = taskTimes.maxOf { it.getValue(name) }
196 }