blob: b15afd5a42d97a00107b66624024bc8cbc43115c [file] [log] [blame]
Daniel Santiago Riveradd6d0d82023-02-16 11:44:02 -05001/*
2 * 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
17import java.io.BufferedReader
18import java.io.File
19import java.util.concurrent.TimeUnit
20
21val currentDir = File(".").absolutePath
22check(currentDir.endsWith("/frameworks/support/.")) {
23 "Script needs to be executed from '<check-out>/frameworks/support', was '$currentDir'."
24}
25val scriptDir = File(currentDir, "room/scripts")
26
27check(args.size >= 6) { "Expected at least 6 args. See usage instructions."}
28val taskIds = args.count { it == "-t" }
29if (taskIds != 2) {
30 error("Exactly two tags are required per invocation. Found $taskIds")
31}
32
33val firstTagIndex = args.indexOfFirst { it == "-t" } + 1
34val firstTag = args[firstTagIndex]
35val firstTasks = extractTasks(firstTagIndex, args)
36check(firstTasks.isNotEmpty()) { "Task list for a tag must not be empty." }
37
38val secondTagIndex = args.indexOfLast { it == "-t" } + 1
39val secondTag = args[secondTagIndex]
40val secondTasks = extractTasks(secondTagIndex, args)
41check(secondTasks.isNotEmpty()) { "Task list for a tag must not be empty." }
42
43println("Comparing tasks groups!")
44println("First tag: $firstTag")
45println("Task list:\n${firstTasks.joinToString(separator = "\n")}")
46println("Second tag: $secondTag")
47println("Task list\n${secondTasks.joinToString(separator = "\n")}")
48
49cleanBuild(firstTasks)
50val firstResult = profile(firstTag, firstTasks)
51
52cleanBuild(secondTasks)
53val secondResult = profile(secondTag, secondTasks)
54
55crunchNumbers(firstResult)
56crunchNumbers(secondResult)
57
58fun 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
69fun cleanBuild(tasks: List<String>) {
70 println("Running initial build to cook cache...")
71 runCommand("./gradlew --stop")
72 runCommand("./gradlew ${tasks.joinToString(separator = " ")}")
73}
74
75fun 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
129fun 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
148fun 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
178data 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
185 fun minTotal(): Float = taskTimes.minOf { it.values.sum() }
186
187 fun avgTotal(): Float = taskTimes.map { it.values.sum() }.sum() / taskTimes.size
188
189 fun maxTotal(): Float = taskTimes.maxOf { it.values.sum() }
190
191 fun minTask(name: String): Float = taskTimes.minOf { it.getValue(name) }
192
193 fun avgTask(name: String): Float = taskTimes.map { it.getValue(name) }.sum() / taskTimes.size
194
195 fun maxTask(name: String): Float = taskTimes.maxOf { it.getValue(name) }
196}