Skip to content

Commit

Permalink
Object watcher description (#1658)
Browse files Browse the repository at this point in the history
Originally KeyedWeakReference had a "name" field that wasn't used out of the box but could be useful to add identifying information. This PR renames it to "description" and uses it for automatic watching, surfacing more clearly why an object is watched.

Related to #1656
  • Loading branch information
pyricau authored Dec 9, 2019
1 parent de916b3 commit 599011e
Show file tree
Hide file tree
Showing 16 changed files with 62 additions and 43 deletions.
2 changes: 1 addition & 1 deletion docs/fundamentals.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Memory leaks are very common in Android apps and the accumulation of small memor
The foundation of LeakCanary is a library called *ObjectWatcher Android*. It hooks into the Android lifecycle to automatically detect when activities and fragments are destroyed and should be garbage collected. These destroyed objects are passed to an `ObjectWatcher`, which holds **weak references** to them. You can also watch any objects that is no longer needed, for example a detached view, a destroyed presenter, etc.

```kotlin
AppWatcher.objectWatcher.watch(myDetachedView)
AppWatcher.objectWatcher.watch(myDetachedView, "View was detached")
```

If the weak references aren't cleared after **waiting 5 seconds** and running the garbage collector, the watched objects are considered **retained**, and potentially leaking. LeakCanary logs this to Logcat:
Expand Down
2 changes: 1 addition & 1 deletion docs/recipes.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class MyService : Service {

override fun onDestroy() {
super.onDestroy()
AppWatcher.objectWatcher.watch(this)
AppWatcher.objectWatcher.watch(this, "MyService received Service#onDestroy() callback")
}
}
```
Expand Down
8 changes: 4 additions & 4 deletions docs/upgrading-to-leakcanary-2.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,18 +103,18 @@ val retainedObjectCount = AppWatcher.objectWatcher.retainedObjectCount
```kotlin
// In shared code
interface MaybeObjectWatcher {
fun watch(watchedObject: Any)
fun watch(watchedObject: Any, description: String)

object None : MaybeObjectWatcher {
override fun watch(watchedObject: Any) {
override fun watch(watchedObject: Any, description: String) {
}
}
}

// In debug code
class RealObjectWatcher : MaybeObjectWatcher {
override fun watch(watchedObject: Any) {
AppWatcher.objectWatcher.watch(watchedObject)
override fun watch(watchedObject: Any, description: String) {
AppWatcher.objectWatcher.watch(watchedObject, description)
}
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class InstrumentationLeakDetectorTest {
@Test fun detectsLeak() {
leaking = Date()
val refWatcher = AppWatcher.objectWatcher
refWatcher.watch(leaking)
refWatcher.watch(leaking, "This date should not live beyond the test")
assertLeak(Date::class.java)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ internal class AndroidXFragmentDestroyWatcher(
) {
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
objectWatcher.watch(view)
objectWatcher.watch(
view, "Fragment received Fragment#onDestroyView() callback " +
"(references to its view should be cleared to prevent leaks)"
)
}
}

Expand All @@ -44,7 +47,7 @@ internal class AndroidXFragmentDestroyWatcher(
fragment: Fragment
) {
if (configProvider().watchFragments) {
objectWatcher.watch(fragment)
objectWatcher.watch(fragment, "Fragment received Fragment#onDestroy() callback")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ internal class AndroidSupportFragmentDestroyWatcher(
) {
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
objectWatcher.watch(view)
objectWatcher.watch(
view, "Fragment received Fragment#onDestroyView() callback " +
"(references to its view should be cleared to prevent leaks)"
)
}
}

Expand All @@ -44,7 +47,7 @@ internal class AndroidSupportFragmentDestroyWatcher(
fragment: Fragment
) {
if (configProvider().watchFragments) {
objectWatcher.watch(fragment)
objectWatcher.watch(fragment, "Fragment received Fragment#onDestroy() callback")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ internal class ActivityDestroyWatcher private constructor(
object : Application.ActivityLifecycleCallbacks by noOpDelegate() {
override fun onActivityDestroyed(activity: Activity) {
if (configProvider().watchActivities) {
objectWatcher.watch(activity)
objectWatcher.watch(activity, "Activity received Activity#onDestroy() callback")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ internal class AndroidOFragmentDestroyWatcher(
) {
val view = fragment.view
if (view != null && configProvider().watchFragmentViews) {
objectWatcher.watch(view)
objectWatcher.watch(
view, "Fragment received Fragment#onDestroyView() callback " +
"(references to its view should be cleared to prevent leaks)"
)
}
}

Expand All @@ -46,7 +49,7 @@ internal class AndroidOFragmentDestroyWatcher(
fragment: Fragment
) {
if (configProvider().watchFragments) {
objectWatcher.watch(fragment)
objectWatcher.watch(fragment, "Fragment received Fragment#onDestroy() callback")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import java.lang.ref.WeakReference
class KeyedWeakReference(
referent: Any,
val key: String,
val name: String,
val description: String,
val watchUptimeMillis: Long,
referenceQueue: ReferenceQueue<Any>
) : WeakReference<Any>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,18 +112,24 @@ class ObjectWatcher constructor(
/**
* Identical to [watch] with an empty string reference name.
*/
@Deprecated(
"Add description parameter explaining why an object is watched to help understand leak traces.",
replaceWith = ReplaceWith(
"watch(watchedObject, \"Explain why this object should be garbage collected soon\")"
)
)
@Synchronized fun watch(watchedObject: Any) {
watch(watchedObject, "")
}

/**
* Watches the provided [watchedObject].
*
* @param name A logical identifier for the watched object.
* @param description Describes why the object is watched.
*/
@Synchronized fun watch(
watchedObject: Any,
name: String
description: String
) {
if (!isEnabled()) {
return
Expand All @@ -133,12 +139,12 @@ class ObjectWatcher constructor(
.toString()
val watchUptimeMillis = clock.uptimeMillis()
val reference =
KeyedWeakReference(watchedObject, key, name, watchUptimeMillis, queue)
KeyedWeakReference(watchedObject, key, description, watchUptimeMillis, queue)
SharkLog.d {
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (name.isNotEmpty()) " named $name" else "") +
" with key $key"
"Watching " +
(if (watchedObject is Class<*>) watchedObject.toString() else "instance of ${watchedObject.javaClass.name}") +
(if (description.isNotEmpty()) " ($description)" else "") +
" with key $key"
}

watchedObjects[key] = reference
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ class ObjectWatcherTest {
var ref: Any? = Any()

@Test fun `unreachable object not retained`() {
objectWatcher.watch(ref!!)
objectWatcher.watch(ref!!, "unreachable object not retained")
ref = null
runGc()
assertThat(objectWatcher.hasRetainedObjects).isFalse()
}

@Test fun `reachable object retained`() {
objectWatcher.watch(ref!!)
objectWatcher.watch(ref!!, "reachable object retained")
runGc()
assertThat(objectWatcher.hasRetainedObjects).isTrue()
}
Expand Down
9 changes: 5 additions & 4 deletions shark/src/main/java/shark/ObjectInspectors.kt
Original file line number Diff line number Diff line change
Expand Up @@ -61,11 +61,12 @@ enum class ObjectInspectors : ObjectInspector {
val objectId = reporter.heapObject.objectId
references.forEach { ref ->
if (ref.referent.value == objectId) {
reporter.leakingReasons += "ObjectWatcher was watching this"
reporter.labels += "key = ${ref.key}"
if (ref.name.isNotEmpty()) {
reporter.labels += "name = ${ref.name}"
reporter.leakingReasons += if (ref.description.isNotEmpty()) {
"ObjectWatcher was watching this because ${ref.description}"
} else {
"ObjectWatcher was watching this"
}
reporter.labels += "key = ${ref.key}"
if (ref.watchDurationMillis != null) {
reporter.labels += "watchDurationMillis = ${ref.watchDurationMillis}"
}
Expand Down
15 changes: 9 additions & 6 deletions shark/src/main/java/shark/internal/KeyedWeakReferenceMirror.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ internal class KeyedWeakReferenceMirror(
val referent: ReferenceHolder,
val key: String,
// The name field does not exist in pre 1.0 heap dumps.
val name: String,
// null in pre 2.0 alpha 3 heap dumps
val description: String,
// null in pre 2.0 alpha 3 heap dumps
val watchDurationMillis: Long?,
// null in pre 2.0 alpha 3 heap dumps, -1 if the instance is not retained.
val retainedDurationMillis: Long?
Expand All @@ -25,7 +25,7 @@ internal class KeyedWeakReferenceMirror(

fun fromInstance(
weakRef: HeapInstance,
// Null for pre 2.0 alpha 3 heap dumps
// Null for pre 2.0 alpha 3 heap dumps
heapDumpUptimeMillis: Long?
): KeyedWeakReferenceMirror {

Expand All @@ -37,21 +37,24 @@ internal class KeyedWeakReferenceMirror(
}

val retainedDurationMillis = if (heapDumpUptimeMillis != null) {
val retainedUptimeMillis = weakRef[keyWeakRefClassName, "retainedUptimeMillis"]!!.value.asLong!!
val retainedUptimeMillis =
weakRef[keyWeakRefClassName, "retainedUptimeMillis"]!!.value.asLong!!
if (retainedUptimeMillis == -1L) -1L else heapDumpUptimeMillis - retainedUptimeMillis
} else {
null
}

val keyString = weakRef[keyWeakRefClassName, "key"]!!.value.readAsJavaString()!!

val name = weakRef[keyWeakRefClassName, "name"]?.value?.readAsJavaString() ?: UNKNOWN_LEGACY
// Changed from name to description after 2.0
val description = (weakRef[keyWeakRefClassName, "description"]
?: weakRef[keyWeakRefClassName, "name"])?.value?.readAsJavaString() ?: UNKNOWN_LEGACY
return KeyedWeakReferenceMirror(
watchDurationMillis = watchDurationMillis,
retainedDurationMillis = retainedDurationMillis,
referent = weakRef["java.lang.ref.Reference", "referent"]!!.value.holder as ReferenceHolder,
key = keyString,
name = name
description = description
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion shark/src/test/java/shark/HprofWriterHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ class HprofWriterHelper constructor(
classId = keyedWeakReferenceClassId,
fields = listOf(
referenceKey,
string(""),
string("its lifecycle has ended"),
LongHolder(5000),
LongHolder(20000),
ReferenceHolder(referentInstanceId.value)
Expand Down
4 changes: 2 additions & 2 deletions shark/src/test/java/shark/LeakStatusTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ class LeakStatusTest {
val leak = analysis.applicationLeaks[0]
assertThat(leak.leakTrace.elements.last().leakStatus).isEqualTo(NOT_LEAKING)
assertThat(leak.leakTrace.elements.last().leakStatusReason).isEqualTo(
"Leaking is not leaking. Conflicts with ObjectWatcher was watching this"
"Leaking is not leaking. Conflicts with ObjectWatcher was watching this because its lifecycle has ended"
)
}

Expand All @@ -241,7 +241,7 @@ class LeakStatusTest {
val leak = analysis.applicationLeaks[0]
assertThat(leak.leakTrace.elements.last().leakStatus).isEqualTo(LEAKING)
assertThat(leak.leakTrace.elements.last().leakStatusReason).isEqualTo(
"Leaking is leaking and ObjectWatcher was watching this"
"Leaking is leaking and ObjectWatcher was watching this because its lifecycle has ended"
)
}

Expand Down
12 changes: 6 additions & 6 deletions shark/src/test/java/shark/LeakTraceStringRenderingTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class LeakTraceStringRenderingTest {
│ ↓ static GcRoot.leak
│ ~~~~
╰→ Leaking
​ Leaking: YES (ObjectWatcher was watching this)
​ Leaking: YES (ObjectWatcher was watching this because its lifecycle has ended)
​ key = 39efcc1a-67bf-2040-e7ab-3fc9f94731dc
​ watchDurationMillis = 25000
​ retainedDurationMillis = 10000
Expand Down Expand Up @@ -69,7 +69,7 @@ class LeakTraceStringRenderingTest {
│ ↓ static GcRoot.leak
│ ~~~~
╰→ Leaking
​ Leaking: YES (ObjectWatcher was watching this)
​ Leaking: YES (ObjectWatcher was watching this because its lifecycle has ended)
​ key = 39efcc1a-67bf-2040-e7ab-3fc9f94731dc
​ watchDurationMillis = 25000
​ retainedDurationMillis = 10000
Expand Down Expand Up @@ -143,7 +143,7 @@ class LeakTraceStringRenderingTest {
│ ↓ static GcRoot.leak
│ ~~~~
╰→ Leaking
​ Leaking: YES (ObjectWatcher was watching this)
​ Leaking: YES (ObjectWatcher was watching this because its lifecycle has ended)
​ ¯\_(ツ)_/¯
​ key = 39efcc1a-67bf-2040-e7ab-3fc9f94731dc
​ watchDurationMillis = 25000
Expand Down Expand Up @@ -179,7 +179,7 @@ class LeakTraceStringRenderingTest {
│ ↓ ClassA.leak
│ ~~~~
╰→ Leaking
​ Leaking: YES (ObjectWatcher was watching this)
​ Leaking: YES (ObjectWatcher was watching this because its lifecycle has ended)
​ key = 39efcc1a-67bf-2040-e7ab-3fc9f94731dc
​ watchDurationMillis = 25000
​ retainedDurationMillis = 10000
Expand Down Expand Up @@ -208,7 +208,7 @@ class LeakTraceStringRenderingTest {
│ ↓ array Object[].[0]
│ ~~~
╰→ Leaking
​ Leaking: YES (ObjectWatcher was watching this)
​ Leaking: YES (ObjectWatcher was watching this because its lifecycle has ended)
​ key = 39efcc1a-67bf-2040-e7ab-3fc9f94731dc
​ watchDurationMillis = 25000
​ retainedDurationMillis = 10000
Expand All @@ -229,7 +229,7 @@ class LeakTraceStringRenderingTest {
│ ↓ thread MyThread.<Java Local>
│ ~~~~~~~~~~~~
╰→ Leaking
​ Leaking: YES (ObjectWatcher was watching this)
​ Leaking: YES (ObjectWatcher was watching this because its lifecycle has ended)
​ key = 39efcc1a-67bf-2040-e7ab-3fc9f94731dc
​ watchDurationMillis = 25000
​ retainedDurationMillis = 10000
Expand Down

0 comments on commit 599011e

Please sign in to comment.