Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add special case to ignore Binder leaks #351

Merged
merged 1 commit into from
Jan 6, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -176,14 +176,33 @@ private void visitRootObj(LeakNode node) {
RootObj rootObj = (RootObj) node.instance;
Instance child = rootObj.getReferredInstance();

Boolean alwaysIgnoreClassHierarchy = null;
if (child != null) {
ClassObj superClassObj = child.getClassObj();
while (superClassObj != null) {
Boolean alwaysIgnoreClass = excludedRefs.rootClassNames.get(superClassObj.getClassName());
if (alwaysIgnoreClass != null) {
// true overrides null or false.
if (alwaysIgnoreClassHierarchy == null || !alwaysIgnoreClassHierarchy) {
alwaysIgnoreClassHierarchy = alwaysIgnoreClass;
}
}
superClassObj = superClassObj.getSuperClassObj();
}

if (alwaysIgnoreClassHierarchy != null && alwaysIgnoreClassHierarchy) {
return;
}
}

if (rootObj.getRootType() == RootType.JAVA_LOCAL) {
Instance holder = HahaSpy.allocatingThread(rootObj);
// We switch the parent node with the thread instance that holds
// the local reference.
LeakNode parent = new LeakNode(holder, null, null, null);
enqueue(true, parent, child, "<Java Local>", LOCAL);
enqueue(alwaysIgnoreClassHierarchy == null, parent, child, "<Java Local>", LOCAL);
} else {
enqueue(true, node, child, null, null);
enqueue(alwaysIgnoreClassHierarchy == null, node, child, null, null);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@

import java.io.File;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.util.Arrays;
import java.util.Collection;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import static com.squareup.leakcanary.TestUtil.fileFromName;
import static com.squareup.leakcanary.TestUtil.analyze;
import static com.squareup.leakcanary.LeakTraceElement.Holder.THREAD;
import static com.squareup.leakcanary.LeakTraceElement.Type.STATIC_FIELD;
import static org.hamcrest.core.StringContains.containsString;
Expand All @@ -49,12 +50,6 @@ public class AsyncTaskLeakTest {
});
}

private static File fileFromName(String filename) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL url = classLoader.getResource(filename);
return new File(url.getPath());
}

final File heapDumpFile;
final String referenceKey;

Expand All @@ -71,7 +66,7 @@ public AsyncTaskLeakTest(File heapDumpFile, String referenceKey) {
}

