Skip to content

Commit eb211a1

Browse files
committed
adb dump
1 parent 875cd1d commit eb211a1

14 files changed

+937
-407
lines changed

build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ tasks.withType<KotlinCompile>() {
3737

3838
compose.desktop {
3939
application {
40-
mainClass = "MainKt"
40+
mainClass = "shark.start.MainKt"
4141
nativeDistributions {
4242
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
4343
packageName = "SharkApp"

src/main/kotlin/shark/GridLayout.kt

+1-2
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@ fun GridLayout(
2222
) {
2323
Layout(
2424
modifier = modifier.verticalScroll(
25-
state = scrollState,
26-
enabled = true
25+
state = scrollState
2726
),
2827
content = content
2928
) { measurables, constraints ->

src/main/kotlin/shark/HeapGraphWindow.kt

+128
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package shark
22

3+
import androidx.compose.desktop.AppWindow
4+
import androidx.compose.desktop.WindowEvents
35
import androidx.compose.foundation.ExperimentalFoundationApi
46
import androidx.compose.foundation.layout.Row
57
import androidx.compose.material.MaterialTheme
@@ -10,9 +12,27 @@ import androidx.compose.runtime.mutableStateOf
1012
import androidx.compose.runtime.remember
1113
import androidx.compose.runtime.setValue
1214
import androidx.compose.ui.input.key.ExperimentalKeyInput
15+
import androidx.compose.ui.input.key.Key
16+
import androidx.compose.ui.input.key.Key.Companion
17+
import androidx.compose.ui.input.key.plus
18+
import androidx.compose.ui.unit.IntSize
19+
import androidx.compose.ui.window.KeyStroke
20+
import androidx.compose.ui.window.Menu
21+
import androidx.compose.ui.window.MenuBar
22+
import androidx.compose.ui.window.MenuItem
1323
import backstack.Backstack
1424
import shark.SharkScreen.HeapObjectTree
1525
import shark.SharkScreen.Home
26+
import shark.adb.showAdbDumpHeapWindow
27+
import java.awt.FileDialog
28+
import java.awt.Frame
29+
import java.awt.KeyboardFocusManager
30+
import java.awt.Toolkit
31+
import java.awt.event.KeyEvent
32+
import java.awt.image.BufferedImage
33+
import java.io.File
34+
import java.util.concurrent.Executors
35+
import javax.swing.SwingUtilities
1636

1737
@OptIn(
1838
ExperimentalKeyInput::class,
@@ -55,3 +75,111 @@ fun HeapGraphWindow(
5575
}
5676
}
5777
}
78+
79+
@OptIn(ExperimentalKeyInput::class)
80+
fun showHeapGraphWindow(
81+
windowIcon: BufferedImage,
82+
heapDumpFile: File,
83+
onWindowShown: () -> Unit = {}
84+
) {
85+
val loadingState = HeapDumpLoadingState(heapDumpFile, Executors.newSingleThreadExecutor())
86+
loadingState.load()
87+
88+
SwingUtilities.invokeLater {
89+
val screenSize = Toolkit.getDefaultToolkit().screenSize
90+
91+
lateinit var appWindow: AppWindow
92+
appWindow = AppWindow(
93+
title = "${heapDumpFile.name} - SharkApp",
94+
size = IntSize((screenSize.width * 0.8f).toInt(), (screenSize.height * 0.8f).toInt()),
95+
centered = true,
96+
icon = windowIcon,
97+
events = WindowEvents(onClose = {
98+
loadingState.loadedGraph.value?.close()
99+
loadingState.ioExecutor.shutdown()
100+
}),
101+
menuBar = MenuBar(
102+
Menu(
103+
name = "File",
104+
MenuItem(
105+
name = "Open Heap Dump",
106+
onClick = {
107+
showSelectHeapDumpFileWindow(onHprofFileSelected = { selectedFile ->
108+
showHeapGraphWindow(windowIcon, selectedFile)
109+
})
110+
},
111+
shortcut = KeyStroke(Key.O)
112+
),
113+
MenuItem(
114+
name = "Dump Heap with adb",
115+
onClick = { showAdbDumpHeapWindow(windowIcon) },
116+
shortcut = KeyStroke(Key.D)
117+
),
118+
MenuItem(
119+
name = "Close Heap Dump",
120+
onClick = {
121+
appWindow.close()
122+
},
123+
shortcut = KeyStroke(Key.W)
124+
)
125+
),
126+
)
127+
)
128+
129+
val pressedKeys = PressedKeys()
130+
131+
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher { keyEvent ->
132+
when (keyEvent.id) {
133+
KeyEvent.KEY_PRESSED -> {
134+
when (keyEvent.keyCode) {
135+
KeyEvent.VK_ALT -> pressedKeys.alt = true
136+
KeyEvent.VK_META -> pressedKeys.meta = true
137+
KeyEvent.VK_CONTROL -> pressedKeys.ctrl = true
138+
KeyEvent.VK_SHIFT -> pressedKeys.shift = true
139+
}
140+
}
141+
KeyEvent.KEY_RELEASED -> {
142+
when (keyEvent.keyCode) {
143+
KeyEvent.VK_ALT -> pressedKeys.alt = false
144+
KeyEvent.VK_META -> pressedKeys.meta = false
145+
KeyEvent.VK_CONTROL -> pressedKeys.ctrl = false
146+
KeyEvent.VK_SHIFT -> pressedKeys.shift = false
147+
}
148+
}
149+
}
150+
false
151+
}
152+
153+
val navigator = ScreenNavigator<SharkScreen>(
154+
Home(),
155+
recentsEquals = { screen1, screen2 -> screen1.title == screen2.title })
156+
appWindow.keyboard.apply {
157+
setShortcut(Key.AltLeft + Key(KeyEvent.VK_LEFT), navigator::goBack)
158+
setShortcut(Key.AltRight + Key(KeyEvent.VK_LEFT), navigator::goBack)
159+
setShortcut(Key.AltLeft + Key(KeyEvent.VK_RIGHT), navigator::goForward)
160+
setShortcut(Key.AltLeft + Key(KeyEvent.VK_RIGHT), navigator::goForward)
161+
}
162+
appWindow.show {
163+
HeapGraphWindow(navigator, loadingState, pressedKeys)
164+
}
165+
166+
onWindowShown()
167+
}
168+
}
169+
170+
class PressedKeys {
171+
var alt = false
172+
var ctrl = false
173+
var meta = false
174+
var shift = false
175+
}
176+
177+
fun showSelectHeapDumpFileWindow(onHprofFileSelected: (File) -> Unit) {
178+
val fileDialog = FileDialog(null as Frame?, "Select hprof file")
179+
fileDialog.isVisible = true
180+
181+
if (fileDialog.file != null && fileDialog.file.endsWith(".hprof")) {
182+
val heapDumpFile = File(fileDialog.directory, fileDialog.file)
183+
onHprofFileSelected(heapDumpFile)
184+
}
185+
}

src/main/kotlin/shark/LoadedGraph.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
package shark
44

5+
import kotlinx.coroutines.suspendCancellableCoroutine
56
import shark.HeapObject.HeapClass
67
import shark.HeapObject.HeapInstance
78
import shark.HeapObject.HeapObjectArray
@@ -102,7 +103,6 @@ class LoadedGraph private constructor(
102103
val realSource = fileSourceProvider.openRandomAccessSource()
103104
return object : RandomAccessSource by realSource {
104105
override fun read(sink: okio.Buffer, position: Long, byteCount: Long): Long {
105-
// println("IO from thread ${Thread.currentThread().name}")
106106
return realSource.read(sink, position, byteCount)
107107
}
108108
}
+11-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
11
package shark
22

3+
import androidx.compose.foundation.layout.Box
4+
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.material.CircularProgressIndicator
6+
import androidx.compose.material.ListItem
37
import androidx.compose.material.Text
48
import androidx.compose.runtime.Composable
9+
import androidx.compose.ui.Alignment
10+
import androidx.compose.ui.Modifier
511

612
@Composable
713
fun LoadingScreen(fileName: String) {
8-
Text("Loading $fileName")
14+
Box(modifier = Modifier.fillMaxSize()) {
15+
ListItem(text = { Text("Loading heap dump...") },
16+
secondaryText = { Text(fileName) })
17+
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
18+
}
919
}

src/main/kotlin/shark/adb/AdbCommands.kt

+88-12
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package shark.adb
22

3+
import androidx.compose.runtime.MutableState
34
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.delay
46
import kotlinx.coroutines.withContext
57
import shark.adb.AdbDevice.Offline
68
import shark.adb.AdbDevice.Online
79
import java.io.File
10+
import java.text.SimpleDateFormat
11+
import java.util.Date
12+
import java.util.Locale
813
import java.util.concurrent.TimeUnit.SECONDS
914

1015
private val workingDirectory by lazy {
@@ -26,11 +31,12 @@ sealed class AdbDevice {
2631
data class Offline(override val serialNumber: String) : AdbDevice()
2732
}
2833

34+
class AndroidProcess(val user: String, val pid: String, val name: String)
35+
2936
suspend fun listDevices(): CommandResult<List<AdbDevice>> {
30-
println("Calling listDevices")
3137
return withContext(Dispatchers.IO) {
3238
with(workingDirectory) {
33-
when (val output = runCommand("adb", "devices", "-l").apply { println("output of command: $this") }) {
39+
when (val output = runCommand("adb", "devices", "-l")) {
3440
is CommandResult.Success -> {
3541
val results = output.value.lines()
3642
.drop(1)
@@ -63,19 +69,89 @@ suspend fun listDevices(): CommandResult<List<AdbDevice>> {
6369
}
6470
}
6571
}
66-
if (results.isEmpty()) {
67-
CommandResult.Success(emptyList())
68-
} else if (results.any { it is CommandResult.Success }) {
69-
val devices = results.filterIsInstance(CommandResult.Success::class.java)
70-
.map { it.value as AdbDevice }
71-
CommandResult.Success(devices)
72-
} else {
73-
CommandResult.Error(results.joinToString("\n") { (it as CommandResult.Error).errorMessage })
72+
when {
73+
results.isEmpty() -> {
74+
CommandResult.Success(emptyList())
75+
}
76+
results.any { it is CommandResult.Success } -> {
77+
val devices = results.filterIsInstance(CommandResult.Success::class.java)
78+
.map { it.value as AdbDevice }
79+
CommandResult.Success(devices)
80+
}
81+
else -> {
82+
CommandResult.Error(results.joinToString("\n") { (it as CommandResult.Error).errorMessage })
83+
}
7484
}
7585
}
7686
is CommandResult.Error -> CommandResult.Error(output.errorMessage)
7787
}
78-
}.apply { println("Result: $this") }
88+
}
89+
}
90+
}
91+
92+
suspend fun AdbDevice.listProcesses(): CommandResult<List<AndroidProcess>> {
93+
return withContext(Dispatchers.IO) {
94+
with(workingDirectory) {
95+
when (val output = runCommand("adb", "-s", serialNumber, "shell", "ps")) {
96+
is CommandResult.Success -> {
97+
val matchingProcesses = output.value.lines()
98+
.drop(1)
99+
.map {
100+
SPACE_PATTERN.split(it)
101+
}.filter { columns ->
102+
// E.g. u0_a14
103+
columns.size >= 9 && columns[0].startsWith("u") && '_' in columns[0]
104+
}.map { columns ->
105+
AndroidProcess(
106+
user = columns[0],
107+
pid = columns[1],
108+
name = columns[8]
109+
)
110+
}
111+
CommandResult.Success(matchingProcesses)
112+
}
113+
is CommandResult.Error -> CommandResult.Error(output.errorMessage)
114+
}
115+
}
116+
}
117+
}
118+
119+
suspend fun AdbDevice.dumpHeap(process: AndroidProcess, progress: MutableState<String>): CommandResult<File> {
120+
return withContext(Dispatchers.IO) {
121+
with(workingDirectory) {
122+
val heapDumpFileName =
123+
SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS'-${process.name}.hprof'", Locale.US).format(
124+
Date()
125+
)
126+
127+
val heapDumpDevicePath = "/data/local/tmp/$heapDumpFileName"
128+
129+
progress.value = "Dumping heap to $heapDumpDevicePath"
130+
val dumpResult = runCommand(
131+
"adb", "-s", serialNumber, "shell", "am", "dumpheap", process.pid,
132+
heapDumpDevicePath
133+
)
134+
if (dumpResult is CommandResult.Error) {
135+
CommandResult.Error(dumpResult.errorMessage)
136+
} else {
137+
// Dump heap takes time but adb returns immediately.
138+
delay(3000)
139+
140+
progress.value = "Pulling $heapDumpDevicePath to $this"
141+
val pullResult =
142+
runCommand("adb", "-s", serialNumber, "pull", heapDumpDevicePath)
143+
144+
progress.value = "Deleting $heapDumpDevicePath on device"
145+
146+
runCommand("adb", "-s", serialNumber, "shell", "rm", heapDumpDevicePath)
147+
if (pullResult is CommandResult.Error) {
148+
CommandResult.Error(pullResult.errorMessage)
149+
} else {
150+
val heapDumpFile = File(workingDirectory, heapDumpFileName)
151+
CommandResult.Success(heapDumpFile)
152+
}
153+
}
154+
}
79155
}
80156
}
81157

@@ -101,7 +177,7 @@ fun File.runCommand(
101177
val command = arguments.joinToString(" ")
102178
val errorOutput = process.errorStream.bufferedReader()
103179
.readText()
104-
return CommandResult.Error("Failed command: '$command', error output:\n---\n$errorOutput---")
180+
return CommandResult.Error("Failed command:\n\n$ $command\n\nOutput:\n\n---\n$errorOutput---")
105181
}
106182
return CommandResult.Success(output)
107183
}

0 commit comments

Comments
 (0)