Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 57066148 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "SysUiLeaks: Add LeakDetector"

parents a6e5041a e1e0b483
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -66,6 +66,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;
@@ -187,6 +188,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));

+83 −0
Original line number Diff line number Diff line
/*
 * 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();
    }
}
+139 −0
Original line number Diff line number Diff line
/*
 * 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);
        }
    }
}
+104 −0
Original line number Diff line number Diff line
/*
 * 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();
            }
        }
    }
}
+112 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
Loading