HeapDumpTrigger.kt
TLDR
This file HeapDumpTrigger.kt
contains the implementation of a class called HeapDumpTrigger
. This class is responsible for triggering heap dumps based on certain conditions and events in the application. It uses the Application
, Handler
, ObjectWatcher
, GcTrigger
, and Config
objects for performing its tasks.
Methods
There are no methods in this file.
Classes
HeapDumpTrigger
The HeapDumpTrigger
class is responsible for triggering heap dumps based on certain conditions and events in the application. It has the following properties:
-
application
: AnApplication
object representing the Android application. -
backgroundHandler
: AHandler
object for performing background tasks. -
objectWatcher
: AnObjectWatcher
object for watching objects. -
gcTrigger
: AGcTrigger
object for triggering garbage collection. -
configProvider
: A lambda function that provides theConfig
object.
The HeapDumpTrigger
class has the following public methods:
-
onApplicationVisibilityChanged(applicationVisible: Boolean)
: Called when the visibility of the application changes. It updates theapplicationInvisibleAt
property and schedules a retained object check. -
onDumpHeapReceived(forceDump: Boolean)
: Called when a heap dump is requested. It runs garbage collection, checks if there are any retained objects, and if so, dumps the heap.
The HeapDumpTrigger
class also has several private helper methods, including checkRetainedObjects()
, dumpHeap()
, saveResourceIdNamesToMemory()
, checkRetainedCount()
, scheduleRetainedObjectCheck()
, showNoMoreRetainedObjectNotification()
, showRetainedCountNotification()
, dismissRetainedCountNotification()
, and dismissNoRetainedOnTapNotification()
.
package leakcanary.internal
import android.app.Application
import android.app.Notification
import android.app.NotificationManager
import android.content.Context
import android.content.res.Resources.NotFoundException
import android.os.Handler
import android.os.SystemClock
import com.squareup.leakcanary.core.R
import java.util.UUID
import leakcanary.AppWatcher
import leakcanary.EventListener.Event.DumpingHeap
import leakcanary.EventListener.Event.HeapDump
import leakcanary.EventListener.Event.HeapDumpFailed
import leakcanary.GcTrigger
import leakcanary.KeyedWeakReference
import leakcanary.LeakCanary.Config
import leakcanary.ObjectWatcher
import leakcanary.internal.HeapDumpControl.ICanHazHeap.Nope
import leakcanary.internal.HeapDumpControl.ICanHazHeap.NotifyingNope
import leakcanary.internal.InternalLeakCanary.onRetainInstanceListener
import leakcanary.internal.NotificationReceiver.Action.CANCEL_NOTIFICATION
import leakcanary.internal.NotificationReceiver.Action.DUMP_HEAP
import leakcanary.internal.NotificationType.LEAKCANARY_LOW
import leakcanary.internal.RetainInstanceEvent.CountChanged.BelowThreshold
import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpHappenedRecently
import leakcanary.internal.RetainInstanceEvent.CountChanged.DumpingDisabled
import leakcanary.internal.RetainInstanceEvent.NoMoreObjects
import leakcanary.internal.friendly.measureDurationMillis
import shark.AndroidResourceIdNames
import shark.SharkLog
internal class HeapDumpTrigger(
private val application: Application,
private val backgroundHandler: Handler,
private val objectWatcher: ObjectWatcher,
private val gcTrigger: GcTrigger,
private val configProvider: () -> Config
) {
private val notificationManager
get() =
application.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val applicationVisible
get() = applicationInvisibleAt == -1L
@Volatile
private var checkScheduledAt: Long = 0L
private var lastDisplayedRetainedObjectCount = 0
private var lastHeapDumpUptimeMillis = 0L
private val scheduleDismissRetainedCountNotification = {
dismissRetainedCountNotification()
}
private val scheduleDismissNoRetainedOnTapNotification = {
dismissNoRetainedOnTapNotification()
}
/**
* When the app becomes invisible, we don't dump the heap immediately. Instead we wait in case
* the app came back to the foreground, but also to wait for new leaks that typically occur on
* back press (activity destroy).
*/
private val applicationInvisibleLessThanWatchPeriod: Boolean
get() {
val applicationInvisibleAt = applicationInvisibleAt
return applicationInvisibleAt != -1L && SystemClock.uptimeMillis() - applicationInvisibleAt < AppWatcher.retainedDelayMillis
}
@Volatile
private var applicationInvisibleAt = -1L
// Needs to be lazy because on Android 16, UUID.randomUUID().toString() will trigger a disk read
// violation by calling RandomBitsSupplier.getUnixDeviceRandom()
// Can't be lazy because this is a var.
private var currentEventUniqueId: String? = null
fun onApplicationVisibilityChanged(applicationVisible: Boolean) {
if (applicationVisible) {
applicationInvisibleAt = -1L
} else {
applicationInvisibleAt = SystemClock.uptimeMillis()
// Scheduling for after watchDuration so that any destroyed activity has time to become
// watch and be part of this analysis.
scheduleRetainedObjectCheck(
delayMillis = AppWatcher.retainedDelayMillis
)
}
}
private fun checkRetainedObjects() {
val iCanHasHeap = HeapDumpControl.iCanHasHeap()
val config = configProvider()
if (iCanHasHeap is Nope) {
if (iCanHasHeap is NotifyingNope) {
// Before notifying that we can't dump heap, let's check if we still have retained object.
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
val nopeReason = iCanHasHeap.reason()
val wouldDump = !checkRetainedCount(
retainedReferenceCount, config.retainedVisibleThreshold, nopeReason
)
if (wouldDump) {
val uppercaseReason = nopeReason[0].toUpperCase() + nopeReason.substring(1)
onRetainInstanceListener.onEvent(DumpingDisabled(uppercaseReason))
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = uppercaseReason
)
}
} else {
SharkLog.d {
application.getString(
R.string.leak_canary_heap_dump_disabled_text, iCanHasHeap.reason()
)
}
}
return
}
var retainedReferenceCount = objectWatcher.retainedObjectCount
if (retainedReferenceCount > 0) {
gcTrigger.runGc()
retainedReferenceCount = objectWatcher.retainedObjectCount
}
if (checkRetainedCount(retainedReferenceCount, config.retainedVisibleThreshold)) return
val now = SystemClock.uptimeMillis()
val elapsedSinceLastDumpMillis = now - lastHeapDumpUptimeMillis
if (elapsedSinceLastDumpMillis < WAIT_BETWEEN_HEAP_DUMPS_MILLIS) {
onRetainInstanceListener.onEvent(DumpHappenedRecently)
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(R.string.leak_canary_notification_retained_dump_wait)
)
scheduleRetainedObjectCheck(
delayMillis = WAIT_BETWEEN_HEAP_DUMPS_MILLIS - elapsedSinceLastDumpMillis
)
return
}
dismissRetainedCountNotification()
val visibility = if (applicationVisible) "visible" else "not visible"
dumpHeap(
retainedReferenceCount = retainedReferenceCount,
retry = true,
reason = "$retainedReferenceCount retained objects, app is $visibility"
)
}
private fun dumpHeap(
retainedReferenceCount: Int,
retry: Boolean,
reason: String
) {
val directoryProvider =
InternalLeakCanary.createLeakDirectoryProvider(InternalLeakCanary.application)
val heapDumpFile = directoryProvider.newHeapDumpFile()
val durationMillis: Long
if (currentEventUniqueId == null) {
currentEventUniqueId = UUID.randomUUID().toString()
}
try {
InternalLeakCanary.sendEvent(DumpingHeap(currentEventUniqueId!!))
if (heapDumpFile == null) {
throw RuntimeException("Could not create heap dump file")
}
saveResourceIdNamesToMemory()
val heapDumpUptimeMillis = SystemClock.uptimeMillis()
KeyedWeakReference.heapDumpUptimeMillis = heapDumpUptimeMillis
durationMillis = measureDurationMillis {
configProvider().heapDumper.dumpHeap(heapDumpFile)
}
if (heapDumpFile.length() == 0L) {
throw RuntimeException("Dumped heap file is 0 byte length")
}
lastDisplayedRetainedObjectCount = 0
lastHeapDumpUptimeMillis = SystemClock.uptimeMillis()
objectWatcher.clearObjectsWatchedBefore(heapDumpUptimeMillis)
currentEventUniqueId = UUID.randomUUID().toString()
InternalLeakCanary.sendEvent(HeapDump(currentEventUniqueId!!, heapDumpFile, durationMillis, reason))
} catch (throwable: Throwable) {
InternalLeakCanary.sendEvent(HeapDumpFailed(currentEventUniqueId!!, throwable, retry))
if (retry) {
scheduleRetainedObjectCheck(
delayMillis = WAIT_AFTER_DUMP_FAILED_MILLIS
)
}
showRetainedCountNotification(
objectCount = retainedReferenceCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_dump_failed
)
)
return
}
}
/**
* Stores in memory the mapping of resource id ints to their corresponding name, so that the heap
* analysis can label views with their resource id names.
*/
private fun saveResourceIdNamesToMemory() {
val resources = application.resources
AndroidResourceIdNames.saveToMemory(
getResourceTypeName = { id ->
try {
resources.getResourceTypeName(id)
} catch (e: NotFoundException) {
null
}
},
getResourceEntryName = { id ->
try {
resources.getResourceEntryName(id)
} catch (e: NotFoundException) {
null
}
})
}
fun onDumpHeapReceived(forceDump: Boolean) {
backgroundHandler.post {
dismissNoRetainedOnTapNotification()
gcTrigger.runGc()
val retainedReferenceCount = objectWatcher.retainedObjectCount
if (!forceDump && retainedReferenceCount == 0) {
SharkLog.d { "Ignoring user request to dump heap: no retained objects remaining after GC" }
@Suppress("DEPRECATION")
val builder = Notification.Builder(application)
.setContentTitle(
application.getString(R.string.leak_canary_notification_no_retained_object_title)
)
.setContentText(
application.getString(
R.string.leak_canary_notification_no_retained_object_content
)
)
.setAutoCancel(true)
.setContentIntent(NotificationReceiver.pendingIntent(application, CANCEL_NOTIFICATION))
val notification =
Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
notificationManager.notify(
R.id.leak_canary_notification_no_retained_object_on_tap, notification
)
backgroundHandler.postDelayed(
scheduleDismissNoRetainedOnTapNotification,
DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
)
lastDisplayedRetainedObjectCount = 0
return@post
}
SharkLog.d { "Dumping the heap because user requested it" }
dumpHeap(retainedReferenceCount, retry = false, "user request")
}
}
private fun checkRetainedCount(
retainedKeysCount: Int,
retainedVisibleThreshold: Int,
nopeReason: String? = null
): Boolean {
val countChanged = lastDisplayedRetainedObjectCount != retainedKeysCount
lastDisplayedRetainedObjectCount = retainedKeysCount
if (retainedKeysCount == 0) {
if (countChanged) {
SharkLog.d { "All retained objects have been garbage collected" }
onRetainInstanceListener.onEvent(NoMoreObjects)
showNoMoreRetainedObjectNotification()
}
return true
}
val applicationVisible = applicationVisible
val applicationInvisibleLessThanWatchPeriod = applicationInvisibleLessThanWatchPeriod
if (countChanged) {
val whatsNext = if (applicationVisible) {
if (retainedKeysCount < retainedVisibleThreshold) {
"not dumping heap yet (app is visible & < $retainedVisibleThreshold threshold)"
} else {
if (nopeReason != null) {
"would dump heap now (app is visible & >=$retainedVisibleThreshold threshold) but $nopeReason"
} else {
"dumping heap now (app is visible & >=$retainedVisibleThreshold threshold)"
}
}
} else if (applicationInvisibleLessThanWatchPeriod) {
val wait =
AppWatcher.retainedDelayMillis - (SystemClock.uptimeMillis() - applicationInvisibleAt)
if (nopeReason != null) {
"would dump heap in $wait ms (app just became invisible) but $nopeReason"
} else {
"dumping heap in $wait ms (app just became invisible)"
}
} else {
if (nopeReason != null) {
"would dump heap now (app is invisible) but $nopeReason"
} else {
"dumping heap now (app is invisible)"
}
}
SharkLog.d {
val s = if (retainedKeysCount > 1) "s" else ""
"Found $retainedKeysCount object$s retained, $whatsNext"
}
}
if (retainedKeysCount < retainedVisibleThreshold) {
if (applicationVisible || applicationInvisibleLessThanWatchPeriod) {
if (countChanged) {
onRetainInstanceListener.onEvent(BelowThreshold(retainedKeysCount))
}
showRetainedCountNotification(
objectCount = retainedKeysCount,
contentText = application.getString(
R.string.leak_canary_notification_retained_visible, retainedVisibleThreshold
)
)
scheduleRetainedObjectCheck(
delayMillis = WAIT_FOR_OBJECT_THRESHOLD_MILLIS
)
return true
}
}
return false
}
fun scheduleRetainedObjectCheck(
delayMillis: Long = 0L
) {
val checkCurrentlyScheduledAt = checkScheduledAt
if (checkCurrentlyScheduledAt > 0) {
return
}
checkScheduledAt = SystemClock.uptimeMillis() + delayMillis
backgroundHandler.postDelayed({
checkScheduledAt = 0
checkRetainedObjects()
}, delayMillis)
}
private fun showNoMoreRetainedObjectNotification() {
backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
if (!Notifications.canShowNotification) {
return
}
val builder = Notification.Builder(application)
.setContentTitle(
application.getString(R.string.leak_canary_notification_no_retained_object_title)
)
.setContentText(
application.getString(
R.string.leak_canary_notification_no_retained_object_content
)
)
.setAutoCancel(true)
.setContentIntent(NotificationReceiver.pendingIntent(application, CANCEL_NOTIFICATION))
val notification =
Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
backgroundHandler.postDelayed(
scheduleDismissRetainedCountNotification, DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS
)
}
private fun showRetainedCountNotification(
objectCount: Int,
contentText: String
) {
backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
if (!Notifications.canShowNotification) {
return
}
@Suppress("DEPRECATION")
val builder = Notification.Builder(application)
.setContentTitle(
application.getString(R.string.leak_canary_notification_retained_title, objectCount)
)
.setContentText(contentText)
.setAutoCancel(true)
.setContentIntent(NotificationReceiver.pendingIntent(application, DUMP_HEAP))
val notification =
Notifications.buildNotification(application, builder, LEAKCANARY_LOW)
notificationManager.notify(R.id.leak_canary_notification_retained_objects, notification)
}
private fun dismissRetainedCountNotification() {
backgroundHandler.removeCallbacks(scheduleDismissRetainedCountNotification)
notificationManager.cancel(R.id.leak_canary_notification_retained_objects)
}
private fun dismissNoRetainedOnTapNotification() {
backgroundHandler.removeCallbacks(scheduleDismissNoRetainedOnTapNotification)
notificationManager.cancel(R.id.leak_canary_notification_no_retained_object_on_tap)
}
companion object {
internal const val WAIT_AFTER_DUMP_FAILED_MILLIS = 5_000L
private const val WAIT_FOR_OBJECT_THRESHOLD_MILLIS = 2_000L
private const val DISMISS_NO_RETAINED_OBJECT_NOTIFICATION_MILLIS = 30_000L
private const val WAIT_BETWEEN_HEAP_DUMPS_MILLIS = 60_000L
}
}