@Test public void leakFound() {
AnalysisResult result = analyze();
AnalysisResult result = analyze(heapDumpFile, referenceKey, excludedRefs);
assertTrue(result.leakFound);
assertFalse(result.excludedLeak);
LeakTraceElement gcRoot = result.leakTrace.elements.get(0);
Expand All @@ -82,7 +77,7 @@ public AsyncTaskLeakTest(File heapDumpFile, String referenceKey) {

@Test public void excludeThread() {
excludedRefs.thread(ASYNC_TASK_THREAD);
AnalysisResult result = analyze();
AnalysisResult result = analyze(heapDumpFile, referenceKey, excludedRefs);
assertTrue(result.leakFound);
assertFalse(result.excludedLeak);
LeakTraceElement gcRoot = result.leakTrace.elements.get(0);
Expand All @@ -96,22 +91,9 @@ public AsyncTaskLeakTest(File heapDumpFile, String referenceKey) {
excludedRefs.thread(ASYNC_TASK_THREAD);
excludedRefs.staticField(ASYNC_TASK_CLASS, EXECUTOR_FIELD_1);
excludedRefs.staticField(ASYNC_TASK_CLASS, EXECUTOR_FIELD_2);
AnalysisResult result = analyze();
AnalysisResult result = analyze(heapDumpFile, referenceKey, excludedRefs);
assertTrue(result.leakFound);
assertTrue(result.excludedLeak);
}

private AnalysisResult analyze() {
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(excludedRefs.build());
AnalysisResult result = heapAnalyzer.checkForLeak(heapDumpFile, referenceKey);
if (result.failure != null) {
result.failure.printStackTrace();
}
if (result.leakTrace != null) {
System.out.println(result.leakTrace);
}
return result;
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.leakcanary;

import org.junit.Before;
import org.junit.Test;

import java.lang.ref.WeakReference;

import static com.squareup.leakcanary.LeakTraceElement.Holder.CLASS;
import static com.squareup.leakcanary.LeakTraceElement.Holder.OBJECT;
import static com.squareup.leakcanary.LeakTraceElement.Type.INSTANCE_FIELD;
import static com.squareup.leakcanary.LeakTraceElement.Type.STATIC_FIELD;
import static com.squareup.leakcanary.TestUtil.analyze;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
* leak_service_binder_ignored.hprof contains a "normal" leak when binding to a service, where
* leak_service_binder.hprof contains a leak where a binder is leaked by a static field.
*/
public class ServiceBinderLeakTest {

ExcludedRefs.Builder excludedRefs;

@Before public void setUp() {
excludedRefs = new ExcludedRefs.Builder()
.clazz(WeakReference.class.getName(), true)
.clazz("java.lang.ref.FinalizerReference", true);
}

@Test public void realBinderLeak() {
excludedRefs.rootClass("android.os.Binder", true);

AnalysisResult result = analyze(
TestUtil.fileFromName("leak_service_binder.hprof"),
"b3abfae6-2c53-42e1-b8c1-96b0558dbeae",
excludedRefs
);

assertTrue(result.leakFound);
assertFalse(result.excludedLeak);
LeakTraceElement gcRoot = result.leakTrace.elements.get(0);
assertEquals(STATIC_FIELD, gcRoot.type);
assertEquals("com.example.leakcanary.LeakyService", gcRoot.className);
assertEquals(CLASS, gcRoot.holder);
}

@Test public void ignorableBinderLeak() {
excludedRefs.rootClass("android.os.Binder", false);

AnalysisResult result = analyze(
TestUtil.fileFromName("leak_service_binder_ignored.hprof"),
"6e524414-9581-4ce7-8690-e8ddf8b82454",
excludedRefs
);

assertTrue(result.leakFound);
assertTrue(result.excludedLeak);
LeakTraceElement gcRoot = result.leakTrace.elements.get(0);
assertEquals(INSTANCE_FIELD, gcRoot.type);
assertEquals("com.example.leakcanary.LeakyService$MyBinder", gcRoot.className);
assertEquals(OBJECT, gcRoot.holder);
}

@Test public void alwaysIgnorableBinderLeak() {
excludedRefs.rootClass("android.os.Binder", true);

AnalysisResult result = analyze(
TestUtil.fileFromName("leak_service_binder_ignored.hprof"),
"6e524414-9581-4ce7-8690-e8ddf8b82454",
excludedRefs
);

assertFalse(result.leakFound);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.squareup.leakcanary;

import java.io.File;
import java.net.URL;

class TestUtil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

final

static File fileFromName(String filename) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
URL url = classLoader.getResource(filename);
return new File(url.getPath());
}

static AnalysisResult analyze(File heapDumpFile, String referenceKey, ExcludedRefs.Builder excludedRefs) {
HeapAnalyzer heapAnalyzer = new HeapAnalyzer(excludedRefs.build());
AnalysisResult result = heapAnalyzer.checkForLeak(heapDumpFile, referenceKey);
if (result.failure != null) {
result.failure.printStackTrace();
}
if (result.leakTrace != null) {
System.out.println(result.leakTrace);
}
return result;
}

private TestUtil() {
throw new AssertionError();
}
}
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,16 @@ public enum AndroidExcludedRefs {
}
},

SERVICE_BINDER {
@Override void add(ExcludedRefs.Builder excluded) {
// We should ignore leaks where an android.os.Binder is the root of the leak.
// When you bind and unbind from a Service, the OS will keep a reference to the Binder
// until the client binder has been GC'ed. This means the Binder can be retained after
// Service.onDestroy() is called.
excluded.rootClass("android.os.Binder", true);
}
},

SOFT_REFERENCES {
@Override void add(ExcludedRefs.Builder excluded) {
excluded.clazz(WeakReference.class.getName(), true);
Expand Down Expand Up @@ -412,7 +422,7 @@ public enum AndroidExcludedRefs {
*/
public static ExcludedRefs.Builder createAndroidDefaults() {
return createBuilder(EnumSet.of(SOFT_REFERENCES, FINALIZER_WATCHDOG_DAEMON, MAIN, LEAK_CANARY_THREAD,
EVENT_RECEIVER__MMESSAGE_QUEUE));
EVENT_RECEIVER__MMESSAGE_QUEUE, SERVICE_BINDER));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,19 @@ public final class ExcludedRefs implements Serializable {
public final Map<String, Map<String, Boolean>> staticFieldNameByClassName;
public final Map<String, Boolean> threadNames;
public final Map<String, Boolean> classNames;
public final Map<String, Boolean> rootClassNames;

private ExcludedRefs(Map<String, Map<String, Boolean>> fieldNameByClassName,
Map<String, Map<String, Boolean>> staticFieldNameByClassName,
Map<String, Boolean> threadNames, Map<String, Boolean> classNames) {
Map<String, Boolean> threadNames, Map<String, Boolean> classNames,
Map<String, Boolean> rootClassNames) {
// Copy + unmodifiable.
this.fieldNameByClassName = unmodifiableMap(new LinkedHashMap<>(fieldNameByClassName));
this.staticFieldNameByClassName =
unmodifiableMap(new LinkedHashMap<>(staticFieldNameByClassName));
this.threadNames = unmodifiableMap(new LinkedHashMap<>(threadNames));
this.classNames = unmodifiableMap(new LinkedHashMap<>(classNames));
this.rootClassNames = unmodifiableMap(new LinkedHashMap<>(rootClassNames));
}

@Override public String toString() {
Expand All @@ -72,6 +75,10 @@ private ExcludedRefs(Map<String, Map<String, Boolean>> fieldNameByClassName,
String always = clazz.getValue() ? " (always)" : "";
string += "| Class:" + clazz.getKey() + always + "\n";
}
for (Map.Entry<String, Boolean> clazz : rootClassNames.entrySet()) {
String always = clazz.getValue() ? " (always)" : "";
string += "| Root Class:" + clazz.getKey() + always + "\n";
}
return string;
}

Expand All @@ -81,6 +88,7 @@ public static final class Builder {
new LinkedHashMap<>();
private final Map<String, Boolean> threadNames = new LinkedHashMap<>();
private final Map<String, Boolean> classNames = new LinkedHashMap<>();
private final Map<String, Boolean> rootClassNames = new LinkedHashMap<>();

public Builder instanceField(String className, String fieldName) {
return instanceField(className, fieldName, false);
Expand Down Expand Up @@ -134,9 +142,19 @@ public Builder clazz(String className, boolean always) {
return this;
}

public Builder rootClass(String rootClassName) {
return rootClass(rootClassName, false);
}

public Builder rootClass(String rootClassName, boolean always) {
checkNotNull(rootClassName, "rootClassName");
rootClassNames.put(rootClassName, always);
return this;
}

public ExcludedRefs build() {
return new ExcludedRefs(fieldNameByClassName, staticFieldNameByClassName, threadNames,
classNames);
classNames, rootClassNames);
}
}
}