OSDN Git Service

SysUiLeaks: Add LeakDetector
authorAdrian Roos <roosa@google.com>
Fri, 3 Feb 2017 00:00:59 +0000 (16:00 -0800)
committerAdrian Roos <roosa@google.com>
Thu, 9 Feb 2017 09:33:02 +0000 (10:33 +0100)
Bug: 34932615
Test: runtest -x $ANDROID_BUILD_TOP/frameworks/base/packages/SystemUI/tests/src/com/android/systemui/util/leak/
Change-Id: Id4a9cbdaddc83ca3e25923eec33d53b6761c54cb

packages/SystemUI/src/com/android/systemui/Dependency.java
packages/SystemUI/src/com/android/systemui/util/leak/AbstractCollection.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/util/leak/TrackedCollections.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/util/leak/TrackedObjects.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/util/leak/WeakIdentityHashMap.java [new file with mode: 0644]
packages/SystemUI/tests/src/com/android/systemui/util/leak/LeakDetectorTest.java [new file with mode: 0644]
packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtils.java [new file with mode: 0644]
packages/SystemUI/tests/src/com/android/systemui/util/leak/ReferenceTestUtilsTest.java [new file with mode: 0644]
packages/SystemUI/tests/src/com/android/systemui/util/leak/WeakIdentityHashMapTest.java [new file with mode: 0644]

index e1f3176..2072fd4 100644 (file)
@@ -59,6 +59,7 @@ import com.android.systemui.statusbar.policy.UserSwitcherController;
 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 @@ public class Dependency extends SystemUI {
         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 (file)
index 0000000..1534def
--- /dev/null
@@ -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 (file)
index 0000000..2124f90
--- /dev/null
@@ -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 (file)
index 0000000..5577daf
--- /dev/null
@@ -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 (file)
index 0000000..fd59aee
--- /dev/null
@@ -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 (file)
index 0000000..88f8b4c
--- /dev/null
@@ -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 (file)
index 0000000..d370ac1
--- /dev/null
@@ -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 (file)
index 0000000..11722fe
--- /dev/null
@@ -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 (file)
index 0000000..b433e7a
--- /dev/null
@@ -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 (file)
index 0000000..9da67b7
--- /dev/null
@@ -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 (file)
index 0000000..fb1c1aa
--- /dev/null
@@ -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