Skip to content

Commit 64b6e5a

Browse files
authored
Fix GC Root bugs that missed leaks (#1168)
In Perflib, Snapshot.getGcRoots() only returns the GC roots from the first heap. Based on empirical evidence, it seems like Android has 4 heaps, and the GC Roots used to only be in the first heap, but more recent versions of Android have GC Roots in other heaps as well. This means LeakCanary would sometimes show "No Leak Found" when there actually was a leak. Fixes #1163
1 parent bd3de26 commit 64b6e5a

File tree

7 files changed

+44
-15
lines changed

7 files changed

+44
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## Future release
44

5+
* [#1163](https://github.com/square/leakcanary/issues/1163) Fixed leaks being incorrectly classified as "no leak" due to missed GC Roots.
56
* [#1153](https://github.com/square/leakcanary/issues/1153) `LeakCanary.isInAnalyzerProcess` now correctly returns true in the analyzer process prior to any first leak (could be triggered by starting the leak result activity).
67
* [#1158](https://github.com/square/leakcanary/issues/1158) Stopped enabling DisplayLeakActivity when not using DisplayLeakService.
78
* [#1135](https://github.com/square/leakcanary/issues/1135) Fixed IndexOutOfBoundsException for leak traces of size 1.

leakcanary-analyzer/src/main/java/com/squareup/haha/perflib/HahaSpy.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package com.squareup.haha.perflib;
1717

1818
import android.support.annotation.NonNull;
19+
import java.util.HashSet;
20+
import java.util.Set;
1921

2022
public final class HahaSpy {
2123

@@ -31,6 +33,18 @@ public final class HahaSpy {
3133
return snapshot.findInstance(thread.mId);
3234
}
3335

36+
/**
37+
* Returns the GC Roots for all heaps in the Snapshot. Unfortunately,
38+
* {@link Snapshot#getGCRoots()} only returns the GC Roots of the first heap.
39+
*/
40+
public static Set<RootObj> allGcRoots(Snapshot snapshot) {
41+
Set<RootObj> allRoots = new HashSet<>();
42+
for (Heap heap : snapshot.getHeaps()) {
43+
allRoots.addAll(heap.mRoots);
44+
}
45+
return allRoots;
46+
}
47+
3448
private HahaSpy() {
3549
throw new AssertionError();
3650
}

leakcanary-analyzer/src/main/java/com/squareup/leakcanary/ShortestPathFinder.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ private void clearState() {
129129
}
130130

131131
private void enqueueGcRoots(Snapshot snapshot) {
132-
for (RootObj rootObj : snapshot.getGCRoots()) {
132+
for (RootObj rootObj : HahaSpy.allGcRoots(snapshot)) {
133133
switch (rootObj.getRootType()) {
134134
case JAVA_LOCAL:
135135
Instance thread = HahaSpy.allocatingThread(rootObj);

leakcanary-analyzer/src/test/java/com/squareup/leakcanary/TrackedReferencesTest.java renamed to leakcanary-analyzer/src/test/java/com/squareup/leakcanary/HeapAnalyzerHeapDumpTest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
import org.junit.runner.RunWith;
66
import org.junit.runners.JUnit4;
77

8-
import static com.squareup.leakcanary.TestUtil.HeapDumpFile.ASYNC_TASK_PRE_M;
98
import static com.squareup.leakcanary.TestUtil.HeapDumpFile.ASYNC_TASK_M;
9+
import static com.squareup.leakcanary.TestUtil.HeapDumpFile.ASYNC_TASK_PRE_M;
10+
import static com.squareup.leakcanary.TestUtil.HeapDumpFile.GC_ROOT_IN_NON_PRIMARY_HEAP;
11+
import static com.squareup.leakcanary.TestUtil.analyze;
1012
import static com.squareup.leakcanary.TestUtil.findTrackedReferences;
1113
import static org.assertj.core.api.Assertions.assertThat;
1214

1315
@RunWith(JUnit4.class)
14-
public class TrackedReferencesTest {
16+
public class HeapAnalyzerHeapDumpTest {
1517

1618
@Test public void findsExpectedRef() {
1719
List<TrackedReference> trackedReferences = findTrackedReferences(ASYNC_TASK_M);
@@ -25,4 +27,9 @@ public class TrackedReferencesTest {
2527
List<TrackedReference> trackedReferences = findTrackedReferences(ASYNC_TASK_PRE_M);
2628
assertThat(trackedReferences).hasSize(2);
2729
}
30+
31+
@Test public void leakFoundWithGcRootInNonPrimaryHeap() {
32+
AnalysisResult result = analyze(GC_ROOT_IN_NON_PRIMARY_HEAP);
33+
assertThat(result.leakFound).isTrue();
34+
}
2835
}

leakcanary-analyzer/src/test/java/com/squareup/leakcanary/RetainedSizeTest.java

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package com.squareup.leakcanary;
22

3-
import java.lang.ref.WeakReference;
43
import java.util.Arrays;
54
import java.util.Collection;
6-
import org.junit.Before;
75
import org.junit.Test;
86
import org.junit.runner.RunWith;
97
import org.junit.runners.Parameterized;
@@ -30,22 +28,14 @@ public class RetainedSizeTest {
3028

3129
private final TestUtil.HeapDumpFile heapDumpFile;
3230
private final long expectedRetainedHeapSize;
33-
ExcludedRefs.BuilderWithParams excludedRefs;
3431

3532
public RetainedSizeTest(TestUtil.HeapDumpFile heapDumpFile, long expectedRetainedHeapSize) {
3633
this.heapDumpFile = heapDumpFile;
3734
this.expectedRetainedHeapSize = expectedRetainedHeapSize;
3835
}
3936

40-
@Before public void setUp() {
41-
excludedRefs = new ExcludedRefs.BuilderWithParams().clazz(WeakReference.class.getName())
42-
.alwaysExclude()
43-
.clazz("java.lang.ref.FinalizerReference")
44-
.alwaysExclude();
45-
}
46-
4737
@Test public void leakFound() {
48-
AnalysisResult result = analyze(heapDumpFile, excludedRefs);
38+
AnalysisResult result = analyze(heapDumpFile);
4939
assertEquals(expectedRetainedHeapSize, result.retainedHeapSize);
5040
}
5141
}

leakcanary-analyzer/src/test/java/com/squareup/leakcanary/TestUtil.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
package com.squareup.leakcanary;
1717

1818
import java.io.File;
19+
import java.lang.ref.PhantomReference;
20+
import java.lang.ref.SoftReference;
21+
import java.lang.ref.WeakReference;
1922
import java.net.URL;
2023
import java.util.Collections;
2124
import java.util.List;
@@ -27,7 +30,9 @@ final class TestUtil {
2730
enum HeapDumpFile {
2831
ASYNC_TASK_PRE_M("leak_asynctask_pre_m.hprof", "dc983a12-d029-4003-8890-7dd644c664c5"), //
2932
ASYNC_TASK_M("leak_asynctask_m.hprof", "25ae1778-7c1d-4ec7-ac50-5cce55424069"), //
30-
ASYNC_TASK_O("leak_asynctask_o.hprof", "0e8d40d7-8302-4493-93d5-962a4c176089");
33+
ASYNC_TASK_O("leak_asynctask_o.hprof", "0e8d40d7-8302-4493-93d5-962a4c176089"),
34+
GC_ROOT_IN_NON_PRIMARY_HEAP("gc_root_in_non_primary_heap.hprof",
35+
"10a5bc66-e9cb-430c-930a-fc1dc4fc0f85");
3136

3237
public final String filename;
3338
public final String referenceKey;
@@ -52,6 +57,18 @@ static List<TrackedReference> findTrackedReferences(HeapDumpFile heapDumpFile) {
5257
return heapAnalyzer.findTrackedReferences(file);
5358
}
5459

60+
static AnalysisResult analyze(HeapDumpFile heapDumpFile) {
61+
ExcludedRefs.BuilderWithParams excludedRefs = new ExcludedRefs.BuilderWithParams()
62+
.clazz(WeakReference.class.getName()).alwaysExclude()
63+
.clazz(SoftReference.class.getName()).alwaysExclude()
64+
.clazz(PhantomReference.class.getName()).alwaysExclude()
65+
.clazz("java.lang.ref.Finalizer").alwaysExclude()
66+
.clazz("java.lang.ref.FinalizerReference").alwaysExclude()
67+
.thread("FinalizerWatchdogDaemon").alwaysExclude()
68+
.thread("main").alwaysExclude();
69+
return analyze(heapDumpFile, excludedRefs);
70+
}
71+
5572
static AnalysisResult analyze(HeapDumpFile heapDumpFile,
5673
ExcludedRefs.BuilderWithParams excludedRefs) {
5774
File file = fileFromName(heapDumpFile.filename);
Binary file not shown.

0 commit comments

Comments
 (0)