diff --git a/android/plugins/leakcanary2/build.gradle b/android/plugins/leakcanary2/build.gradle
new file mode 100644
index 000000000..dbc0b409c
--- /dev/null
+++ b/android/plugins/leakcanary2/build.gradle
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'kotlin-android-extensions'
+apply plugin: 'kotlin-kapt'
+apply plugin: 'maven'
+
+android {
+ compileSdkVersion rootProject.compileSdkVersion
+ buildToolsVersion rootProject.buildToolsVersion
+
+ defaultConfig {
+ minSdkVersion rootProject.minSdkVersion
+ targetSdkVersion rootProject.targetSdkVersion
+ }
+
+ dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION"
+ implementation project(':android')
+ implementation deps.leakcanary2
+ compileOnly deps.jsr305
+ }
+}
+
+apply plugin: 'com.vanniktech.maven.publish'
diff --git a/android/plugins/leakcanary2/gradle.properties b/android/plugins/leakcanary2/gradle.properties
new file mode 100644
index 000000000..51ae3ccfc
--- /dev/null
+++ b/android/plugins/leakcanary2/gradle.properties
@@ -0,0 +1,8 @@
+# This source code is licensed under the MIT license found in the LICENSE
+# file in the root directory of this source tree.
+
+POM_NAME=Flipper LeakCanary2 Plugin
+POM_DESCRIPTION=LeakCanary2 plugin for Flipper
+POM_ARTIFACT_ID=flipper-leakcanary2-plugin
+POM_PACKAGING=aar
+
diff --git a/android/plugins/leakcanary2/src/main/AndroidManifest.xml b/android/plugins/leakcanary2/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..8308d56d9
--- /dev/null
+++ b/android/plugins/leakcanary2/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakListener.kt b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakListener.kt
new file mode 100644
index 000000000..2cba5cfb4
--- /dev/null
+++ b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/FlipperLeakListener.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.leakcanary2
+
+import com.facebook.flipper.android.AndroidFlipperClient
+import leakcanary.DefaultOnHeapAnalyzedListener
+import leakcanary.OnHeapAnalyzedListener
+import shark.HeapAnalysis
+import shark.HeapAnalysisSuccess
+
+class FlipperLeakListener : OnHeapAnalyzedListener {
+ private val leaks: MutableList = mutableListOf()
+
+ private val defaultListener = DefaultOnHeapAnalyzedListener.create()
+
+ override fun onHeapAnalyzed(heapAnalysis: HeapAnalysis) {
+ leaks.addAll(heapAnalysis.toLeakList())
+
+ AndroidFlipperClient.getInstanceIfInitialized()?.let { client ->
+ (client.getPlugin(LeakCanary2FlipperPlugin.ID) as? LeakCanary2FlipperPlugin)
+ ?.reportLeaks(leaks)
+ }
+
+ defaultListener.onHeapAnalyzed(heapAnalysis)
+ }
+
+ private fun HeapAnalysis.toLeakList(): List {
+ return if (this is HeapAnalysisSuccess) {
+ allLeaks.mapNotNull {
+ if (it.leakTraces.isNotEmpty()) {
+ it.leakTraces[0].toLeak(it.shortDescription)
+ } else {
+ null
+ }
+ }.toList()
+ } else {
+ emptyList()
+ }
+ }
+}
diff --git a/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/LeakCanary2FlipperPlugin.kt b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/LeakCanary2FlipperPlugin.kt
new file mode 100644
index 000000000..9d0388139
--- /dev/null
+++ b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/LeakCanary2FlipperPlugin.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.leakcanary2
+
+import com.facebook.flipper.core.FlipperConnection
+import com.facebook.flipper.core.FlipperPlugin
+
+private const val REPORT_LEAK_EVENT = "reportLeak2"
+private const val CLEAR_EVENT = "clear"
+
+class LeakCanary2FlipperPlugin : FlipperPlugin {
+ private val leaks: MutableList = mutableListOf()
+ private val alreadySeenLeakSignatures: MutableSet = mutableSetOf()
+ private var connection: FlipperConnection? = null
+
+ override fun getId() = ID
+
+ override fun onConnect(connection: FlipperConnection?) {
+ this.connection = connection
+ connection?.receive(CLEAR_EVENT) { _, _ -> leaks.clear() }
+ sendLeakList()
+ }
+
+ override fun onDisconnect() {
+ connection = null
+ }
+
+ override fun runInBackground() = false
+
+ internal fun reportLeaks(leaks: List) {
+ for (leak in leaks) {
+ if (leak.signature !in alreadySeenLeakSignatures) {
+ this.leaks.add(leak)
+ alreadySeenLeakSignatures.add(leak.signature)
+ }
+ }
+
+ sendLeakList()
+ }
+
+ private fun sendLeakList() {
+ connection?.send(REPORT_LEAK_EVENT, LeakCanary2Report(leaks).toFlipperObject())
+ }
+
+ companion object {
+ const val ID = "LeakCanary"
+ }
+}
diff --git a/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/LeakCanary2Report.kt b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/LeakCanary2Report.kt
new file mode 100644
index 000000000..410db39f8
--- /dev/null
+++ b/android/plugins/leakcanary2/src/main/java/com/facebook/flipper/plugins/leakcanary2/LeakCanary2Report.kt
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+package com.facebook.flipper.plugins.leakcanary2
+
+import com.facebook.flipper.core.FlipperArray
+import com.facebook.flipper.core.FlipperObject
+import com.facebook.flipper.core.FlipperValue
+import shark.LeakTrace
+import shark.LeakTraceObject
+import java.util.UUID
+
+internal data class LeakCanary2Report(val leaks: List) : FlipperValue {
+ override fun toFlipperObject(): FlipperObject = FlipperObject.Builder()
+ .put("leaks", leaks.map { it.toFlipperObject() }.toFlipperArray())
+ .build()
+}
+
+internal data class Leak(
+ val title: String,
+ val root: String,
+ val elements: Map,
+ val retainedSize: String,
+ val signature: String,
+ val details: String
+) : FlipperValue {
+ override fun toFlipperObject(): FlipperObject {
+ return FlipperObject.Builder()
+ .put("title", title)
+ .put("root", root)
+ .put("elements", elements.toFlipperObject())
+ .put("retainedSize", retainedSize)
+ .put("details", details)
+ .build()
+ }
+
+ private fun Map.toFlipperObject(): FlipperObject =
+ mapValues { it.value.toFlipperObject() }.toFlipperObject()
+
+ @JvmName("toFlipperObjectStringFlipperObject")
+ private fun Map.toFlipperObject(): FlipperObject =
+ asIterable()
+ .fold(FlipperObject.Builder()) { builder, entry ->
+ builder.put(entry.key, entry.value)
+ }
+ .build()
+}
+
+internal fun LeakTrace.toLeak(title: String): Leak {
+ val elements = getElements()
+ return Leak(
+ title = title,
+ elements = elements.toMap(),
+ retainedSize = retainedHeapByteSize?.let { "$it bytes" } ?: "unknown size",
+ signature = signature,
+ root = elements.first().first,
+ details = "$this"
+ )
+}
+
+private fun LeakTrace.getElements(): List> {
+ val referenceElements = referencePath.map { reference ->
+ val id = UUID.randomUUID().toString()
+ id to Element(id, reference.originObject)
+ }.toMutableList()
+
+ val leakId = UUID.randomUUID().toString()
+ referenceElements.add(leakId to Element(leakId, leakingObject))
+
+ return referenceElements.mapIndexed { index, pair ->
+ pair.first to if (index == referenceElements.lastIndex) pair.second else pair.second.copy(
+ children = listOf(referenceElements[index + 1].second.id)
+ )
+ }
+}
+
+internal data class Element(
+ val id: String,
+ val name: String,
+ val expanded: Boolean = true,
+ val children: List = emptyList(),
+ val attributes: List,
+ val decoration: String = ""
+) : FlipperValue {
+ constructor(id: String, leakObject: LeakTraceObject) : this(
+ id = id,
+ name = "${leakObject.className} (${leakObject.typeName})",
+ attributes = listOf(
+ ElementAttribute("leaking", leakObject.leakingStatus.shortName),
+ ElementAttribute("retaining", leakObject.retaining)
+ )
+ )
+
+ override fun toFlipperObject(): FlipperObject {
+ return FlipperObject.Builder()
+ .put("id", id)
+ .put("name", name)
+ .put("expanded", expanded)
+ .put("children", children.toFlipperArray())
+ .put("attributes", attributes.toFlipperArray())
+ .put("data", EMPTY_FLIPPER_OBJECT)
+ .put("decoration", decoration)
+ .put("extraInfo", EMPTY_FLIPPER_OBJECT)
+ .build()
+ }
+
+ @JvmName("toFlipperArrayFlipperValue")
+ private fun Iterable.toFlipperArray(): FlipperArray =
+ map { it.toFlipperObject() }.toFlipperArray()
+
+ @JvmName("toFlipperArrayString")
+ private fun Iterable.toFlipperArray(): FlipperArray =
+ fold(FlipperArray.Builder()) { builder, row -> builder.put(row) }.build()
+}
+
+internal fun Iterable.toFlipperArray(): FlipperArray =
+ fold(FlipperArray.Builder()) { builder, row -> builder.put(row) }.build()
+
+private val LeakTraceObject.LeakingStatus.shortName: String
+ get() = when (this) {
+ LeakTraceObject.LeakingStatus.NOT_LEAKING -> "N"
+ LeakTraceObject.LeakingStatus.LEAKING -> "Y"
+ LeakTraceObject.LeakingStatus.UNKNOWN -> "?"
+ }
+private val LeakTraceObject.retaining: String
+ get() = retainedHeapByteSize?.let { "$it bytes ($retainedObjectCount objects)" } ?: "unknown"
+
+private val EMPTY_FLIPPER_OBJECT = FlipperObject.Builder().build()
+
+data class ElementAttribute(
+ val name: String,
+ val value: String
+) : FlipperValue {
+ override fun toFlipperObject(): FlipperObject {
+ return FlipperObject.Builder()
+ .put("name", name)
+ .put("value", value)
+ .build()
+ }
+}
diff --git a/build.gradle b/build.gradle
index 322101213..96e3a9ce7 100644
--- a/build.gradle
+++ b/build.gradle
@@ -15,6 +15,7 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.1.2'
classpath 'com.vanniktech:gradle-maven-publish-plugin:0.14.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$KOTLIN_VERSION"
+ classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.4.20"
}
}
@@ -101,6 +102,7 @@ ext.deps = [
mockito : 'org.mockito:mockito-core:2.26.0',
okhttp3 : 'com.squareup.okhttp3:okhttp:3.14.1',
leakcanary : 'com.squareup.leakcanary:leakcanary-android:1.6.3',
+ leakcanary2 : 'com.squareup.leakcanary:leakcanary-android:2.6',
testCore : 'androidx.test:core:1.1.0',
testRules : 'androidx.test:rules:1.1.0',
// Plugin dependencies
diff --git a/desktop/plugins/leak_canary/index.tsx b/desktop/plugins/leak_canary/index.tsx
index 5370691e7..e968a6081 100644
--- a/desktop/plugins/leak_canary/index.tsx
+++ b/desktop/plugins/leak_canary/index.tsx
@@ -36,6 +36,10 @@ type LeakReport = {
leaks: string[];
};
+type LeakCanary2Report = {
+ leaks: Leak2[];
+};
+
export type Fields = {[key: string]: string};
export type Leak = {
title: string;
@@ -45,6 +49,15 @@ export type Leak = {
instanceFields: {[key: string]: Fields};
staticFields: {[key: string]: Fields};
retainedSize: string;
+ details?: string;
+};
+
+export type Leak2 = {
+ title: string;
+ root: string;
+ elements: {[key: string]: Element};
+ retainedSize: string;
+ details: string;
};
const Window = styled(FlexRow)({
@@ -72,23 +85,43 @@ export default class LeakCanary extends FlipperPlugin<
init() {
this.client.subscribe('reportLeak', (results: LeakReport) => {
- // We only process new leaks instead of replacing the whole list in order
- // to both avoid redundant processing and to preserve the expanded/
- // collapsed state of the tree view
- const newLeaks = processLeaks(results.leaks.slice(this.state.leaksCount));
+ this._addNewLeaks(processLeaks(results.leaks));
+ });
- const leaks = this.state.leaks;
- for (let i = 0; i < newLeaks.length; i++) {
- leaks.push(newLeaks[i]);
- }
-
- this.setState({
- leaks: leaks,
- leaksCount: results.leaks.length,
- });
+ this.client.subscribe('reportLeak2', (results: LeakCanary2Report) => {
+ this._addNewLeaks(results.leaks.map(this._adaptLeak2));
});
}
+ _addNewLeaks = (incomingLeaks: Leak[]) => {
+ // We only process new leaks instead of replacing the whole list in order
+ // to both avoid redundant processing and to preserve the expanded/
+ // collapsed state of the tree view
+ const newLeaks = incomingLeaks.slice(this.state.leaksCount);
+ const leaks = this.state.leaks;
+ for (let i = 0; i < newLeaks.length; i++) {
+ leaks.push(newLeaks[i]);
+ }
+
+ this.setState({
+ leaks: leaks,
+ leaksCount: leaks.length,
+ });
+ };
+
+ _adaptLeak2 = (leak: Leak2): Leak => {
+ return {
+ title: leak.title,
+ root: leak.root,
+ elements: leak.elements,
+ elementsSimple: leak.elements,
+ staticFields: {},
+ instanceFields: {},
+ retainedSize: leak.retainedSize,
+ details: leak.details,
+ };
+ };
+
_clearLeaks = () => {
this.setState({
leaks: [],
@@ -154,20 +187,29 @@ export default class LeakCanary extends FlipperPlugin<
return (
-
-
-
-
-
-
+ {instanceFields && (
+
+
+
+ )}
+ {staticFields && (
+
+
+
+ )}
+ {leak.details && (
+
+ {leak.details}
+
+ )}
);
}
@@ -184,6 +226,7 @@ export default class LeakCanary extends FlipperPlugin<
const elements = showFullClassPaths
? leak.elements
: leak.elementsSimple;
+
const selected = selectedIdx == idx ? selectedEid : null;
return (