From 2d8f2a2b55c983975dbfa3e6dbb5b6be5cc07593 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Ricau Date: Wed, 31 May 2023 10:48:23 -0700 Subject: [PATCH 1/2] Exploring treemaps --- .../internal/LeakCanaryFileProvider.kt | 5 +- .../internal/HeapDataRepository.aidl | 4 +- .../org/leakcanary/internal/LeakUiApp.aidl | 4 +- .../internal/ParcelableDominators.kt | 34 ++ .../leakcanary-app-service/build.gradle | 2 + .../internal/HeapDataRepositoryService.kt | 35 ++- .../leakcanary/internal/LeakUiAppClient.kt | 18 +- leakcanary/leakcanary-app/build.gradle | 1 + .../src/main/AndroidManifest.xml | 7 + .../main/java/org/leakcanary/MainActivity.kt | 1 + .../org/leakcanary/data/HeapRepository.kt | 2 +- .../screens/ClientAppAnalysisScreen.kt | 23 +- .../org/leakcanary/screens/Destination.kt | 4 + .../java/org/leakcanary/screens/ScreenHost.kt | 2 + .../org/leakcanary/screens/TreeMapScreen.kt | 210 +++++++++++++ .../org/leakcanary/screens/TreemapLayout.kt | 294 ++++++++++++++++++ .../leakcanary/service/LeakUiAppService.kt | 49 ++- .../org/leakcanary/service/TreeMapFetcher.kt | 92 ++++++ shark/shark/src/main/java/shark/Dominators.kt | 6 + .../src/main/java/shark/ObjectDominators.kt | 29 +- 20 files changed, 805 insertions(+), 17 deletions(-) create mode 100644 leakcanary/leakcanary-app-aidl/src/main/java/org/leakcanary/internal/ParcelableDominators.kt create mode 100644 leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreeMapScreen.kt create mode 100644 leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreemapLayout.kt create mode 100644 leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/TreeMapFetcher.kt create mode 100644 shark/shark/src/main/java/shark/Dominators.kt diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryFileProvider.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryFileProvider.kt index 346f096d8c..2fb8d74c76 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryFileProvider.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/LeakCanaryFileProvider.kt @@ -40,8 +40,11 @@ import org.xmlpull.v1.XmlPullParserException /** * Copy of androidx.core.content.FileProvider, converted to Kotlin. + * This is an internal class, only public to be usable in another module. + * TODO Consider building a public API for exposing files with the right permissions to + * be shared. */ -internal class LeakCanaryFileProvider : ContentProvider() { +class LeakCanaryFileProvider : ContentProvider() { private lateinit var mStrategy: PathStrategy diff --git a/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/HeapDataRepository.aidl b/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/HeapDataRepository.aidl index 7c32252d9f..bbf6589fab 100644 --- a/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/HeapDataRepository.aidl +++ b/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/HeapDataRepository.aidl @@ -1,5 +1,7 @@ package org.leakcanary.internal; +parcelable ParcelableDominators; + interface HeapDataRepository { - void sayHi(); + ParcelableDominators sayHi(String heapDumpFilePath); } diff --git a/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/LeakUiApp.aidl b/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/LeakUiApp.aidl index 37ee60ab99..c068c5c1ab 100644 --- a/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/LeakUiApp.aidl +++ b/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/LeakUiApp.aidl @@ -1,7 +1,9 @@ package org.leakcanary.internal; +import android.net.Uri; + parcelable ParcelableHeapAnalysis; interface LeakUiApp { - void sendHeapAnalysis(in ParcelableHeapAnalysis heapAnalysis); + void sendHeapAnalysis(in ParcelableHeapAnalysis heapAnalysis, in Uri heapDumpUri); } diff --git a/leakcanary/leakcanary-app-aidl/src/main/java/org/leakcanary/internal/ParcelableDominators.kt b/leakcanary/leakcanary-app-aidl/src/main/java/org/leakcanary/internal/ParcelableDominators.kt new file mode 100644 index 0000000000..07016bb660 --- /dev/null +++ b/leakcanary/leakcanary-app-aidl/src/main/java/org/leakcanary/internal/ParcelableDominators.kt @@ -0,0 +1,34 @@ +package org.leakcanary.internal + +import android.os.Parcel +import android.os.Parcelable +import shark.Dominators +import shark.HeapAnalysis + +class ParcelableDominators(val wrapped: Dominators) : Parcelable { + + private constructor(source: Parcel) : this(source.readSerializable() as Dominators) + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeSerializable(wrapped) + println("FOO WRITE ${dest.dataSize()} $dest") + } + + override fun describeContents() = 0 + + companion object { + @Suppress("UNCHECKED_CAST") + @JvmField val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(source: Parcel): ParcelableDominators { + println("FOO READ ${source.dataSize()} $source") + return ParcelableDominators(source) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + fun Dominators.asParcelable(): ParcelableDominators = ParcelableDominators(this) + } +} diff --git a/leakcanary/leakcanary-app-service/build.gradle b/leakcanary/leakcanary-app-service/build.gradle index 817bfc555c..ccf54c1dbd 100644 --- a/leakcanary/leakcanary-app-service/build.gradle +++ b/leakcanary/leakcanary-app-service/build.gradle @@ -24,5 +24,7 @@ android { dependencies { implementation projects.leakcanary.leakcanaryAppAidl + implementation projects.leakcanary.leakcanaryAndroidCore implementation projects.shark.shark + implementation projects.shark.sharkAndroid } diff --git a/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/HeapDataRepositoryService.kt b/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/HeapDataRepositoryService.kt index c9e58ea7ee..4098621f20 100644 --- a/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/HeapDataRepositoryService.kt +++ b/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/HeapDataRepositoryService.kt @@ -3,14 +3,45 @@ package org.leakcanary.internal import android.app.Service import android.content.Intent import android.os.IBinder +import java.io.File +import java.util.EnumSet +import org.leakcanary.internal.ParcelableDominators.Companion.asParcelable +import shark.AndroidReferenceMatchers +import shark.Dominators +import shark.HprofHeapGraph.Companion.openHeapGraph +import shark.IgnoredReferenceMatcher +import shark.ObjectDominators internal class HeapDataRepositoryService : Service() { // TODO Stubs can be longer lived than the outer service, handle // manually clearing out the stub reference to the service. private val binder = object : HeapDataRepository.Stub() { - override fun sayHi() { - println("HeapDataRepository says hi") + override fun sayHi(heapDumpFilePath: String): ParcelableDominators { + try { + return File(heapDumpFilePath).openHeapGraph().use { heapGraph -> + val weakAndFinalizerRefs = EnumSet.of( + AndroidReferenceMatchers.REFERENCES, AndroidReferenceMatchers.FINALIZER_WATCHDOG_DAEMON + ) + val ignoredRefs = + AndroidReferenceMatchers.buildKnownReferences(weakAndFinalizerRefs).map { matcher -> + matcher as IgnoredReferenceMatcher + } + + // TODO Move offline part here. + // Also trying out without the names to see how big this is. + // Didn't work => need to expose an object that can be queried through IPC? + // That means keeping this structure in memory probably? + val result = Dominators( + ObjectDominators().buildDominatorTree(heapGraph, ignoredRefs) + ).asParcelable() + result + } + } catch (throwable: Throwable) { + // TODO cleanup. But we do need the right stacktrace here. + throwable.printStackTrace() + throw throwable + } } } diff --git a/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/LeakUiAppClient.kt b/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/LeakUiAppClient.kt index b682f7f4c5..92bbf15f65 100644 --- a/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/LeakUiAppClient.kt +++ b/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/LeakUiAppClient.kt @@ -3,10 +3,12 @@ package org.leakcanary.internal import android.content.ComponentName import android.content.Context import android.content.Intent +import android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION import android.content.ServiceConnection import android.os.IBinder import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import leakcanary.internal.LeakCanaryFileProvider import org.leakcanary.internal.ParcelableHeapAnalysis.Companion.asParcelable import shark.HeapAnalysis import shark.SharkLog @@ -60,7 +62,21 @@ class LeakUiAppClient( SharkLog.d { "LeakUiAppService up=$bringingServiceUp" } val serviceConnected = sendLatch.await(20, TimeUnit.SECONDS) if (serviceConnected) { - leakUiApp.sendHeapAnalysis(heapAnalysis.asParcelable()) + val heapDumpContentUri = LeakCanaryFileProvider.getUriForFile( + appContext, + "com.squareup.leakcanary.fileprovider.${appContext.packageName}", + heapAnalysis.heapDumpFile + ) + appContext.grantUriPermission("org.leakcanary", heapDumpContentUri, FLAG_GRANT_READ_URI_PERMISSION) + try { + leakUiApp.sendHeapAnalysis(heapAnalysis.asParcelable(), heapDumpContentUri) + } finally { + appContext.revokeUriPermission( + "org.leakcanary", heapDumpContentUri, FLAG_GRANT_READ_URI_PERMISSION + ) + } + + // TODO Revoke permission } else { // TODO Handle service connection error } diff --git a/leakcanary/leakcanary-app/build.gradle b/leakcanary/leakcanary-app/build.gradle index 15fc137836..ad4ece3b1b 100644 --- a/leakcanary/leakcanary-app/build.gradle +++ b/leakcanary/leakcanary-app/build.gradle @@ -95,6 +95,7 @@ dependencies { // TODO Split out what's included in debug vs the subset for release implementation projects.leakcanary.leakcanaryAndroid implementation 'com.google.dagger:hilt-android:2.43.2' + implementation libs.okio2 kapt 'com.google.dagger:hilt-compiler:2.43.2' } diff --git a/leakcanary/leakcanary-app/src/main/AndroidManifest.xml b/leakcanary/leakcanary-app/src/main/AndroidManifest.xml index 4af98d278f..a9d0fe96c1 100644 --- a/leakcanary/leakcanary-app/src/main/AndroidManifest.xml +++ b/leakcanary/leakcanary-app/src/main/AndroidManifest.xml @@ -42,4 +42,11 @@ + + + + + + + diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/MainActivity.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/MainActivity.kt index cc05fac45e..b1e63a0411 100644 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/MainActivity.kt +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/MainActivity.kt @@ -59,6 +59,7 @@ class MainActivity : ComponentActivity() { val intent = Intent("org.leakcanary.internal.HeapDataRepositoryService.BIND") .apply { + // TODO pass package in. setPackage("com.example.leakcanary") } diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/data/HeapRepository.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/data/HeapRepository.kt index 9b47c96c15..b3e493f83e 100644 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/data/HeapRepository.kt +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/data/HeapRepository.kt @@ -88,7 +88,7 @@ class HeapRepository @Inject constructor( """.trimMargin(), parameters = 1, binders = { - bindString(1, signature) + bindString(0, signature) } ) } diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/ClientAppAnalysisScreen.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/ClientAppAnalysisScreen.kt index 72d87c46ee..42988c6f5d 100644 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/ClientAppAnalysisScreen.kt +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/ClientAppAnalysisScreen.kt @@ -36,10 +36,12 @@ import org.leakcanary.data.HeapRepository import org.leakcanary.screens.ClientAppAnalysisState.Loading import org.leakcanary.screens.ClientAppAnalysisState.Success import org.leakcanary.screens.Destination.ClientAppAnalysisDestination +import org.leakcanary.screens.Destination.TreeMapDestination import org.leakcanary.screens.HeaderCardLink.EXPLORE_HPROF import org.leakcanary.screens.HeaderCardLink.PRINT import org.leakcanary.screens.HeaderCardLink.SHARE_ANALYSIS import org.leakcanary.screens.HeaderCardLink.SHARE_HPROF +import org.leakcanary.screens.HeaderCardLink.SHOW_TREE_MAP import org.leakcanary.util.LeakTraceWrapper import org.leakcanary.util.Sharer import shark.HeapAnalysisSuccess @@ -91,6 +93,9 @@ class ClientAppAnalysisViewModel @Inject constructor( SharkLog.d { "\u200B\n" + LeakTraceWrapper.wrap(heapAnalysis.toString(), 120) } } SHARE_HPROF -> TODO() + SHOW_TREE_MAP -> { + navigator.goTo(TreeMapDestination(heapAnalysis.heapDumpFile)) + } } } @@ -105,7 +110,8 @@ enum class HeaderCardLink { EXPLORE_HPROF, SHARE_ANALYSIS, PRINT, - SHARE_HPROF + SHARE_HPROF, + SHOW_TREE_MAP } @Composable fun ClientAppAnalysisScreen(viewModel: ClientAppAnalysisViewModel = viewModel()) { @@ -145,6 +151,10 @@ enum class HeaderCardLink { appendLink("Heap Dump file", SHARE_HPROF) append("\n\n") } + // TODO check we can connect to app + append("Show ") + appendLink("Tree Map", SHOW_TREE_MAP) + append("\n\n") // TODO this should be an expendable item row instead. /* val dumpDurationMillis = @@ -170,11 +180,12 @@ enum class HeaderCardLink { ClickableText(text = annotatedString, style = MaterialTheme.typography.bodySmall, onClick = { offset -> - val link = HeaderCardLink.valueOf( - annotatedString.getStringAnnotations(tag = "link", start = offset, end = offset) - .single().item - ) - viewModel.onHeaderCardLinkClicked(heapAnalysis, link) + + val annotations = annotatedString.getStringAnnotations(tag = "link", start = offset, end = offset) + if (annotations.size == 1) { + val link = HeaderCardLink.valueOf(annotations.single().item) + viewModel.onHeaderCardLinkClicked(heapAnalysis, link) + } }) } } diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/Destination.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/Destination.kt index 7af39c7485..e583a45696 100644 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/Destination.kt +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/Destination.kt @@ -1,6 +1,7 @@ package org.leakcanary.screens import android.os.Parcelable +import java.io.File import kotlinx.parcelize.Parcelize sealed class Destination(val title: String) : Parcelable { @@ -17,6 +18,9 @@ sealed class Destination(val title: String) : Parcelable { @Parcelize class ClientAppAnalysisDestination(val analysisId: Long) : Destination("Analysis") + @Parcelize + class TreeMapDestination(val heapDump: File) : Destination("TreeMap") + @Parcelize object LeaksDestination : Destination("Leaks") diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/ScreenHost.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/ScreenHost.kt index fe8e6e6d91..5409913d99 100644 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/ScreenHost.kt +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/ScreenHost.kt @@ -27,6 +27,7 @@ import org.leakcanary.screens.Destination.ClientAppAnalysisDestination import org.leakcanary.screens.Destination.ClientAppsDestination import org.leakcanary.screens.Destination.LeakDestination import org.leakcanary.screens.Destination.LeaksDestination +import org.leakcanary.screens.Destination.TreeMapDestination // TODO Handle intents @OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @@ -78,6 +79,7 @@ fun ScreenHost(backStack: BackStackViewModel = viewModel()) { is ClientAppAnalysisDestination -> ClientAppAnalysisScreen() ClientAppsDestination -> ClientAppsScreen() is LeakDestination -> LeakScreen() + is TreeMapDestination -> TreeMapScreen() LeaksDestination -> TODO() } } diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreeMapScreen.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreeMapScreen.kt new file mode 100644 index 0000000000..d0d3575c26 --- /dev/null +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreeMapScreen.kt @@ -0,0 +1,210 @@ +package org.leakcanary.screens + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.rememberTextMeasurer +import androidx.compose.ui.tooling.preview.Preview +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.viewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.File +import java.util.EnumSet +import java.util.Random +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import org.leakcanary.WhileSubscribedOrRetained +import org.leakcanary.screens.Destination.TreeMapDestination +import org.leakcanary.screens.TreeMapState.Loading +import org.leakcanary.screens.TreeMapState.Success +import org.leakcanary.screens.TreemapLayout.NodeValue +import org.leakcanary.service.TreeMapFetcher +import shark.AndroidReferenceMatchers +import shark.HprofHeapGraph.Companion.openHeapGraph +import shark.IgnoredReferenceMatcher +import shark.ObjectDominators +import shark.ObjectDominators.OfflineDominatorNode +import shark.ValueHolder + +sealed interface TreeMapState { + object Loading : TreeMapState + class Success(val dominators: Map) : TreeMapState +} + +@HiltViewModel +class TreeMapViewModel @Inject constructor( + // TODO Add a thing that can make IPC calls + navigator: Navigator, + private val treeMapFetcher: TreeMapFetcher, +) : ViewModel() { + + val state = + navigator.filterDestination() + .flatMapLatest { destination -> + stateStream(destination.heapDump) + }.stateIn( + viewModelScope, started = WhileSubscribedOrRetained, initialValue = Loading + ) + + private fun stateStream(heapDump: File) = flow { + // TODO Dynamic package + val result = withContext(Dispatchers.IO) { + heapDump.openHeapGraph().use { heapGraph -> + val weakAndFinalizerRefs = EnumSet.of( + AndroidReferenceMatchers.REFERENCES, AndroidReferenceMatchers.FINALIZER_WATCHDOG_DAEMON + ) + val ignoredRefs = + AndroidReferenceMatchers.buildKnownReferences(weakAndFinalizerRefs).map { matcher -> + matcher as IgnoredReferenceMatcher + } + + ObjectDominators().buildOfflineDominatorTree(heapGraph, ignoredRefs) + } + } + // val result = emptyMap() + emit(Success(result)) + } +} + +@OptIn(ExperimentalTextApi::class) +@Composable fun TreeMapScreen(viewModel: TreeMapViewModel = viewModel()) { + val stateProp by viewModel.state.collectAsState() + + when (val state = stateProp) { + is Loading -> { + Text("Loading...") + } + + is Success -> { + val dominators = state.dominators + val textMeasure = rememberTextMeasurer() + val root = ValueHolder.NULL_REFERENCE + val treemapInput = DominatorNodeMapper( + dominators = dominators, + maxDepth = 1, + minSize = 10000 + ).mapToTreemapInput(root) + Treemap(treemapInput) { dominators.getValue(it).name } + } + } +} + +class DominatorNodeMapper( + private val dominators: Map, + private val maxDepth: Int, + private val minSize: Int +) { + + fun mapToTreemapInput( + objectId: Long, + depth: Int = 0 + ): NodeValue { + val offlineNode = dominators.getValue(objectId) + val node = offlineNode.node + val children = if (depth > maxDepth) { + emptyList() + } else { + node.dominatedObjectIds.mapNotNull { dominatedObjectId -> + val node = dominators.getValue(dominatedObjectId).node + // Ignoring small nodes. + if ((node.shallowSize + node.retainedSize) >= minSize) { + mapToTreemapInput(dominatedObjectId, depth + 1) + } else { + null + } + } + } + val value = if (objectId == ValueHolder.NULL_REFERENCE) { + // Root is a forest, retained size isn't computed. + node.dominatedObjectIds.sumOf { dominatedObjectId -> + val childNode = dominators.getValue(dominatedObjectId).node + childNode.shallowSize + childNode.retainedSize + } + } else { + node.shallowSize + node.retainedSize + } + return NodeValue( + value = value, + content = objectId, + children = children + ) + } +} + +@Composable +@Preview +fun TreemapPreview() { + val root = NodeValue( + 25, + "Root", + listOf( + NodeValue(10, "A", listOf( + NodeValue(5, "A1", emptyList()), + NodeValue(5, "A2", emptyList()) + )), + NodeValue(5, "B", emptyList()), + NodeValue(5, "C", emptyList()), + NodeValue(5, "D", emptyList()), + ) + ) + Treemap(root, text = { it }) +} + +@OptIn(ExperimentalTextApi::class) +@Composable +fun Treemap(root: NodeValue, text: (T) -> String) { + // TODO Colors should be a gradient related to depth + // Try colors from https://observablehq.com/@d3/nested-treemap + // Also colors as related to the node. + // d3.scaleSequential([8, 0], d3.interpolateMagma) + val colors = listOf( + Color(169, 64, 119), + Color(206, 88, 98), + Color(237, 143, 106), + Color(253, 253, 198), + ) + val textMeasure = rememberTextMeasurer() + + Canvas(modifier = Modifier.fillMaxSize()) { + + val layout = TreemapLayout( + // TODO This isn't working + paddingInner = { 0f }, + paddingLeft = { 32f }, + paddingTop = { 64f }, + paddingRight = { 32f }, + paddingBottom = { 32f } + + ).layout(root, size) + + layout.depthFirstTraversal { node -> + drawRect( + colors[node.depth], + topLeft = node.topLeft, + size = node.size + ) + // TODO Figure out what's up with negative numbers + // java.lang.IllegalArgumentException: maxHeight(-1233) must be >= than minHeight(0) + // if (node.x0 > 0 && node.y0 > 0) { + drawText( + textMeasurer = textMeasure, + text = text(node.content), + topLeft = node.topLeft + ) + // } + } + } +} + diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreemapLayout.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreemapLayout.kt new file mode 100644 index 0000000000..a3b3ce9af8 --- /dev/null +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreemapLayout.kt @@ -0,0 +1,294 @@ +package org.leakcanary.screens + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import kotlin.math.max +import kotlin.math.sqrt +import org.leakcanary.screens.TreemapLayout.NodeLayout + +/** + * Based on https://github.com/d3/d3-hierarchy Treemap implementation. + * + */ +class TreemapLayout( + private val paddingInner: (NodeLayout) -> Float = { 0f }, + private val paddingLeft: (NodeLayout) -> Float = { 0f }, + private val paddingTop: (NodeLayout) -> Float = { 0f }, + private val paddingRight: (NodeLayout) -> Float = { 0f }, + private val paddingBottom: (NodeLayout) -> Float = { 0f } +) { + + data class NodeValue( + // TODO Float + val value: Int, + val content: T, + val children: List> + ) + + interface NodeLayout { + val value: Int + val content: T + val depth: Int + val children: List> + val topLeft: Offset + val size: Size + } + + fun layout( + root: NodeValue, + size: Size + ): NodeLayout { + val rootLayout = root.mapNode() + rootLayout.x0 = 0f + rootLayout.y0 = 0f + rootLayout.x1 = size.width + rootLayout.y1 = size.height + val paddingStack = mutableListOf(0f) + rootLayout.depthFirstTraversal { node -> + positionNode(node, paddingStack) + } + return rootLayout + } + + private fun NodeValue.mapNode(depth: Int = 0): InternalNodeLayout { + return InternalNodeLayout( + value = value, + content = content, + depth = depth, + children = children.map { it.mapNode(depth + 1) } + ) + } + + private fun positionNode( + node: InternalNodeLayout, + paddingStack: MutableList + ) { + var p = paddingStack[node.depth] + var x0 = node.x0 + p + var y0 = node.y0 + p + var x1 = node.x1 - p + var y1 = node.y1 - p + if (x1 < x0) { + x1 = (x0 + x1) / 2 + x0 = x1 + } + if (y1 < y0) { + y1 = (y0 + y1) / 2 + y0 = y1 + } + if (node.children.isNotEmpty()) { + // TODO Debug with examples to check that padding is right. + val halfPaddingInner = paddingInner(node) / 2 + val childDepth = node.depth + 1 + if (childDepth < paddingStack.size) { + paddingStack[childDepth] = halfPaddingInner + } else { + paddingStack += halfPaddingInner + } + p = halfPaddingInner + x0 += paddingLeft(node) - p + y0 += paddingTop(node) - p + x1 -= paddingRight(node) - p + y1 -= paddingBottom(node) - p + if (x1 < x0) { + x1 = (x0 + x1) / 2 + x0 = x1 + } + if (y1 < y0) { + y1 = (y0 + y1) / 2 + y0 = y1 + } + squarifyRatio(phi, node, x0, y0, x1, y1) + } + } + + private data class InternalNodeLayout( + override val value: Int, + override val content: T, + override val depth: Int, + override val children: List> + ) : NodeLayout { + + var x0 = 0f + var y0 = 0f + var x1 = 0f + var y1 = 0f + + override val topLeft: Offset + get() = Offset(x0, y0) + override val size: Size + get() = Size(x1 - x0, y1 - y0) + } + + private class Row( + val value: Int, + val children: List> + ) + + private fun squarifyRatio( + ratio: Float, + parent: InternalNodeLayout<*>, + x0Start: Float, + y0Start: Float, + x1Start: Float, + y1Start: Float + ) { + // TODO Check out resquarity and try that? + val nodes = parent.children + + var value = parent.value + + var x0 = x0Start + var y0 = y0Start + var x1 = x1Start + var y1 = y1Start + + var i0 = 0 + var i1 = 0 + val n = nodes.size + while (i0 < n) { + val dx = x1 - x0 + val dy = y1 - y0 + + // Find the next non-empty node. + var sumValue: Int + do { + sumValue = nodes[i1].value + i1++ + } while (sumValue == 0 && i1 < n) + var minValue = sumValue + var maxValue = sumValue + val alpha = max(dy / dx, dx / dy) / (value * ratio) + var beta = sumValue * sumValue * alpha + var minRatio = max(maxValue / beta, beta / minValue) + + // Keep adding nodes while the aspect ratio maintains or improves. + while (i1 < n) { + val nodeValue = nodes[i1].value + sumValue += nodeValue + if (nodeValue < minValue) minValue = nodeValue + if (nodeValue > maxValue) maxValue = nodeValue + beta = sumValue * sumValue * alpha + val newRatio = max(maxValue / beta, beta / minValue) + if (newRatio > minRatio) { + sumValue -= nodeValue + break + } + minRatio = newRatio + i1++ + } + + // Position and record the row orientation. + val row = Row( + value = sumValue, + children = nodes.slice(i0 until i1) + ) + + if (dx < dy) { + val initialY0 = y0 + val lastY = if (value > 0) { + y0 += dy * sumValue / value + y0 + } else { + y1 + } + treemapDice(row, x0, initialY0, x1, lastY) + } else { + val initialX0 = x0 + val lastX = if (value > 0) { + x0 += dx * sumValue / value + x0 + } else { + x1 + } + treemapSlice(row, initialX0, y0, lastX, y1) + } + value -= sumValue + i0 = i1 + } + } + + private fun treemapSlice( + parent: Row, + x0Start: Float, + y0Start: Float, + x1Start: Float, + y1Start: Float + ) { + val nodes = parent.children + + val k = if (parent.value > 0) { + (y1Start - y0Start) / parent.value + } else { + 0f + } + + var y0 = y0Start + + var i = -1 + val n = nodes.size + while (++i < n) { + val node = nodes[i] + node.x0 = x0Start + node.x1 = x1Start + node.y0 = y0 + y0 += node.value.toFloat() * k + node.y1 = y0 + } + } + + private fun treemapDice( + parent: Row, + x0Start: Float, + y0Start: Float, + x1Start: Float, + y1Start: Float + ) { + val nodes = parent.children + + val n = nodes.size + val k = if (parent.value > 0) { + (x1Start - x0Start) / parent.value + } else { + 0f + } + + var i = -1 + var x0 = x0Start + while (++i < n) { + val node = nodes[i] + node.y0 = y0Start + node.y1 = y1Start + node.x0 = x0 + x0 += node.value.toFloat() * k + node.x1 = x0 + } + } + + companion object { + // Golden ratio + val phi = (1 + sqrt(5f)) / 2 + } +} + +/** + * Invokes [callback] for node and each descendant in pre-order traversal, such that a given node + * is only visited after all of its ancestors have already been visited. [callback] is passed the + * current descendant, the zero-based traversal index, and this node. + */ +inline fun > N.depthFirstTraversal(callback: (N) -> Unit) { + var node = this + val nodes = ArrayDeque() + nodes += node + while (nodes.isNotEmpty()) { + node = nodes.removeLast() + callback(node) + val children = node.children + if (children.isNotEmpty()) { + for (child in children.reversed()) { + @Suppress("UNCHECKED_CAST") + nodes.addLast(child as N) + } + } + } +} diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/LeakUiAppService.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/LeakUiAppService.kt index 0954ff18a6..4271a8a8d9 100644 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/LeakUiAppService.kt +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/LeakUiAppService.kt @@ -2,13 +2,20 @@ package org.leakcanary.service import android.app.Service import android.content.Intent +import android.net.Uri import android.os.Binder import android.os.IBinder import dagger.hilt.android.AndroidEntryPoint +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream import javax.inject.Inject import org.leakcanary.data.HeapRepository import org.leakcanary.internal.LeakUiApp import org.leakcanary.internal.ParcelableHeapAnalysis +import shark.HeapAnalysisFailure +import shark.HeapAnalysisSuccess +import shark.SharkLog @AndroidEntryPoint class LeakUiAppService : Service() { @@ -19,14 +26,52 @@ class LeakUiAppService : Service() { // manually clearing out the stub reference to the service. private val binder = object : LeakUiApp.Stub() { - override fun sendHeapAnalysis(heapAnalysis: ParcelableHeapAnalysis) { + override fun sendHeapAnalysis( + heapAnalysis: ParcelableHeapAnalysis, + heapDumpUri: Uri + ) { + val heapDumpDir = File(filesDir, "heapdumps") + if (!heapDumpDir.exists()) { + check(heapDumpDir.mkdirs()) { + "Failed to create directory $heapDumpDir" + } + } + + val sourceHeapAnalysis = heapAnalysis.wrapped + + val destination = File(heapDumpDir, sourceHeapAnalysis.heapDumpFile.name) + + val updatedHeapAnalysis = when (sourceHeapAnalysis) { + is HeapAnalysisSuccess -> { + sourceHeapAnalysis.copy(heapDumpFile = destination) + } + + is HeapAnalysisFailure -> { + sourceHeapAnalysis.copy(heapDumpFile = destination) + } + } + + val parcelFileDescriptor = contentResolver.openFileDescriptor(heapDumpUri, "r") + if (parcelFileDescriptor != null) { + parcelFileDescriptor.use { + FileInputStream(it.fileDescriptor).use { inputStream -> + FileOutputStream(destination).use { fos -> + val sourceChannel = inputStream.channel + sourceChannel.transferTo(0, sourceChannel.size(), fos.channel) + } + } + } + } else { + SharkLog.d { "ContentProvider crashed, skipping copy of $heapDumpUri" } + } + // TODO Use sendHeapAnalysis as a trigger to check if this is the first ever linking and // if we need to import any past analysis. This should start background work that // asks the app for all analysis between 2 times: last analysis that we know and this // analysis time. val callerPackageName = packageManager.getNameForUid(Binder.getCallingUid())!! // TODO maybe return an intent for notification? - heapRepository.insertHeapAnalysis(callerPackageName, heapAnalysis.wrapped) + heapRepository.insertHeapAnalysis(callerPackageName, updatedHeapAnalysis) } } diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/TreeMapFetcher.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/TreeMapFetcher.kt new file mode 100644 index 0000000000..69f2f59ca3 --- /dev/null +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/TreeMapFetcher.kt @@ -0,0 +1,92 @@ +package org.leakcanary.service + +import android.app.Application +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.os.IInterface +import java.io.File +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.leakcanary.internal.HeapDataRepository +import shark.Dominators + +/** + * A one shot wrapper around [HeapDataRepository] that handles service lifecycle, and retrieves the + * data, all from a suspend function. This isn't meant to stick around, more a place to play with + * all the components at hand. + */ +class TreeMapFetcher @Inject constructor( + private val application: Application +) { + + suspend fun fetchTreeMap( + packageName: String, + heapDumpFile: File + ): Dominators { + return withContext(Dispatchers.IO) { + val intent = Intent("org.leakcanary.internal.HeapDataRepositoryService.BIND") + .apply { + setPackage(packageName) + } + + val (service, connection) = application.connectToService(intent) { + HeapDataRepository.Stub.asInterface(it) + } + try { + service.sayHi(heapDumpFile.absolutePath).wrapped + } finally { + application.unbindService(connection) + } + } + } + + suspend inline fun Context.connectToService( + intent: Intent, + crossinline asInterface: (IBinder) -> B + ): Pair = suspendCoroutine { continuation -> + val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName) { + // TODO Is this right? we should handle this. + continuation.resumeWithException(RuntimeException("Service Disconnected")) + } + + override fun onServiceConnected( + name: ComponentName, + binder: IBinder + ) { + continuation.resume(asInterface(binder) to this) + } + } + + // TODO What handle error case. + try { + val bringingServiceUp = + applicationContext.bindService( + intent, + connection, + Context.BIND_AUTO_CREATE + ) + // TODO If bringingServiceUp is false, must call unbindService + // false if the system couldn't find the service or if your client doesn't have permission + // to bind to it. Documentation says the same thing for throwing SecurityException though. + // TODO If this is false we don't have access to the service. Adding queries in manifest fixes it + // but that's not the right solution. + // This works again once we have the source app make a call. Unclear when this stops working. + // Maybe we need a way to flush like starting a transparent activity that just establishes + // a back service call which then makes this authorized. + check(bringingServiceUp) { + "Service not brought up" + } + } catch (e: SecurityException) { + // TODO must call unbindService + throw e + } + } +} diff --git a/shark/shark/src/main/java/shark/Dominators.kt b/shark/shark/src/main/java/shark/Dominators.kt new file mode 100644 index 0000000000..ace85cb754 --- /dev/null +++ b/shark/shark/src/main/java/shark/Dominators.kt @@ -0,0 +1,6 @@ +package shark + +import java.io.Serializable +import shark.ObjectDominators.DominatorNode + +class Dominators(val dominatorNodes: Map) : Serializable diff --git a/shark/shark/src/main/java/shark/ObjectDominators.kt b/shark/shark/src/main/java/shark/ObjectDominators.kt index e467bc51f8..3424f71785 100644 --- a/shark/shark/src/main/java/shark/ObjectDominators.kt +++ b/shark/shark/src/main/java/shark/ObjectDominators.kt @@ -1,5 +1,6 @@ package shark +import java.io.Serializable import shark.GcRoot.ThreadObject import shark.HeapObject.HeapClass import shark.HeapObject.HeapInstance @@ -24,7 +25,12 @@ class ObjectDominators { val retainedSize: Int, val retainedCount: Int, val dominatedObjectIds: List - ) + ) : Serializable + + data class OfflineDominatorNode( + val node: DominatorNode, + val name: String + ) : Serializable fun renderDominatorTree( graph: HeapGraph, @@ -131,7 +137,26 @@ class ObjectDominators { } } - private fun buildDominatorTree( + fun buildOfflineDominatorTree( + graph: HeapGraph, + ignoredRefs: List + ): Map { + return buildDominatorTree(graph, ignoredRefs).mapValues { (objectId, node) -> + val name = if (objectId == ValueHolder.NULL_REFERENCE) { + "root" + } else when (val heapObject = graph.findObjectById(objectId)) { + is HeapClass -> "class ${heapObject.name}" + is HeapInstance -> heapObject.instanceClassName + is HeapObjectArray -> heapObject.arrayClassName + is HeapPrimitiveArray -> heapObject.arrayClassName + } + OfflineDominatorNode( + node, name + ) + } + } + + fun buildDominatorTree( graph: HeapGraph, ignoredRefs: List ): Map { From f0281805598073519381caf70e68b8200bc9f258 Mon Sep 17 00:00:00 2001 From: Pierre-Yves Ricau Date: Wed, 31 May 2023 15:04:55 -0700 Subject: [PATCH 2/2] Remove AIDL, focus on sending hprof --- config/detekt-config.yml | 10 +- .../api/leakcanary-android-core.api | 17 ++++ .../activity/screen/HeapDumpRenderer.kt | 7 +- .../api/leakcanary-app-aidl.api | 23 +---- .../internal/HeapDataRepository.aidl | 7 -- .../internal/ParcelableDominators.kt | 34 ------- .../src/main/AndroidManifest.xml | 13 --- .../internal/HeapDataRepositoryService.kt | 53 ----------- .../leakcanary/internal/LeakUiAppClient.kt | 7 +- .../src/main/AndroidManifest.xml | 7 -- .../main/java/org/leakcanary/MainActivity.kt | 48 ---------- .../org/leakcanary/screens/TreeMapScreen.kt | 81 ++++++++++++---- .../org/leakcanary/service/TreeMapFetcher.kt | 92 ------------------- .../src/main/java/shark/Neo4JCommand.kt | 2 +- .../java/shark/internal/HprofInMemoryIndex.kt | 16 ---- .../src/main/java/shark/HprofWriter.kt | 42 ++++----- .../main/java/shark/StreamingHprofReader.kt | 2 +- .../shark/StreamingRecordReaderAdapter.kt | 2 +- shark/shark/api/shark.api | 22 ++++- 19 files changed, 137 insertions(+), 348 deletions(-) delete mode 100644 leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/HeapDataRepository.aidl delete mode 100644 leakcanary/leakcanary-app-aidl/src/main/java/org/leakcanary/internal/ParcelableDominators.kt delete mode 100644 leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/HeapDataRepositoryService.kt delete mode 100644 leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/TreeMapFetcher.kt diff --git a/config/detekt-config.yml b/config/detekt-config.yml index 00852fd0ef..9123704eda 100644 --- a/config/detekt-config.yml +++ b/config/detekt-config.yml @@ -69,11 +69,11 @@ complexity: # #LeakCanary - increased from 60 to 90 # active: true # threshold: 90 - LongParameterList: - #LeakCanary - enabled ignore - active: true - threshold: 6 - ignoreDefaultParameters: true + # LongParameterList: + # #LeakCanary - enabled ignore + # active: true + # threshold: 6 + # ignoreDefaultParameters: true MethodOverloading: active: false threshold: 6 diff --git a/leakcanary/leakcanary-android-core/api/leakcanary-android-core.api b/leakcanary/leakcanary-android-core/api/leakcanary-android-core.api index c160056f4a..d5314d35f2 100644 --- a/leakcanary/leakcanary-android-core/api/leakcanary-android-core.api +++ b/leakcanary/leakcanary-android-core/api/leakcanary-android-core.api @@ -167,3 +167,20 @@ public final class leakcanary/WorkManagerHeapAnalyzer : leakcanary/EventListener public fun onEvent (Lleakcanary/EventListener$Event;)V } +public final class leakcanary/internal/LeakCanaryFileProvider : android/content/ContentProvider { + public static final field Companion Lleakcanary/internal/LeakCanaryFileProvider$Companion; + public fun ()V + public fun attachInfo (Landroid/content/Context;Landroid/content/pm/ProviderInfo;)V + public fun delete (Landroid/net/Uri;Ljava/lang/String;[Ljava/lang/String;)I + public fun getType (Landroid/net/Uri;)Ljava/lang/String; + public fun insert (Landroid/net/Uri;Landroid/content/ContentValues;)Landroid/net/Uri; + public fun onCreate ()Z + public fun openFile (Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor; + public fun query (Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor; + public fun update (Landroid/net/Uri;Landroid/content/ContentValues;Ljava/lang/String;[Ljava/lang/String;)I +} + +public final class leakcanary/internal/LeakCanaryFileProvider$Companion { + public final fun getUriForFile (Landroid/content/Context;Ljava/lang/String;Ljava/io/File;)Landroid/net/Uri; +} + diff --git a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/screen/HeapDumpRenderer.kt b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/screen/HeapDumpRenderer.kt index 56d9b7e4df..700fb7185a 100644 --- a/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/screen/HeapDumpRenderer.kt +++ b/leakcanary/leakcanary-android-core/src/main/java/leakcanary/internal/activity/screen/HeapDumpRenderer.kt @@ -11,6 +11,9 @@ import android.graphics.Paint.Style.FILL import android.graphics.Paint.Style.STROKE import android.graphics.Rect import com.squareup.leakcanary.core.R +import java.io.File +import kotlin.math.ceil +import kotlin.math.max import leakcanary.internal.navigation.getColorCompat import shark.HprofRecord import shark.HprofRecord.HeapDumpEndRecord @@ -32,9 +35,6 @@ import shark.HprofRecord.StackTraceRecord import shark.HprofRecord.StringRecord import shark.StreamingHprofReader import shark.StreamingRecordReaderAdapter.Companion.asStreamingRecordReader -import java.io.File -import kotlin.math.ceil -import kotlin.math.max internal object HeapDumpRenderer { @@ -48,7 +48,6 @@ internal object HeapDumpRenderer { get() = this * density } - @Suppress("LongMethod") fun render( context: Context, heapDumpFile: File, diff --git a/leakcanary/leakcanary-app-aidl/api/leakcanary-app-aidl.api b/leakcanary/leakcanary-app-aidl/api/leakcanary-app-aidl.api index 065b0d79b6..ebe0305ac3 100644 --- a/leakcanary/leakcanary-app-aidl/api/leakcanary-app-aidl.api +++ b/leakcanary/leakcanary-app-aidl/api/leakcanary-app-aidl.api @@ -1,30 +1,11 @@ -public abstract interface class org/leakcanary/internal/HeapDataRepository : android/os/IInterface { - public abstract fun sayHi ()V -} - -public class org/leakcanary/internal/HeapDataRepository$Default : org/leakcanary/internal/HeapDataRepository { - public fun ()V - public fun asBinder ()Landroid/os/IBinder; - public fun sayHi ()V -} - -public abstract class org/leakcanary/internal/HeapDataRepository$Stub : android/os/Binder, org/leakcanary/internal/HeapDataRepository { - public fun ()V - public fun asBinder ()Landroid/os/IBinder; - public static fun asInterface (Landroid/os/IBinder;)Lorg/leakcanary/internal/HeapDataRepository; - public static fun getDefaultImpl ()Lorg/leakcanary/internal/HeapDataRepository; - public fun onTransact (ILandroid/os/Parcel;Landroid/os/Parcel;I)Z - public static fun setDefaultImpl (Lorg/leakcanary/internal/HeapDataRepository;)Z -} - public abstract interface class org/leakcanary/internal/LeakUiApp : android/os/IInterface { - public abstract fun sendHeapAnalysis (Lorg/leakcanary/internal/ParcelableHeapAnalysis;)V + public abstract fun sendHeapAnalysis (Lorg/leakcanary/internal/ParcelableHeapAnalysis;Landroid/net/Uri;)V } public class org/leakcanary/internal/LeakUiApp$Default : org/leakcanary/internal/LeakUiApp { public fun ()V public fun asBinder ()Landroid/os/IBinder; - public fun sendHeapAnalysis (Lorg/leakcanary/internal/ParcelableHeapAnalysis;)V + public fun sendHeapAnalysis (Lorg/leakcanary/internal/ParcelableHeapAnalysis;Landroid/net/Uri;)V } public abstract class org/leakcanary/internal/LeakUiApp$Stub : android/os/Binder, org/leakcanary/internal/LeakUiApp { diff --git a/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/HeapDataRepository.aidl b/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/HeapDataRepository.aidl deleted file mode 100644 index bbf6589fab..0000000000 --- a/leakcanary/leakcanary-app-aidl/src/main/aidl/org/leakcanary/internal/HeapDataRepository.aidl +++ /dev/null @@ -1,7 +0,0 @@ -package org.leakcanary.internal; - -parcelable ParcelableDominators; - -interface HeapDataRepository { - ParcelableDominators sayHi(String heapDumpFilePath); -} diff --git a/leakcanary/leakcanary-app-aidl/src/main/java/org/leakcanary/internal/ParcelableDominators.kt b/leakcanary/leakcanary-app-aidl/src/main/java/org/leakcanary/internal/ParcelableDominators.kt deleted file mode 100644 index 07016bb660..0000000000 --- a/leakcanary/leakcanary-app-aidl/src/main/java/org/leakcanary/internal/ParcelableDominators.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.leakcanary.internal - -import android.os.Parcel -import android.os.Parcelable -import shark.Dominators -import shark.HeapAnalysis - -class ParcelableDominators(val wrapped: Dominators) : Parcelable { - - private constructor(source: Parcel) : this(source.readSerializable() as Dominators) - - override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeSerializable(wrapped) - println("FOO WRITE ${dest.dataSize()} $dest") - } - - override fun describeContents() = 0 - - companion object { - @Suppress("UNCHECKED_CAST") - @JvmField val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(source: Parcel): ParcelableDominators { - println("FOO READ ${source.dataSize()} $source") - return ParcelableDominators(source) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - - fun Dominators.asParcelable(): ParcelableDominators = ParcelableDominators(this) - } -} diff --git a/leakcanary/leakcanary-app-service/src/main/AndroidManifest.xml b/leakcanary/leakcanary-app-service/src/main/AndroidManifest.xml index 15a9ea42cb..96263a62ad 100644 --- a/leakcanary/leakcanary-app-service/src/main/AndroidManifest.xml +++ b/leakcanary/leakcanary-app-service/src/main/AndroidManifest.xml @@ -1,19 +1,6 @@ - - - - - - - - - - - diff --git a/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/HeapDataRepositoryService.kt b/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/HeapDataRepositoryService.kt deleted file mode 100644 index 4098621f20..0000000000 --- a/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/HeapDataRepositoryService.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.leakcanary.internal - -import android.app.Service -import android.content.Intent -import android.os.IBinder -import java.io.File -import java.util.EnumSet -import org.leakcanary.internal.ParcelableDominators.Companion.asParcelable -import shark.AndroidReferenceMatchers -import shark.Dominators -import shark.HprofHeapGraph.Companion.openHeapGraph -import shark.IgnoredReferenceMatcher -import shark.ObjectDominators - -internal class HeapDataRepositoryService : Service() { - - // TODO Stubs can be longer lived than the outer service, handle - // manually clearing out the stub reference to the service. - private val binder = object : HeapDataRepository.Stub() { - override fun sayHi(heapDumpFilePath: String): ParcelableDominators { - try { - return File(heapDumpFilePath).openHeapGraph().use { heapGraph -> - val weakAndFinalizerRefs = EnumSet.of( - AndroidReferenceMatchers.REFERENCES, AndroidReferenceMatchers.FINALIZER_WATCHDOG_DAEMON - ) - val ignoredRefs = - AndroidReferenceMatchers.buildKnownReferences(weakAndFinalizerRefs).map { matcher -> - matcher as IgnoredReferenceMatcher - } - - // TODO Move offline part here. - // Also trying out without the names to see how big this is. - // Didn't work => need to expose an object that can be queried through IPC? - // That means keeping this structure in memory probably? - val result = Dominators( - ObjectDominators().buildDominatorTree(heapGraph, ignoredRefs) - ).asParcelable() - result - } - } catch (throwable: Throwable) { - // TODO cleanup. But we do need the right stacktrace here. - throwable.printStackTrace() - throw throwable - } - } - } - - override fun onBind(intent: Intent): IBinder { - // TODO Check signature of the calling app. - // TODO Return null if we can't handle the caller's version - return binder - } -} diff --git a/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/LeakUiAppClient.kt b/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/LeakUiAppClient.kt index 92bbf15f65..6a841aedc7 100644 --- a/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/LeakUiAppClient.kt +++ b/leakcanary/leakcanary-app-service/src/main/java/org/leakcanary/internal/LeakUiAppClient.kt @@ -25,10 +25,9 @@ import shark.SharkLog * We can ensure apps have the LeakCanary app in their manifest, however the other way round * isn't possible, we don't know in advance which apps we'll talk to. * - * One of the automatic cases is "Any app that starts or binds to a service in your app". - * - * So we'll have apps poke the LeakCanary app by binding, which then gives it permission to bind - * a service back. + * One of the automatic cases is "Any app that starts or binds to a service in your app", so if we + * ever need the LeakCanary app to talk back we could have apps first poke the LeakCanary app by + * binding, which then gives it permission to bind a service back. * * On AIDL backward compatibility: HeapAnalysis is Serializable so we need to ensure compatibility * via Serializable. diff --git a/leakcanary/leakcanary-app/src/main/AndroidManifest.xml b/leakcanary/leakcanary-app/src/main/AndroidManifest.xml index a9d0fe96c1..4af98d278f 100644 --- a/leakcanary/leakcanary-app/src/main/AndroidManifest.xml +++ b/leakcanary/leakcanary-app/src/main/AndroidManifest.xml @@ -42,11 +42,4 @@ - - - - - - - diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/MainActivity.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/MainActivity.kt index b1e63a0411..6298a22bb5 100644 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/MainActivity.kt +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/MainActivity.kt @@ -1,55 +1,21 @@ package org.leakcanary -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection import android.os.Bundle -import android.os.IBinder import androidx.activity.ComponentActivity import androidx.activity.compose.setContent -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.ViewModelProvider import dagger.hilt.android.AndroidEntryPoint -import org.leakcanary.internal.HeapDataRepository import org.leakcanary.screens.BackStackViewModel import org.leakcanary.screens.ScreenHost import org.leakcanary.ui.theme.MyApplicationTheme -import shark.HeapAnalysis -import shark.SharkLog @AndroidEntryPoint class MainActivity : ComponentActivity() { - private var heapDataRepository: HeapDataRepository? = null - - private var connected by mutableStateOf(false) - - private val serviceConnection = object : ServiceConnection { - override fun onServiceConnected(name: ComponentName, service: IBinder) { - heapDataRepository = HeapDataRepository.Stub.asInterface(service) - connected = true - } - - override fun onServiceDisconnected(name: ComponentName) { - heapDataRepository = null - connected = false - } - } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -57,15 +23,6 @@ class MainActivity : ComponentActivity() { // graph so that BackStackHolder will always have a ref. ViewModelProvider(this)[BackStackViewModel::class.java] - val intent = Intent("org.leakcanary.internal.HeapDataRepositoryService.BIND") - .apply { - // TODO pass package in. - setPackage("com.example.leakcanary") - } - - val bringingServiceUp = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) - SharkLog.d { "HeapDataRepositoryService up=$bringingServiceUp" } - setContent { MyApplicationTheme { Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { @@ -74,9 +31,4 @@ class MainActivity : ComponentActivity() { } } } - - override fun onDestroy() { - super.onDestroy() - unbindService(serviceConnection) - } } diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreeMapScreen.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreeMapScreen.kt index d0d3575c26..65a543f9ef 100644 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreeMapScreen.kt +++ b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/screens/TreeMapScreen.kt @@ -7,7 +7,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.drawText import androidx.compose.ui.text.rememberTextMeasurer @@ -18,19 +21,18 @@ import androidx.lifecycle.viewmodel.compose.viewModel import dagger.hilt.android.lifecycle.HiltViewModel import java.io.File import java.util.EnumSet -import java.util.Random import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext +import leakcanary.AndroidDebugHeapDumper import org.leakcanary.WhileSubscribedOrRetained import org.leakcanary.screens.Destination.TreeMapDestination import org.leakcanary.screens.TreeMapState.Loading import org.leakcanary.screens.TreeMapState.Success import org.leakcanary.screens.TreemapLayout.NodeValue -import org.leakcanary.service.TreeMapFetcher import shark.AndroidReferenceMatchers import shark.HprofHeapGraph.Companion.openHeapGraph import shark.IgnoredReferenceMatcher @@ -45,9 +47,7 @@ sealed interface TreeMapState { @HiltViewModel class TreeMapViewModel @Inject constructor( - // TODO Add a thing that can make IPC calls - navigator: Navigator, - private val treeMapFetcher: TreeMapFetcher, + navigator: Navigator ) : ViewModel() { val state = @@ -59,7 +59,6 @@ class TreeMapViewModel @Inject constructor( ) private fun stateStream(heapDump: File) = flow { - // TODO Dynamic package val result = withContext(Dispatchers.IO) { heapDump.openHeapGraph().use { heapGraph -> val weakAndFinalizerRefs = EnumSet.of( @@ -73,12 +72,10 @@ class TreeMapViewModel @Inject constructor( ObjectDominators().buildOfflineDominatorTree(heapGraph, ignoredRefs) } } - // val result = emptyMap() emit(Success(result)) } } -@OptIn(ExperimentalTextApi::class) @Composable fun TreeMapScreen(viewModel: TreeMapViewModel = viewModel()) { val stateProp by viewModel.state.collectAsState() @@ -89,10 +86,12 @@ class TreeMapViewModel @Inject constructor( is Success -> { val dominators = state.dominators - val textMeasure = rememberTextMeasurer() val root = ValueHolder.NULL_REFERENCE val treemapInput = DominatorNodeMapper( dominators = dominators, + // TODO Ideally depth & min size would be handled dynamically + // by the layout algo based on available space, so as not to keep rectangles + // large enough. maxDepth = 1, minSize = 10000 ).mapToTreemapInput(root) @@ -143,6 +142,35 @@ class DominatorNodeMapper( } } +@Composable +@Preview +fun OnDeviceHeapTreemapPreview() { + val filesDir = LocalContext.current.filesDir + val heapDumpFile = File(filesDir, "heapdump-${System.currentTimeMillis()}.hprof") + AndroidDebugHeapDumper.dumpHeap(heapDumpFile) + val dominators = heapDumpFile.openHeapGraph().use { heapGraph -> + val weakAndFinalizerRefs = EnumSet.of( + AndroidReferenceMatchers.REFERENCES, AndroidReferenceMatchers.FINALIZER_WATCHDOG_DAEMON + ) + val ignoredRefs = + AndroidReferenceMatchers.buildKnownReferences(weakAndFinalizerRefs).map { matcher -> + matcher as IgnoredReferenceMatcher + } + + ObjectDominators().buildOfflineDominatorTree(heapGraph, ignoredRefs) + } + val root = ValueHolder.NULL_REFERENCE + val treemapInput = DominatorNodeMapper( + dominators = dominators, + // TODO Ideally depth & min size would be handled dynamically + // by the layout algo based on available space, so as not to keep rectangles + // large enough. + maxDepth = 1, + minSize = 10000 + ).mapToTreemapInput(root) + Treemap(treemapInput) { dominators.getValue(it).name } +} + @Composable @Preview fun TreemapPreview() { @@ -150,10 +178,12 @@ fun TreemapPreview() { 25, "Root", listOf( - NodeValue(10, "A", listOf( + NodeValue( + 10, "A", listOf( NodeValue(5, "A1", emptyList()), NodeValue(5, "A2", emptyList()) - )), + ) + ), NodeValue(5, "B", emptyList()), NodeValue(5, "C", emptyList()), NodeValue(5, "D", emptyList()), @@ -164,7 +194,10 @@ fun TreemapPreview() { @OptIn(ExperimentalTextApi::class) @Composable -fun Treemap(root: NodeValue, text: (T) -> String) { +fun Treemap( + root: NodeValue, + text: (T) -> String +) { // TODO Colors should be a gradient related to depth // Try colors from https://observablehq.com/@d3/nested-treemap // Also colors as related to the node. @@ -190,19 +223,29 @@ fun Treemap(root: NodeValue, text: (T) -> String) { ).layout(root, size) layout.depthFirstTraversal { node -> + val topLeft = node.topLeft + val size = node.size drawRect( colors[node.depth], - topLeft = node.topLeft, - size = node.size + topLeft = node.topLeft + Offset(1f, 1f), + size = Size(node.size.width, node.size.height) ) + val leftX = topLeft.x + val topY = topLeft.y + val rightX = topLeft.x + size.width - 1 + val bottomY = topLeft.y + size.height - 1 + drawLine(color = Color.Black, start = topLeft, end = Offset(rightX, topY), strokeWidth = 2f) + drawLine(color = Color.Black, start = topLeft, end = Offset(leftX, bottomY), strokeWidth = 2f) + drawLine(color = Color.Black, start = Offset(leftX, bottomY), end = Offset(rightX, bottomY), strokeWidth = 2f) + drawLine(color = Color.Black, start = Offset(rightX, topY), end = Offset(rightX, bottomY), strokeWidth = 2f) // TODO Figure out what's up with negative numbers // java.lang.IllegalArgumentException: maxHeight(-1233) must be >= than minHeight(0) // if (node.x0 > 0 && node.y0 > 0) { - drawText( - textMeasurer = textMeasure, - text = text(node.content), - topLeft = node.topLeft - ) + drawText( + textMeasurer = textMeasure, + text = text(node.content), + topLeft = node.topLeft + Offset(4f, 4f) + ) // } } } diff --git a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/TreeMapFetcher.kt b/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/TreeMapFetcher.kt deleted file mode 100644 index 69f2f59ca3..0000000000 --- a/leakcanary/leakcanary-app/src/main/java/org/leakcanary/service/TreeMapFetcher.kt +++ /dev/null @@ -1,92 +0,0 @@ -package org.leakcanary.service - -import android.app.Application -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder -import android.os.IInterface -import java.io.File -import javax.inject.Inject -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import org.leakcanary.internal.HeapDataRepository -import shark.Dominators - -/** - * A one shot wrapper around [HeapDataRepository] that handles service lifecycle, and retrieves the - * data, all from a suspend function. This isn't meant to stick around, more a place to play with - * all the components at hand. - */ -class TreeMapFetcher @Inject constructor( - private val application: Application -) { - - suspend fun fetchTreeMap( - packageName: String, - heapDumpFile: File - ): Dominators { - return withContext(Dispatchers.IO) { - val intent = Intent("org.leakcanary.internal.HeapDataRepositoryService.BIND") - .apply { - setPackage(packageName) - } - - val (service, connection) = application.connectToService(intent) { - HeapDataRepository.Stub.asInterface(it) - } - try { - service.sayHi(heapDumpFile.absolutePath).wrapped - } finally { - application.unbindService(connection) - } - } - } - - suspend inline fun Context.connectToService( - intent: Intent, - crossinline asInterface: (IBinder) -> B - ): Pair = suspendCoroutine { continuation -> - val connection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName) { - // TODO Is this right? we should handle this. - continuation.resumeWithException(RuntimeException("Service Disconnected")) - } - - override fun onServiceConnected( - name: ComponentName, - binder: IBinder - ) { - continuation.resume(asInterface(binder) to this) - } - } - - // TODO What handle error case. - try { - val bringingServiceUp = - applicationContext.bindService( - intent, - connection, - Context.BIND_AUTO_CREATE - ) - // TODO If bringingServiceUp is false, must call unbindService - // false if the system couldn't find the service or if your client doesn't have permission - // to bind to it. Documentation says the same thing for throwing SecurityException though. - // TODO If this is false we don't have access to the service. Adding queries in manifest fixes it - // but that's not the right solution. - // This works again once we have the source app make a call. Unclear when this stops working. - // Maybe we need a way to flush like starting a transparent activity that just establishes - // a back service call which then makes this authorized. - check(bringingServiceUp) { - "Service not brought up" - } - } catch (e: SecurityException) { - // TODO must call unbindService - throw e - } - } -} diff --git a/shark/shark-cli/src/main/java/shark/Neo4JCommand.kt b/shark/shark-cli/src/main/java/shark/Neo4JCommand.kt index 46f99a3ee2..2ad0247bd6 100644 --- a/shark/shark-cli/src/main/java/shark/Neo4JCommand.kt +++ b/shark/shark-cli/src/main/java/shark/Neo4JCommand.kt @@ -75,7 +75,7 @@ import shark.ValueHolder.ShortHolder * WHERE "android.app.Activity.mDestroyed = true" in activity.fields * RETURN shortestPath((roots)-[:ROOT|REF*]->(activity)) */ -@SuppressWarnings("MaxLineLength", "LongMethod") +@SuppressWarnings("MaxLineLength") class Neo4JCommand : CliktCommand( name = "neo4j", help = "Convert heap dump to Neo4j database" diff --git a/shark/shark-graph/src/main/java/shark/internal/HprofInMemoryIndex.kt b/shark/shark-graph/src/main/java/shark/internal/HprofInMemoryIndex.kt index 7721ad3442..dfb13ab7e8 100644 --- a/shark/shark-graph/src/main/java/shark/internal/HprofInMemoryIndex.kt +++ b/shark/shark-graph/src/main/java/shark/internal/HprofInMemoryIndex.kt @@ -7,21 +7,11 @@ import shark.GcRoot.StickyClass import shark.HprofHeader import shark.HprofRecordReader import shark.HprofRecordTag -import shark.HprofRecordTag.ALLOC_SITES import shark.HprofRecordTag.CLASS_DUMP -import shark.HprofRecordTag.CONTROL_SETTINGS -import shark.HprofRecordTag.CPU_SAMPLES -import shark.HprofRecordTag.END_THREAD -import shark.HprofRecordTag.HEAP_DUMP -import shark.HprofRecordTag.HEAP_DUMP_END -import shark.HprofRecordTag.HEAP_DUMP_INFO -import shark.HprofRecordTag.HEAP_DUMP_SEGMENT -import shark.HprofRecordTag.HEAP_SUMMARY import shark.HprofRecordTag.INSTANCE_DUMP import shark.HprofRecordTag.LOAD_CLASS import shark.HprofRecordTag.OBJECT_ARRAY_DUMP import shark.HprofRecordTag.PRIMITIVE_ARRAY_DUMP -import shark.HprofRecordTag.PRIMITIVE_ARRAY_NODATA import shark.HprofRecordTag.ROOT_DEBUGGER import shark.HprofRecordTag.ROOT_FINALIZING import shark.HprofRecordTag.ROOT_INTERNED_STRING @@ -38,12 +28,7 @@ import shark.HprofRecordTag.ROOT_THREAD_OBJECT import shark.HprofRecordTag.ROOT_UNKNOWN import shark.HprofRecordTag.ROOT_UNREACHABLE import shark.HprofRecordTag.ROOT_VM_INTERNAL -import shark.HprofRecordTag.STACK_FRAME -import shark.HprofRecordTag.STACK_TRACE -import shark.HprofRecordTag.START_THREAD import shark.HprofRecordTag.STRING_IN_UTF8 -import shark.HprofRecordTag.UNLOAD_CLASS -import shark.HprofVersion import shark.HprofVersion.ANDROID import shark.OnHprofRecordTagListener import shark.PrimitiveType @@ -414,7 +399,6 @@ internal class HprofInMemoryIndex private constructor( ((classFieldBytes[classFieldsIndex - 2].toInt() and 0xff shl 8) or (classFieldBytes[classFieldsIndex - 1].toInt() and 0xff)).toShort() - @Suppress("LongMethod") override fun onHprofRecord( tag: HprofRecordTag, length: Long, diff --git a/shark/shark-hprof/src/main/java/shark/HprofWriter.kt b/shark/shark-hprof/src/main/java/shark/HprofWriter.kt index 11187af45c..33d0a5fcc1 100644 --- a/shark/shark-hprof/src/main/java/shark/HprofWriter.kt +++ b/shark/shark-hprof/src/main/java/shark/HprofWriter.kt @@ -1,5 +1,7 @@ package shark +import java.io.Closeable +import java.io.File import okio.Buffer import okio.BufferedSink import okio.Okio @@ -35,28 +37,9 @@ import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.In import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.LongArrayDump import shark.HprofRecord.HeapDumpRecord.ObjectRecord.PrimitiveArrayDumpRecord.ShortArrayDump import shark.HprofRecord.LoadClassRecord +import shark.HprofRecord.StackFrameRecord import shark.HprofRecord.StackTraceRecord import shark.HprofRecord.StringRecord -import shark.PrimitiveType.BOOLEAN -import shark.PrimitiveType.BYTE -import shark.PrimitiveType.CHAR -import shark.PrimitiveType.DOUBLE -import shark.PrimitiveType.FLOAT -import shark.PrimitiveType.INT -import shark.PrimitiveType.LONG -import shark.PrimitiveType.SHORT -import shark.ValueHolder.BooleanHolder -import shark.ValueHolder.ByteHolder -import shark.ValueHolder.CharHolder -import shark.ValueHolder.DoubleHolder -import shark.ValueHolder.FloatHolder -import shark.ValueHolder.IntHolder -import shark.ValueHolder.LongHolder -import shark.ValueHolder.ReferenceHolder -import shark.ValueHolder.ShortHolder -import java.io.Closeable -import java.io.File -import shark.HprofRecord.StackFrameRecord import shark.HprofRecordTag.CLASS_DUMP import shark.HprofRecordTag.HEAP_DUMP_INFO import shark.HprofRecordTag.INSTANCE_DUMP @@ -81,6 +64,24 @@ import shark.HprofRecordTag.ROOT_UNREACHABLE import shark.HprofRecordTag.ROOT_VM_INTERNAL import shark.HprofRecordTag.STACK_TRACE import shark.HprofRecordTag.STRING_IN_UTF8 +import shark.HprofWriter.Companion.openWriterFor +import shark.PrimitiveType.BOOLEAN +import shark.PrimitiveType.BYTE +import shark.PrimitiveType.CHAR +import shark.PrimitiveType.DOUBLE +import shark.PrimitiveType.FLOAT +import shark.PrimitiveType.INT +import shark.PrimitiveType.LONG +import shark.PrimitiveType.SHORT +import shark.ValueHolder.BooleanHolder +import shark.ValueHolder.ByteHolder +import shark.ValueHolder.CharHolder +import shark.ValueHolder.DoubleHolder +import shark.ValueHolder.FloatHolder +import shark.ValueHolder.IntHolder +import shark.ValueHolder.LongHolder +import shark.ValueHolder.ReferenceHolder +import shark.ValueHolder.ShortHolder /** * Generates Hprof files. @@ -140,7 +141,6 @@ class HprofWriter private constructor( } } - @Suppress("LongMethod") private fun BufferedSink.write(record: HprofRecord) { when (record) { is StringRecord -> { diff --git a/shark/shark-hprof/src/main/java/shark/StreamingHprofReader.kt b/shark/shark-hprof/src/main/java/shark/StreamingHprofReader.kt index 4d09ede428..45117fe9b5 100644 --- a/shark/shark-hprof/src/main/java/shark/StreamingHprofReader.kt +++ b/shark/shark-hprof/src/main/java/shark/StreamingHprofReader.kt @@ -50,7 +50,7 @@ class StreamingHprofReader private constructor( * * @return the number of bytes read from the source */ - @Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth") + @Suppress("ComplexMethod", "NestedBlockDepth") fun readRecords( recordTags: Set, listener: OnHprofRecordTagListener diff --git a/shark/shark-hprof/src/main/java/shark/StreamingRecordReaderAdapter.kt b/shark/shark-hprof/src/main/java/shark/StreamingRecordReaderAdapter.kt index d2bfa21e7d..c5f5376f88 100644 --- a/shark/shark-hprof/src/main/java/shark/StreamingRecordReaderAdapter.kt +++ b/shark/shark-hprof/src/main/java/shark/StreamingRecordReaderAdapter.kt @@ -54,7 +54,7 @@ class StreamingRecordReaderAdapter(private val streamingHprofReader: StreamingHp * * @return the number of bytes read from the source */ - @Suppress("ComplexMethod", "LongMethod", "NestedBlockDepth") + @Suppress("ComplexMethod", "NestedBlockDepth") fun readRecords( recordTypes: Set>, listener: OnHprofRecordListener diff --git a/shark/shark/api/shark.api b/shark/shark/api/shark.api index c857d699a6..c27861c9f3 100644 --- a/shark/shark/api/shark.api +++ b/shark/shark/api/shark.api @@ -99,6 +99,11 @@ public final class shark/DominatorTree { public final fun updateDominatedAsRoot (J)Z } +public final class shark/Dominators : java/io/Serializable { + public fun (Ljava/util/Map;)V + public final fun getDominatorNodes ()Ljava/util/Map; +} + public final class shark/FieldInstanceReferenceReader : shark/ReferenceReader { public fun (Lshark/HeapGraph;Ljava/util/List;)V public fun read (Lshark/HeapObject$HeapInstance;)Lkotlin/sequences/Sequence; @@ -437,11 +442,13 @@ public final class shark/MetadataExtractor$Companion { public final class shark/ObjectDominators { public fun ()V + public final fun buildDominatorTree (Lshark/HeapGraph;Ljava/util/List;)Ljava/util/Map; + public final fun buildOfflineDominatorTree (Lshark/HeapGraph;Ljava/util/List;)Ljava/util/Map; public final fun renderDominatorTree (Lshark/HeapGraph;Ljava/util/List;ILjava/lang/String;Z)Ljava/lang/String; public static synthetic fun renderDominatorTree$default (Lshark/ObjectDominators;Lshark/HeapGraph;Ljava/util/List;ILjava/lang/String;ZILjava/lang/Object;)Ljava/lang/String; } -public final class shark/ObjectDominators$DominatorNode { +public final class shark/ObjectDominators$DominatorNode : java/io/Serializable { public fun (IIILjava/util/List;)V public final fun component1 ()I public final fun component2 ()I @@ -458,6 +465,19 @@ public final class shark/ObjectDominators$DominatorNode { public fun toString ()Ljava/lang/String; } +public final class shark/ObjectDominators$OfflineDominatorNode : java/io/Serializable { + public fun (Lshark/ObjectDominators$DominatorNode;Ljava/lang/String;)V + public final fun component1 ()Lshark/ObjectDominators$DominatorNode; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Lshark/ObjectDominators$DominatorNode;Ljava/lang/String;)Lshark/ObjectDominators$OfflineDominatorNode; + public static synthetic fun copy$default (Lshark/ObjectDominators$OfflineDominatorNode;Lshark/ObjectDominators$DominatorNode;Ljava/lang/String;ILjava/lang/Object;)Lshark/ObjectDominators$OfflineDominatorNode; + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getNode ()Lshark/ObjectDominators$DominatorNode; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public abstract interface class shark/ObjectInspector { public abstract fun inspect (Lshark/ObjectReporter;)V }