SysUiLeaks: Add LeakDetector
Bug: 34932615
Test: runtest -x $ANDROID_BUILD_TOP/frameworks/base/packages/SystemUI/tests/src/com/android/systemui/util/leak/
Change-Id: Id4a9cbdaddc83ca3e25923eec33d53b6761c54cb
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index e1f3176..2072fd4 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -59,6 +59,7 @@
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.leak.LeakDetector;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -180,6 +181,8 @@
mProviders.put(SecurityController.class.getName(), () ->
new SecurityControllerImpl(mContext));
+ mProviders.put(LeakDetector.class.getName(), LeakDetector::create);
+
mProviders.put(TunerService.class.getName(), () ->
new TunerService(mContext));
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/AbstractCollection.java b/packages/SystemUI/src/com/android/systemui/util/leak/AbstractCollection.java
new file mode 100644
index 0000000..1534def
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/AbstractCollection.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import java.util.Collection;
+import java.util.Iterator;
+
+abstract class AbstractCollection<T> implements Collection<T> {
+ @Override
+ public abstract int size();
+
+ @Override
+ public abstract boolean isEmpty();
+
+ @Override
+ public boolean contains(Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Iterator<T> iterator() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object[] toArray() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public <T1> T1[] toArray(T1[] t1s) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean add(T t) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean containsAll(Collection<?> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean addAll(Collection<? extends T> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean removeAll(Collection<?> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean retainAll(Collection<?> collection) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void clear() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
new file mode 100644
index 0000000..2124f90
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import android.os.Build;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.systemui.Dumpable;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Collection;
+
+/**
+ * Detects leaks.
+ */
+public class LeakDetector implements Dumpable {
+
+ private static final boolean ENABLED = Build.IS_DEBUGGABLE;
+
+ private final TrackedCollections mTrackedCollections;
+ private final TrackedGarbage mTrackedGarbage;
+ private final TrackedObjects mTrackedObjects;
+
+ @VisibleForTesting
+ public LeakDetector(TrackedCollections trackedCollections,
+ TrackedGarbage trackedGarbage,
+ TrackedObjects trackedObjects) {
+ mTrackedCollections = trackedCollections;
+ mTrackedGarbage = trackedGarbage;
+ mTrackedObjects = trackedObjects;
+ }
+
+ /**
+ * Tracks an instance that has a high leak risk (i.e. has complex ownership and references
+ * a large amount of memory).
+ *
+ * The LeakDetector will monitor and keep weak references to such instances, dump statistics
+ * about them in a bugreport, and in the future dump the heap if their count starts growing
+ * unreasonably.
+ *
+ * This should be called when the instance is first constructed.
+ */
+ public <T> void trackInstance(T object) {
+ if (mTrackedObjects != null) {
+ mTrackedObjects.track(object);
+ }
+ }
+
+ /**
+ * Tracks a collection that is at risk of leaking large objects, e.g. a collection of
+ * dynamically registered listeners.
+ *
+ * The LeakDetector will monitor and keep weak references to such collections, dump
+ * statistics about them in a bugreport, and in the future dump the heap if their size starts
+ * growing unreasonably.
+ *
+ * This should be called whenever the collection grows.
+ *
+ * @param tag A tag for labeling the collection in a bugreport
+ */
+ public <T> void trackCollection(Collection<T> collection, String tag) {
+ if (mTrackedCollections != null) {
+ mTrackedCollections.track(collection, tag);
+ }
+ }
+
+ /**
+ * Tracks an instance that should become garbage soon.
+ *
+ * The LeakDetector will monitor and keep weak references to such garbage, dump
+ * statistics about them in a bugreport, and in the future dump the heap if it is not
+ * collected reasonably soon.
+ *
+ * This should be called when the last strong reference to the instance is dropped.
+ */
+ public void trackGarbage(Object o) {
+ if (mTrackedGarbage != null) {
+ mTrackedGarbage.track(o);
+ }
+ }
+
+ @Override
+ public void dump(FileDescriptor df, PrintWriter w, String[] args) {
+ IndentingPrintWriter pw = new IndentingPrintWriter(w, " ");
+
+ pw.println("SYSUI LEAK DETECTOR");
+ pw.increaseIndent();
+
+ if (mTrackedCollections != null && mTrackedGarbage != null) {
+ pw.println("TrackedCollections:");
+ pw.increaseIndent();
+ mTrackedCollections.dump(pw, (col) -> !TrackedObjects.isTrackedObject(col));
+ pw.decreaseIndent();
+ pw.println();
+
+ pw.println("TrackedObjects:");
+ pw.increaseIndent();
+ mTrackedCollections.dump(pw, TrackedObjects::isTrackedObject);
+ pw.decreaseIndent();
+ pw.println();
+
+ pw.print("TrackedGarbage:");
+ pw.increaseIndent();
+ mTrackedGarbage.dump(pw);
+ pw.decreaseIndent();
+ } else {
+ pw.println("disabled");
+ }
+ pw.decreaseIndent();
+ pw.println();
+ }
+
+ public static LeakDetector create() {
+ if (ENABLED) {
+ TrackedCollections collections = new TrackedCollections();
+ return new LeakDetector(collections, new TrackedGarbage(collections),
+ new TrackedObjects(collections));
+ } else {
+ return new LeakDetector(null, null, null);
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedCollections.java b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedCollections.java
new file mode 100644
index 0000000..5577daf
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedCollections.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import android.os.SystemClock;
+
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.Collection;
+import java.util.Map;
+import java.util.function.Predicate;
+
+/**
+ * Tracks the size of collections.
+ */
+public class TrackedCollections {
+ private static final long MILLIS_IN_MINUTE = 60 * 1000;
+ private static final long HALFWAY_DELAY = 30 * MILLIS_IN_MINUTE;
+
+ private final WeakIdentityHashMap<Collection<?>, CollectionState> mCollections
+ = new WeakIdentityHashMap<>();
+
+ /**
+ * @see LeakDetector#trackCollection(Collection, String)
+ */
+ public synchronized void track(Collection<?> collection, String tag) {
+ CollectionState collectionState = mCollections.get(collection);
+ if (collectionState == null) {
+ collectionState = new CollectionState();
+ collectionState.tag = tag;
+ collectionState.startUptime = SystemClock.uptimeMillis();
+ mCollections.put(collection, collectionState);
+ }
+ if (collectionState.halfwayCount == -1
+ && SystemClock.uptimeMillis() - collectionState.startUptime > HALFWAY_DELAY) {
+ collectionState.halfwayCount = collectionState.lastCount;
+ }
+ collectionState.lastCount = collection.size();
+ collectionState.lastUptime = SystemClock.uptimeMillis();
+ }
+
+ private static class CollectionState {
+ String tag;
+ long startUptime;
+ /** The number of elements in the collection at startUptime + HALFWAY_DELAY */
+ int halfwayCount = -1;
+ /** The number of elements in the collection at lastUptime */
+ int lastCount = -1;
+ long lastUptime;
+
+ /**
+ * Dump statistics about the tracked collection:
+ * - the tag
+ * - average elements inserted per hour during
+ * - the first 30min of its existence
+ * - after the first 30min
+ * - overall
+ * - the current size of the collection
+ */
+ void dump(PrintWriter pw) {
+ long now = SystemClock.uptimeMillis();
+
+ pw.format("%s: %.2f (start-30min) / %.2f (30min-now) / %.2f (start-now)"
+ + " (growth rate in #/hour); %d (current size)",
+ tag,
+ ratePerHour(startUptime, 0, startUptime + HALFWAY_DELAY, halfwayCount),
+ ratePerHour(startUptime + HALFWAY_DELAY, halfwayCount, now, lastCount),
+ ratePerHour(startUptime, 0, now, lastCount),
+ lastCount);
+ }
+
+ private float ratePerHour(long uptime1, int count1, long uptime2, int count2) {
+ if (uptime1 >= uptime2 || count1 < 0 || count2 < 0) {
+ return Float.NaN;
+ }
+ return ((float) count2 - count1) / (uptime2 - uptime1) * 60 * MILLIS_IN_MINUTE;
+ }
+ }
+
+ public synchronized void dump(PrintWriter pw, Predicate<Collection<?>> filter) {
+ for (Map.Entry<WeakReference<Collection<?>>, CollectionState> entry
+ : mCollections.entrySet()) {
+ Collection<?> key = entry.getKey().get();
+ if (filter == null || key != null && filter.test(key)) {
+ entry.getValue().dump(pw);
+ pw.println();
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java
new file mode 100644
index 0000000..fd59aee
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import android.os.SystemClock;
+import android.util.ArrayMap;
+
+import java.io.PrintWriter;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.HashSet;
+import java.util.Map;
+
+/**
+ * Tracks objects that have been marked as garbage.
+ */
+public class TrackedGarbage {
+
+ /** Duration after which we consider garbage to be old. */
+ private static final long GARBAGE_COLLECTION_DEADLINE_MILLIS = 60000; // 1min
+
+ private final HashSet<LeakReference> mGarbage = new HashSet<>();
+ private final ReferenceQueue<Object> mRefQueue = new ReferenceQueue<>();
+ private final TrackedCollections mTrackedCollections;
+
+ public TrackedGarbage(TrackedCollections trackedCollections) {
+ mTrackedCollections = trackedCollections;
+ }
+
+ /**
+ * @see LeakDetector#trackGarbage(Object)
+ */
+ public synchronized void track(Object o) {
+ cleanUp();
+ mGarbage.add(new LeakReference(o, mRefQueue));
+ mTrackedCollections.track(mGarbage, "Garbage");
+ }
+
+ private void cleanUp() {
+ Reference<?> ref;
+ while ((ref = mRefQueue.poll()) != null) {
+ mGarbage.remove(ref);
+ }
+ }
+
+ /**
+ * A reference to something we consider leaked if it still has strong references.
+ *
+ * Helpful for finding potential leaks in a heapdump: Simply find an instance of
+ * LeakReference, find the object it refers to, then find a strong path to a GC root.
+ */
+ private static class LeakReference extends WeakReference<Object> {
+ private final Class<?> clazz;
+ private final long createdUptimeMillis;
+
+ LeakReference(Object t, ReferenceQueue<Object> queue) {
+ super(t, queue);
+ clazz = t.getClass();
+ createdUptimeMillis = SystemClock.uptimeMillis();
+ }
+ }
+
+ /**
+ * Dump statistics about the garbage.
+ *
+ * For each class, dumps the number of "garbage objects" that have not been collected yet.
+ * A large number of old instances indicates a probable leak.
+ */
+ public synchronized void dump(PrintWriter pw) {
+ cleanUp();
+
+ long now = SystemClock.uptimeMillis();
+
+ ArrayMap<Class<?>, Integer> acc = new ArrayMap<>();
+ ArrayMap<Class<?>, Integer> accOld = new ArrayMap<>();
+ for (LeakReference ref : mGarbage) {
+ acc.put(ref.clazz, acc.getOrDefault(ref.clazz, 0) + 1);
+ if (isOld(ref.createdUptimeMillis, now)) {
+ accOld.put(ref.clazz, accOld.getOrDefault(ref.clazz, 0) + 1);
+ }
+ }
+
+ for (Map.Entry<Class<?>, Integer> entry : acc.entrySet()) {
+ pw.print(entry.getKey().getName());
+ pw.print(": ");
+ pw.print(entry.getValue());
+ pw.print(" total, ");
+ pw.print(accOld.getOrDefault(entry.getKey(), 0));
+ pw.print(" old");
+ pw.println();
+ }
+ }
+
+ private boolean isOld(long createdUptimeMillis, long now) {
+ return createdUptimeMillis + GARBAGE_COLLECTION_DEADLINE_MILLIS < now;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedObjects.java b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedObjects.java
new file mode 100644
index 0000000..88f8b4c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedObjects.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import java.util.Collection;
+import java.util.WeakHashMap;
+
+/**
+ * Tracks instances of classes.
+ */
+public class TrackedObjects {
+
+ private final TrackedCollections mTrackedCollections;
+ private final WeakHashMap<Class<?>, TrackedClass<?>> mTrackedClasses = new WeakHashMap<>();
+
+ public TrackedObjects(TrackedCollections trackedCollections) {
+ mTrackedCollections = trackedCollections;
+ }
+
+ /**
+ * @see LeakDetector#trackInstance(Object)
+ */
+ public synchronized <T> void track(T object) {
+ Class<?> clazz = object.getClass();
+ @SuppressWarnings("unchecked")
+ TrackedClass<T> trackedClass = (TrackedClass<T>) mTrackedClasses.get(clazz);
+
+ if (trackedClass == null) {
+ trackedClass = new TrackedClass<T>();
+ mTrackedClasses.put(clazz, trackedClass);
+ }
+
+ trackedClass.track(object);
+ mTrackedCollections.track(trackedClass, clazz.getName());
+ }
+
+ public static boolean isTrackedObject(Collection<?> collection) {
+ return collection instanceof TrackedClass;
+ }
+
+ private static class TrackedClass<T> extends AbstractCollection<T> {
+ final WeakIdentityHashMap<T, Void> instances = new WeakIdentityHashMap<>();
+
+ void track(T object) {
+ instances.put(object, null);
+ }
+
+ @Override
+ public int size() {
+ return instances.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return instances.isEmpty();
+ }
+
+ }
+
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/WeakIdentityHashMap.java b/packages/SystemUI/src/com/android/systemui/util/leak/WeakIdentityHashMap.java
new file mode 100644
index 0000000..d370ac1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/WeakIdentityHashMap.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Like WeakHashMap, but uses identity instead of equality when comparing keys.
+ */
+public class WeakIdentityHashMap<K,V> {
+
+ private final HashMap<WeakReference<K>,V> mMap = new HashMap<>();
+ private final ReferenceQueue<Object> mRefQueue = new ReferenceQueue<>();
+
+ private void cleanUp() {
+ Reference<?> ref;
+ while ((ref = mRefQueue.poll()) != null) {
+ mMap.remove(ref);
+ }
+ }
+
+ public void put(K key, V value) {
+ cleanUp();
+ mMap.put(new CmpWeakReference<>(key, mRefQueue), value);
+ }
+
+ public V get(K key) {
+ cleanUp();
+ return mMap.get(new CmpWeakReference<>(key));
+ }
+
+ public Collection<V> values() {
+ cleanUp();
+ return mMap.values();
+ }
+
+ public Set<Map.Entry<WeakReference<K>, V>> entrySet() {
+ return mMap.entrySet();
+ }
+
+ public int size() {
+ cleanUp();
+ return mMap.size();
+ }
+
+ public boolean isEmpty() {
+ cleanUp();
+ return mMap.isEmpty();
+ }
+
+ private static class CmpWeakReference<K> extends WeakReference<K> {
+ private final int mHashCode;
+
+ public CmpWeakReference(K key) {
+ super(key);
+ mHashCode = System.identityHashCode(key);
+ }
+
+ public CmpWeakReference(K key, ReferenceQueue<Object> refQueue) {
+ super(key, refQueue);
+ mHashCode = System.identityHashCode(key);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == this) {
+ return true;
+ }
+ K k = get();
+ if (k != null && o instanceof CmpWeakReference) {
+ return ((CmpWeakReference) o).get() == k;
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return mHashCode;
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakDetectorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakDetectorTest.java
new file mode 100644
index 0000000..11722fe
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakDetectorTest.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.systemui.util.leak.ReferenceTestUtils.CollectionWaiter;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.FileDescriptor;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class LeakDetectorTest {
+
+ private LeakDetector mLeakDetector;
+
+ @Before
+ public void setup() {
+ mLeakDetector = LeakDetector.create();
+
+ // Note: Do not try to factor out object / collection waiter creation. The optimizer will
+ // try and cache accesses to fields and thus create a GC root for the duration of the test
+ // method, thus breaking the test.
+ }
+
+ @Test
+ public void trackInstance_doesNotLeakTrackedObject() {
+ Object object = new Object();
+ CollectionWaiter collectionWaiter = ReferenceTestUtils.createCollectionWaiter(object);
+
+ mLeakDetector.trackInstance(object);
+ object = null;
+ collectionWaiter.waitForCollection();
+ }
+
+ @Test
+ public void trackCollection_doesNotLeakTrackedObject() {
+ Collection<?> object = new ArrayList<>();
+ CollectionWaiter collectionWaiter = ReferenceTestUtils.createCollectionWaiter(object);
+
+ mLeakDetector.trackCollection(object, "tag");
+ object = null;
+ collectionWaiter.waitForCollection();
+ }
+
+ @Test
+ public void trackGarbage_doesNotLeakTrackedObject() {
+ Object object = new Object();
+ CollectionWaiter collectionWaiter = ReferenceTestUtils.createCollectionWaiter(object);
+
+ mLeakDetector.trackGarbage(object);
+ object = null;
+ collectionWaiter.waitForCollection();
+ }
+
+ @Test
+ public void testDump() throws Exception {
+ Object o1 = new Object();
+ Object o2 = new Object();
+ Collection<Object> col1 = new ArrayList<>();
+
+ mLeakDetector.trackInstance(o1);
+ mLeakDetector.trackCollection(col1, "tag");
+ mLeakDetector.trackGarbage(o2);
+
+ FileOutputStream fos = new FileOutputStream("/dev/null");
+ mLeakDetector.dump(fos.getFD(), new PrintWriter(fos), new String[0]);
+ }
+
+ @Test
+ public void testDisabled() throws Exception {
+ mLeakDetector = new LeakDetector(null, null, null);
+
+ Object o1 = new Object();
+ Object o2 = new Object();
+ Collection<Object> col1 = new ArrayList<>();
+
+ mLeakDetector.trackInstance(o1);
+ mLeakDetector.trackCollection(col1, "tag");
+ mLeakDetector.trackGarbage(o2);
+
+ FileOutputStream fos = new FileOutputStream("/dev/null");
+ mLeakDetector.dump(fos.getFD(), new PrintWriter(fos), new String[0]);
+ }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtils.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtils.java
new file mode 100644
index 0000000..b433e7a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtils.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import android.os.SystemClock;
+
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+
+/**
+ * Utilities for writing tests that manipulate weak or other references.
+ */
+public class ReferenceTestUtils {
+
+ /** Returns a runnable that blocks until {@code o} has been collected. */
+ public static CollectionWaiter createCollectionWaiter(Object o) {
+ ReferenceQueue<Object> q = new ReferenceQueue<>();
+ Reference<?> ref = new WeakReference<>(o, q);
+ o = null; // Ensure this variable can't be referenced from the lambda.
+
+ return () -> {
+ Runtime.getRuntime().gc();
+ while (true) {
+ try {
+ if (q.remove(5_000) == ref) {
+ return;
+ } else {
+ throw new RuntimeException("timeout while waiting for object collection");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ };
+ }
+
+ public static void waitForCondition(Condition p) {
+ long deadline = SystemClock.uptimeMillis() + 5_000;
+ while (!p.apply()) {
+ if (SystemClock.uptimeMillis() > deadline) {
+ throw new RuntimeException("timeout while waiting for condition");
+ }
+ SystemClock.sleep(100);
+ }
+ }
+
+ public interface Condition {
+ boolean apply();
+ }
+
+ public interface CollectionWaiter {
+ void waitForCollection();
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtilsTest.java
new file mode 100644
index 0000000..9da67b7
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtilsTest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import static org.junit.Assert.assertEquals;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class ReferenceTestUtilsTest {
+
+ @Test
+ public void testCollectionWaiter_doesntBlockIndefinitely() {
+ ReferenceTestUtils.createCollectionWaiter(new Object()).waitForCollection();
+ }
+
+ @Test
+ public void testConditionWaiter_doesntBlockIndefinitely() {
+ ReferenceTestUtils.waitForCondition(() -> true);
+ }
+
+ @Test
+ public void testConditionWaiter_waitsUntilConditionIsTrue() {
+ int[] countHolder = new int[]{0};
+
+ ReferenceTestUtils.waitForCondition(() -> {
+ countHolder[0] += 1;
+ return countHolder[0] >= 5;
+ });
+
+ assertEquals(5, countHolder[0]);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/WeakIdentityHashMapTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/WeakIdentityHashMapTest.java
new file mode 100644
index 0000000..fb1c1aa
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/WeakIdentityHashMapTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * 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.android.systemui.util.leak;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertTrue;
+
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.systemui.util.leak.ReferenceTestUtils.CollectionWaiter;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class WeakIdentityHashMapTest {
+
+ WeakIdentityHashMap<Object, Object> mMap;
+
+ @Before
+ public void setup() {
+ mMap = new WeakIdentityHashMap<>();
+ }
+
+ @Test
+ public void testUsesIdentity() {
+ String a1 = new String("a");
+ String a2 = new String("a");
+ assertNotSame(a1, a2);
+
+ mMap.put(a1, "value1");
+ mMap.put(a2, "value2");
+
+ assertEquals("value1", mMap.get(a1));
+ assertEquals("value2", mMap.get(a2));
+ }
+
+ @Test
+ public void testWeaklyReferences() {
+ Object object = new Object();
+ CollectionWaiter collectionWaiter = ReferenceTestUtils.createCollectionWaiter(object);
+
+ mMap.put(object, "value");
+ object = null;
+
+ // Wait until object has been collected. We'll also need to wait for mMap to become empty,
+ // because our collection waiter may be told about the collection earlier than mMap.
+ collectionWaiter.waitForCollection();
+ ReferenceTestUtils.waitForCondition(mMap::isEmpty);
+
+ assertEquals(0, mMap.size());
+ assertTrue(mMap.isEmpty());
+ }
+}
\ No newline at end of file