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 }