Skip to content

Instantly share code, notes, and snippets.

@pyricau
Created July 4, 2019 17:43
Show Gist options
  • Save pyricau/11b74199023ddbbf9dfc7f5360cfd328 to your computer and use it in GitHub Desktop.
Save pyricau/11b74199023ddbbf9dfc7f5360cfd328 to your computer and use it in GitHub Desktop.
package com.squareup.leakcanary
import android.app.Application
import com.bugsnag.android.Client
import com.bugsnag.android.MetaData
import com.bugsnag.android.Severity.ERROR
import com.squareup.leakcanary.BugsnagLeakUploader.ReportType.FAILURE
import com.squareup.leakcanary.BugsnagLeakUploader.ReportType.LEAK
import com.squareup.leakcanary.BugsnagLeakUploader.ReportType.NOT_FOUND
import com.squareup.leakcanary.BugsnagLeakUploader.ReportType.WONT_FIX_LEAK
import com.squareup.util.AndroidId
import com.squareup.util.Logs
import com.squareup.util.createSHA1Hash
import com.squareup.util.lastSegment
import leakcanary.HeapAnalysis
import leakcanary.HeapAnalysisFailure
import leakcanary.HeapAnalysisSuccess
import leakcanary.LeakTraceElement
import leakcanary.LeakingInstance
import leakcanary.NoPathToInstance
import java.util.UUID
import javax.inject.Inject
import javax.inject.Provider
/**
* Available bugsnag filters: LEAK.reportType, LEAK.fromInstrumentationTests, LEAK.versionNumber,
* LEAK.analysisUuid
*
*/
class BugsnagLeakUploader @Inject constructor(
private val application: Application,
@AndroidId private val androidIdProvider: Provider<String?>
) {
private enum class ReportType {
FAILURE,
WONT_FIX_LEAK,
LEAK,
NOT_FOUND
}
private val bugsnagClient = Client(application, BUGSNAG_DEV_LEAK_API_KEY, false)
init {
bugsnagClient.setSendThreads(false)
bugsnagClient.beforeNotify { error ->
// Bugsnag does smart grouping of exceptions, which we don't want for leak traces.
// So instead we rely on the SHA-1 of the stacktrace, which has a low risk of collision.
val stackTraceString = Logs.getStackTraceString(error.exception)
val uniqueHash = stackTraceString.createSHA1Hash()
error.setGroupingHash(uniqueHash)
true
}
}
fun uploadLeak(
heapAnalysis: HeapAnalysis,
fromInstrumentationTests: Boolean
) {
when (heapAnalysis) {
is HeapAnalysisFailure -> {
val metadata = createMetadata(FAILURE, heapAnalysis, fromInstrumentationTests)
bugsnagClient.notify(heapAnalysis.exception, ERROR, metadata)
}
is HeapAnalysisSuccess -> {
val analysisUuid = UUID.randomUUID()
.toString()
for (retainedInstance in heapAnalysis.retainedInstances) {
var notFoundReport = ""
when (retainedInstance) {
is NoPathToInstance -> {
notFoundReport += "$retainedInstance\n"
}
is LeakingInstance -> {
val wontFix = retainedInstance.exclusionStatus != null
val metadata = if (wontFix)
createMetadata(WONT_FIX_LEAK, heapAnalysis, fromInstrumentationTests)
else
createMetadata(LEAK, heapAnalysis, fromInstrumentationTests)
val exception = if (wontFix)
wontFixLeakAsFakeException(retainedInstance)
else
leakTraceAsFakeException(retainedInstance)
metadata.addToTab("Leak", "analysisUuid", analysisUuid)
metadata.addToTab("Leak", "leakTrace", retainedInstance.leakTrace.toString())
bugsnagClient.notify(exception, ERROR, metadata)
}
}
if (notFoundReport != "") {
val metadata = createMetadata(NOT_FOUND, heapAnalysis, fromInstrumentationTests)
metadata.addToTab("Leak", "analysisUuid", analysisUuid)
metadata.addToTab("Leak", "report", notFoundReport)
bugsnagClient.notify(notFoundReportException(), ERROR, metadata)
}
}
}
}
}
/**
* For excluded leaks we create a stacktrace based on the elements marked as excluded in the leak
* trace. This should group together all leaks excluded by the same exclusion.
*/
private fun wontFixLeakAsFakeException(retainedInstance: LeakingInstance): RuntimeException {
val matching = retainedInstance.leakTrace.firstElementExclusion.matching
val exception = RuntimeException(
"[Won't fix] Known memory leak: $matching. See LEAK tab."
)
val stackTrace = mutableListOf<StackTraceElement>()
for (element in retainedInstance.leakTrace.elements) {
if (element.exclusion != null) {
stackTrace.add(buildStackTraceElement(element))
}
}
exception.stackTrace = stackTrace.toTypedArray()
return exception
}
/**
* Creates an exception which has a stacktrace that matches the likely causes of the leak trace.
* Skipping the reachable parts in the fake stacktrace means those won't be included when grouping
* and we'll see better grouped leak reports.
*/
private fun leakTraceAsFakeException(retainedInstance: LeakingInstance): RuntimeException {
val element = retainedInstance.leakTrace.leakCauses.first()
val referenceName = element.reference!!.groupingName
val refDescription = element.classSimpleName + "." + referenceName
val exception = RuntimeException("Memory leak starting at: $refDescription. See LEAK tab.")
val stackTrace = mutableListOf<StackTraceElement>()
for (cause in retainedInstance.leakTrace.leakCauses) {
stackTrace.add(buildStackTraceElement(cause))
}
exception.stackTrace = stackTrace.toTypedArray()
return exception
}
private fun notFoundReportException(): RuntimeException {
val exception = RuntimeException("Leak not found, see LEAK tab.")
val stackTrace =
mutableListOf(StackTraceElement("LeakNotFound", "notFound", "LeakNotFound.java", 42))
exception.stackTrace = stackTrace.toTypedArray()
return exception
}
private fun createMetadata(
reportType: ReportType,
heapAnalysis: HeapAnalysis,
fromInstrumentationTests: Boolean
): MetaData {
val metadata = MetaData()
metadata.addToTab(
"App", "buildSha", application.getString(com.squareup.utilities.R.string.git_sha)
)
metadata.addToTab("Device", "androidId", androidIdProvider.get())
// Allows filtering
metadata.addToTab("Leak", "reportType", reportType.name)
if (heapAnalysis is HeapAnalysisSuccess) {
metadata.addToTab("Leak", "retainedInstanceCount", heapAnalysis.retainedInstances.size)
}
metadata.addToTab("Leak", "fromInstrumentationTests", fromInstrumentationTests)
metadata.addToTab("Leak", "versionNumber", LeakCanaryConfig.VERSION_NUMBER)
metadata.addToTab("Leak", "analysisDurationMs", heapAnalysis.analysisDurationMillis)
metadata.addToTab("Leak", "heapDumpPath", heapAnalysis.heapDumpFile.absolutePath)
return metadata
}
private fun buildStackTraceElement(element: LeakTraceElement): StackTraceElement {
val file = element.className.lastSegment('.') + ".java"
return StackTraceElement(element.className, element.reference!!.groupingName, file, 42)
}
companion object {
private const val BUGSNAG_DEV_LEAK_API_KEY = "KEY"
}
}
class LeakCanaryConfig @Inject constructor(private val leakUploader: BugsnagLeakUploader) :
ManualLeakCanaryConfig {
companion object {
/**
* Increase this when the leakcanary config or version changes, or when a leak is fixed. This
* allows filtering leak reports on the latest leak related changes.
*/
internal const val VERSION_NUMBER = 3
}
override fun configure() {
Timber.d("Configuring LeakCanary for Square")
// Increment BugsnagLeakUploader.VERSION_NUMBER when doing changes to the config.
LeakCanary.config = LeakCanary.config.copy(
leakTraceInspectors = createLeakTraceInspectors(),
analysisResultListener = createAnalysisResultListener()
)
}
private fun createLeakTraceInspectors(): List<LeakTraceInspector> {
val leakInspectors = AndroidLeakTraceInspectors.defaultInspectors()
.toMutableList()
// Reachability inspectors that are custom to the POS codebase (or common but not yet supported
// in LeakCanary).
// Reachability inspectors help LeakCanary reduce possible leak causes. This also help with
// leak grouping (which is based on leak causes).
leakInspectors += AppSingletonInspector(
AndroidMainThread::class.java.name,
AppContextWrapper::class.java.name,
"com.squareup.RegisterAppDelegate"
)
return leakInspectors
}
private fun createAnalysisResultListener(): AnalysisResultListener = { application, heapAnalysis ->
leakUploader.uploadLeak(heapAnalysis, fromInstrumentationTests = false)
DefaultAnalysisResultListener(application, heapAnalysis)
}
}
@alexjlockwood
Copy link

FYI, seems like there is some stuff specific to Square in here. Not sure what to do with some of it:

import com.squareup.util.AndroidId
import com.squareup.util.Logs
import com.squareup.util.createSHA1Hash
import com.squareup.util.lastSegment

Also, element.classSimpleName doesn't resolve for me:

val refDescription = element.classSimpleName + "." + referenceName

Also not certain what com.squareup.utilities.R.string.git_sha is used for:

metadata.addToTab(
    "App", "buildSha", application.getString(com.squareup.utilities.R.string.git_sha)
)

Working my way through it though. :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment