Created
July 4, 2019 17:43
-
-
Save pyricau/11b74199023ddbbf9dfc7f5360cfd328 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FYI, seems like there is some stuff specific to Square in here. Not sure what to do with some of it:
Also,
element.classSimpleName
doesn't resolve for me:Also not certain what
com.squareup.utilities.R.string.git_sha
is used for:Working my way through it though. :)