Add plugin for LeakCanary 2 (#1959)

Summary:
Add plugin for LeakCanary 2 as requested various times: https://github.com/facebook/flipper/issues/1379 https://github.com/facebook/flipper/issues/832  https://github.com/square/leakcanary/issues/1777

## Changelog

* Adds a leakcanary2 plugin for Android
* Adds support for leakcanary2 to existing desktop plugin

Pull Request resolved: https://github.com/facebook/flipper/pull/1959

Test Plan:
* Docs updated to show new implementation
* Should old leakcanary plugin in sample be replaced?

Reviewed By: mweststrate

Differential Revision: D26691637

Pulled By: passy

fbshipit-source-id: 5e236fa6cc124f0720a6b21b5ee7c117ccf96fbf
This commit is contained in:
Harold Martin
2021-03-01 09:10:05 -08:00
committed by Facebook GitHub Bot
parent 61eabf372a
commit 4d8be35d1a
10 changed files with 416 additions and 27 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.facebook.flipper.plugins.leakcanary2">
</manifest>

View File

@@ -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<Leak> = 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<Leak> {
return if (this is HeapAnalysisSuccess) {
allLeaks.mapNotNull {
if (it.leakTraces.isNotEmpty()) {
it.leakTraces[0].toLeak(it.shortDescription)
} else {
null
}
}.toList()
} else {
emptyList()
}
}
}

View File

@@ -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<Leak> = mutableListOf()
private val alreadySeenLeakSignatures: MutableSet<String> = 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<Leak>) {
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"
}
}

View File

@@ -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<Leak>) : 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<String, Element>,
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<String, FlipperValue>.toFlipperObject(): FlipperObject =
mapValues { it.value.toFlipperObject() }.toFlipperObject()
@JvmName("toFlipperObjectStringFlipperObject")
private fun Map<String, FlipperObject>.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<Pair<String, Element>> {
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<String> = emptyList(),
val attributes: List<ElementAttribute>,
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<FlipperValue>.toFlipperArray(): FlipperArray =
map { it.toFlipperObject() }.toFlipperArray()
@JvmName("toFlipperArrayString")
private fun Iterable<String>.toFlipperArray(): FlipperArray =
fold(FlipperArray.Builder()) { builder, row -> builder.put(row) }.build()
}
internal fun Iterable<FlipperObject>.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()
}
}