Skip to content

Commit e29b8cf

Browse files
committed
laybox: add more commands
- gki - boot timing analyzer
1 parent 33224b0 commit e29b8cf

9 files changed

Lines changed: 2786 additions & 246 deletions

File tree

lazybox/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ dependencies {
2828
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
2929
implementation("com.squareup.okhttp3:okhttp:4.12.0")
3030
implementation("com.squareup.okio:okio:3.9.0")
31+
implementation("org.bytedeco:javacv-platform:1.5.12")
3132
// Use the Kotlin JUnit 5 integration.
3233
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
3334
// Use the JUnit 5 integration.
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package cfig.lazybox
2+
3+
import java.io.File
4+
import java.text.SimpleDateFormat
5+
import java.util.Date
6+
import java.util.concurrent.TimeUnit
7+
import kotlin.system.exitProcess
8+
9+
private object Colors {
10+
const val RED = "\u001B[0;31m"
11+
const val GREEN = "\u001B[0;32m"
12+
const val YELLOW = "\u001B[1;33m"
13+
const val BLUE = "\u001B[0;34m"
14+
const val CYAN = "\u001B[0;36m"
15+
const val NC = "\u001B[0m"
16+
}
17+
18+
private fun printHeader(message: String) {
19+
println("${Colors.BLUE}========================================${Colors.NC}")
20+
println("${Colors.BLUE}$message${Colors.NC}")
21+
println("${Colors.BLUE}========================================${Colors.NC}")
22+
}
23+
24+
private fun printInfo(message: String) {
25+
println("${Colors.GREEN}[INFO]${Colors.NC} $message")
26+
}
27+
28+
private fun printWarn(message: String) {
29+
println("${Colors.YELLOW}[WARN]${Colors.NC} $message")
30+
}
31+
32+
private fun printError(message: String) {
33+
println("${Colors.RED}[ERROR]${Colors.NC} $message")
34+
}
35+
36+
private fun printSuccess(message: String) {
37+
println("${Colors.GREEN}[OK]${Colors.NC} $message")
38+
}
39+
40+
private data class CommandResult(val output: List<String>, val exitCode: Int)
41+
42+
private fun runHostCommand(vararg command: String): CommandResult {
43+
return try {
44+
val process = ProcessBuilder(*command)
45+
.redirectErrorStream(true)
46+
.start()
47+
val output = process.inputStream.bufferedReader().readLines()
48+
if (!process.waitFor(10, TimeUnit.SECONDS)) {
49+
process.destroy()
50+
CommandResult(output, 124)
51+
} else {
52+
CommandResult(output, process.exitValue())
53+
}
54+
} catch (e: Exception) {
55+
CommandResult(emptyList(), 127)
56+
}
57+
}
58+
59+
private class AdbHelper {
60+
fun isAdbAvailable(): Boolean {
61+
return runHostCommand("adb", "version").exitCode == 0
62+
}
63+
64+
fun isConnected(): Boolean {
65+
val devices = runHostCommand("adb", "devices").output
66+
return devices.any { line ->
67+
val trimmed = line.trim()
68+
trimmed.isNotEmpty() && trimmed.endsWith("device") && !trimmed.startsWith("List")
69+
}
70+
}
71+
72+
fun executeOnDevice(command: String): List<String> {
73+
return runHostCommand("adb", "shell", command).output
74+
}
75+
76+
fun findApexFiles(): List<String> {
77+
val apexFiles = mutableListOf<String>()
78+
apexFiles.addAll(executeOnDevice("find / -name '*.apex' 2>/dev/null"))
79+
apexFiles.addAll(executeOnDevice("find / -name '*.capex' 2>/dev/null"))
80+
return apexFiles.filter { it.isNotBlank() }
81+
}
82+
83+
fun getApexMounts(): List<String> {
84+
return executeOnDevice("mount | grep apex").filter { it.isNotBlank() }
85+
}
86+
87+
fun findPermissionController(): List<String> {
88+
return executeOnDevice("find / -name '*permission*.apex' 2>/dev/null").filter { it.isNotBlank() }
89+
}
90+
}
91+
92+
class Apex {
93+
private val adb = AdbHelper()
94+
95+
private fun checkConnection() {
96+
printHeader("Check ADB connection")
97+
98+
if (!adb.isAdbAvailable()) {
99+
printError("ADB is not installed or not in PATH")
100+
exitProcess(1)
101+
}
102+
103+
if (!adb.isConnected()) {
104+
printError("No connected devices found")
105+
println("Please ensure:")
106+
println(" 1. Device connected via USB")
107+
println(" 2. USB debugging enabled")
108+
println(" 3. ADB authorization granted")
109+
exitProcess(1)
110+
}
111+
112+
printSuccess("ADB connection OK")
113+
runHostCommand("adb", "devices").output.forEach { println(it) }
114+
println()
115+
}
116+
117+
private fun findApexFiles() {
118+
printHeader("Search APEX files")
119+
120+
printInfo("Searching APEX files on device...")
121+
val apexFiles = adb.findApexFiles()
122+
123+
if (apexFiles.isEmpty()) {
124+
printWarn("No APEX files found")
125+
return
126+
}
127+
128+
printSuccess("Found ${apexFiles.size} APEX files")
129+
println()
130+
131+
printHeader("APEX file list")
132+
apexFiles.sorted().forEachIndexed { index, file ->
133+
println("${index + 1}. $file")
134+
}
135+
println()
136+
137+
val apexCount = apexFiles.count { it.endsWith(".apex") }
138+
val capexCount = apexFiles.count { it.endsWith(".capex") }
139+
140+
printHeader("Statistics")
141+
println("Total: ${apexFiles.size}")
142+
println(" - .apex files: $apexCount")
143+
println(" - .capex files: $capexCount")
144+
println()
145+
146+
printHeader("Group by location")
147+
println()
148+
149+
val byLocation = apexFiles.groupBy { file ->
150+
when {
151+
file.contains("/system/apex") -> "/system/apex"
152+
file.startsWith("/apex") -> "/apex"
153+
file.contains("/system/priv-app") -> "/system/priv-app"
154+
file.contains("/data") -> "/data"
155+
else -> "other"
156+
}
157+
}
158+
159+
byLocation.forEach { (location, files) ->
160+
println("${Colors.CYAN}$location:${Colors.NC}")
161+
files.sorted().forEach { println(" $it") }
162+
println()
163+
}
164+
165+
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
166+
val outputFile = File("apex_files_$timeStamp.txt")
167+
outputFile.writeText(apexFiles.sorted().joinToString("\n"))
168+
printInfo("Saved to: ${outputFile.absolutePath}")
169+
println()
170+
}
171+
172+
private fun checkMounts() {
173+
printHeader("Check APEX mounts")
174+
175+
val mounts = adb.getApexMounts()
176+
if (mounts.isEmpty()) {
177+
printWarn("No mounted APEX found")
178+
} else {
179+
mounts.forEach { println(it) }
180+
}
181+
println()
182+
}
183+
184+
private fun checkPermissionController() {
185+
printHeader("Check PermissionController")
186+
187+
printInfo("Searching permission-related APEX...")
188+
val permApex = adb.findPermissionController()
189+
if (permApex.isEmpty()) {
190+
printWarn("No permission-related APEX found")
191+
} else {
192+
permApex.forEach { println(it) }
193+
}
194+
println()
195+
196+
printInfo("Searching permission XML files...")
197+
val privappPerms = adb.executeOnDevice("find / -name 'privapp-permissions*.xml' 2>/dev/null")
198+
.filter { it.isNotBlank() }
199+
if (privappPerms.isEmpty()) {
200+
printWarn("No permission XML files found")
201+
} else {
202+
privappPerms.forEach { println(it) }
203+
}
204+
println()
205+
}
206+
207+
private fun showHelp() {
208+
println(
209+
"""
210+
Usage: lazybox apex [options]
211+
212+
Options:
213+
-h, --help Show help
214+
-a, --all Run all checks
215+
-f, --find Find APEX files (default)
216+
-m, --mounts Check APEX mounts
217+
-p, --perm Check PermissionController
218+
219+
Examples:
220+
lazybox apex
221+
lazybox apex --all
222+
lazybox apex --mounts
223+
""".trimIndent()
224+
)
225+
}
226+
227+
fun run(args: Array<String>) {
228+
when {
229+
args.isEmpty() || args[0] == "-f" || args[0] == "--find" -> {
230+
checkConnection()
231+
findApexFiles()
232+
}
233+
args[0] == "-h" || args[0] == "--help" -> {
234+
showHelp()
235+
}
236+
args[0] == "-a" || args[0] == "--all" -> {
237+
checkConnection()
238+
findApexFiles()
239+
checkMounts()
240+
checkPermissionController()
241+
}
242+
args[0] == "-m" || args[0] == "--mounts" -> {
243+
checkConnection()
244+
checkMounts()
245+
}
246+
args[0] == "-p" || args[0] == "--perm" -> {
247+
checkConnection()
248+
checkPermissionController()
249+
}
250+
else -> {
251+
println("Unknown option: ${args[0]}")
252+
showHelp()
253+
}
254+
}
255+
}
256+
}

lazybox/src/main/kotlin/cfig/lazybox/App.kt

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package cfig.lazybox
22

3+
import cfig.lazybox.profiler.VideoAnalyzer
34
import cfig.lazybox.staging.AospCompiledb
45
import cfig.lazybox.staging.DiffCI
56
import cfig.lazybox.staging.Perfetto
@@ -22,7 +23,7 @@ fun main(args: Array<String>) {
2223
println("Usage: args: (Array<String>) ...")
2324
println(" or: function [arguments]...")
2425
println("\nCurrently defined functions:")
25-
println("\tcpuinfo gki sysinfo sysstat pidstat bootchart thermal_info compiledb")
26+
println("\tcpuinfo gki sysinfo sysstat pidstat bootchart thermal_info compiledb analyze batch_analyze stat apex")
2627
println("\nCommand Usage:")
2728
println("bootchart: generate Android bootchart")
2829
println("gki : interactive GKI JSON downloader/parser, or process GKI modules from <dir>")
@@ -31,6 +32,12 @@ fun main(args: Array<String>) {
3132
println("cpuinfo : get cpu info from /sys/devices/system/cpu/")
3233
println("sysinfo : get overall system info from Android")
3334
println("thermal_info : get thermal info from /sys/class/thermal/")
35+
println("apex : find APEX files and mounts on Android device")
36+
println("analyze [log|video] <log_dir> : analyze device boot performance from video recording and/or logs")
37+
println(" default to analyze both video and logs")
38+
println("batch_analyze <dir> : analyze every immediate subdirectory under <dir>")
39+
println("stat outlier <dir> : generate boot_time_stability_report.md for all runs under <dir>")
40+
println("stat compare <a_dir> <b_dir> [--a-name NAME] [--b-name NAME] : compare boot times between A and B")
3441
println("\nIncubating usage:")
3542
println("compiledb : generate compilation database for AOSP")
3643
println("dmainfo : parse /d/dma_buf/bufinfo")
@@ -130,4 +137,67 @@ fun main(args: Array<String>) {
130137
if (args[0] == "ina") {
131138
InaSensor().run()
132139
}
140+
if (args[0] == "analyze") {
141+
val subCommand = args.getOrNull(1)
142+
if (subCommand == "log") {
143+
// analyze log <log_dir>
144+
VideoAnalyzer().analyzeLog(args.drop(2).toTypedArray())
145+
} else if (subCommand == "video") {
146+
// analyze video <log_dir>
147+
VideoAnalyzer().analyzeVideo(args.drop(2).toTypedArray())
148+
} else if (subCommand == "merge") {
149+
VideoAnalyzer().mergeReports(args.drop(2).toTypedArray())
150+
} else {
151+
// analyze <log_dir>
152+
val analyzerArgs = args.drop(1).toTypedArray()
153+
VideoAnalyzer().analyzeVideo(analyzerArgs)
154+
VideoAnalyzer().analyzeLog(analyzerArgs)
155+
VideoAnalyzer().mergeReports(analyzerArgs)
156+
}
157+
}
158+
if (args[0] == "batch_analyze") {
159+
if (args.size != 2) {
160+
log.error("Usage: batch_analyze <parent_dir>")
161+
return
162+
}
163+
val parentDir = File(args[1])
164+
if (!parentDir.exists() || !parentDir.isDirectory) {
165+
log.error("Directory not found: ${parentDir.absolutePath}")
166+
return
167+
}
168+
169+
val children = parentDir.listFiles()?.filter { it.isDirectory }?.sortedBy { it.name } ?: emptyList()
170+
if (children.isEmpty()) {
171+
log.warn("No subdirectories found under: ${parentDir.absolutePath}")
172+
return
173+
}
174+
175+
for (child in children) {
176+
log.info("batch_analyze: ${child.absolutePath}")
177+
val analyzerArgs = arrayOf(child.absolutePath)
178+
val analyzer = VideoAnalyzer()
179+
val videoFile = File(child, "video.mp4")
180+
val csvFile = File(child, "video.csv")
181+
if (videoFile.exists() && csvFile.exists()) {
182+
analyzer.analyzeVideo(analyzerArgs)
183+
} else {
184+
log.warn("Skip video analyze (video.mp4/video.csv missing): ${child.absolutePath}")
185+
}
186+
analyzer.analyzeLog(analyzerArgs)
187+
analyzer.mergeReports(analyzerArgs)
188+
}
189+
}
190+
if (args[0] == "stat") {
191+
val subCommand = args.getOrNull(1)
192+
if (subCommand == "outlier") {
193+
BootTimeStability.run(args.drop(2).toTypedArray())
194+
} else if (subCommand == "compare") {
195+
BootTimeABCompare.run(args.drop(2).toTypedArray())
196+
} else {
197+
log.error("Usage: stat outlier <dir> | stat compare <a_dir> <b_dir>")
198+
}
199+
}
200+
if (args[0] == "apex") {
201+
Apex().run(args.drop(1).toTypedArray())
202+
}
133203
}

0 commit comments

Comments
 (0)