SystemUI heap monitor & hprof dump tile.

Adding the tile to your active set will start a continuous memory tracker,
updating the tile every minute with the latest pss value. Tapping the tile
will pull a full heap dump, zip it, and share it via ACTION_SEND.

Additionally, @integer/watch_heap_limit can be set to the heap size (in KB)
at which ActivityManager will automatically generate a heap dump and notify
the user when it's ready.

Available only on IS_DEBUGGABLE builds. Supplies limited. Order now!

Bug: 76208386
Test: atest com.android.systemui.util.leak.GarbageMonitorTest
Test: runtest systemui
Change-Id: I0434dd7dc330784c750469b013e40b31ef92638d
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
index 021f9c4..b2cc269 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
@@ -16,88 +16,469 @@
 
 package com.android.systemui.util.leak;
 
+import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN;
 
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.ColorStateList;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
 import android.os.Build;
+import android.os.Debug;
 import android.os.Handler;
 import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
 import android.os.SystemProperties;
 import android.provider.Settings;
-import android.support.annotation.VisibleForTesting;
+import android.service.quicksettings.Tile;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.util.LongSparseArray;
 
 import com.android.systemui.Dependency;
+import com.android.systemui.R;
 import com.android.systemui.SystemUI;
+import com.android.systemui.plugins.qs.QSTile;
+import com.android.systemui.qs.QSHost;
+import com.android.systemui.qs.tileimpl.QSTileImpl;
+
+import java.util.ArrayList;
 
 public class GarbageMonitor {
+    private static final boolean LEAK_REPORTING_ENABLED =
+            Build.IS_DEBUGGABLE
+                    && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
+    private static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting";
+
+    private static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE;
+    private static final boolean ENABLE_AM_HEAP_LIMIT = true; // use ActivityManager.setHeapLimit
 
     private static final String TAG = "GarbageMonitor";
 
-    private static final long GARBAGE_INSPECTION_INTERVAL = 5 * 60 * 1000; // 5min
+    private static final long GARBAGE_INSPECTION_INTERVAL =
+            15 * DateUtils.MINUTE_IN_MILLIS; // 15 min
+    private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min
+
+    private static final int DO_GARBAGE_INSPECTION = 1000;
+    private static final int DO_HEAP_TRACK = 3000;
+
     private static final int GARBAGE_ALLOWANCE = 5;
 
     private final Handler mHandler;
     private final TrackedGarbage mTrackedGarbage;
     private final LeakReporter mLeakReporter;
+    private final Context mContext;
+    private final ActivityManager mAm;
+    private MemoryTile mQSTile;
+    private DumpTruck mDumpTruck;
 
-    public GarbageMonitor(Looper bgLooper, LeakDetector leakDetector,
+    private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
+    private final ArrayList<Long> mPids = new ArrayList<>();
+    private int[] mPidsArray = new int[1];
+
+    private long mHeapLimit;
+
+    public GarbageMonitor(
+            Context context,
+            Looper bgLooper,
+            LeakDetector leakDetector,
             LeakReporter leakReporter) {
-        mHandler = bgLooper != null ? new Handler(bgLooper): null;
+        mContext = context.getApplicationContext();
+        mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        mHandler = new BackgroundHeapCheckHandler(bgLooper);
+
         mTrackedGarbage = leakDetector.getTrackedGarbage();
         mLeakReporter = leakReporter;
+
+        mDumpTruck = new DumpTruck(mContext);
+
+        if (ENABLE_AM_HEAP_LIMIT) {
+            mHeapLimit = mContext.getResources().getInteger(R.integer.watch_heap_limit);
+        }
     }
 
-    public void start() {
+    public void startLeakMonitor() {
         if (mTrackedGarbage == null) {
             return;
         }
 
-        scheduleInspectGarbage(this::inspectGarbage);
+        mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION);
     }
 
-    @VisibleForTesting
-    void scheduleInspectGarbage(Runnable runnable) {
-        mHandler.postDelayed(runnable, GARBAGE_INSPECTION_INTERVAL);
+    public void startHeapTracking() {
+        startTrackingProcess(
+                android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
+        mHandler.sendEmptyMessage(DO_HEAP_TRACK);
     }
 
-    private void inspectGarbage() {
+    private boolean gcAndCheckGarbage() {
         if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
             Runtime.getRuntime().gc();
-
-            // Allow some time to for ReferenceQueue to catch up.
-            scheduleReinspectGarbage(this::reinspectGarbageAfterGc);
+            return true;
         }
-        scheduleInspectGarbage(this::inspectGarbage);
+        return false;
     }
 
-    @VisibleForTesting
-    void scheduleReinspectGarbage(Runnable runnable) {
-        mHandler.postDelayed(runnable, (long) 100);
-    }
-
-    private void reinspectGarbageAfterGc() {
+    void reinspectGarbageAfterGc() {
         int count = mTrackedGarbage.countOldGarbage();
         if (count > GARBAGE_ALLOWANCE) {
             mLeakReporter.dumpLeak(count);
         }
     }
 
+    public ProcessMemInfo getMemInfo(int pid) {
+        return mData.get(pid);
+    }
+
+    public int[] getTrackedProcesses() {
+        return mPidsArray;
+    }
+
+    public void startTrackingProcess(long pid, String name, long start) {
+        synchronized (mPids) {
+            if (mPids.contains(pid)) return;
+
+            mPids.add(pid);
+            updatePidsArrayL();
+
+            mData.put(pid, new ProcessMemInfo(pid, name, start));
+        }
+    }
+
+    private void updatePidsArrayL() {
+        final int N = mPids.size();
+        mPidsArray = new int[N];
+        StringBuffer sb = new StringBuffer("Now tracking processes: ");
+        for (int i = 0; i < N; i++) {
+            final int p = mPids.get(i).intValue();
+            mPidsArray[i] = p;
+            sb.append(p);
+            sb.append(" ");
+        }
+        Log.v(TAG, sb.toString());
+    }
+
+    private void update() {
+        synchronized (mPids) {
+            Debug.MemoryInfo[] dinfos = mAm.getProcessMemoryInfo(mPidsArray);
+            for (int i = 0; i < dinfos.length; i++) {
+                Debug.MemoryInfo dinfo = dinfos[i];
+                if (i > mPids.size()) {
+                    Log.e(TAG, "update: unknown process info received: " + dinfo);
+                    break;
+                }
+                final long pid = mPids.get(i).intValue();
+                final ProcessMemInfo info = mData.get(pid);
+                info.head = (info.head + 1) % info.pss.length;
+                info.pss[info.head] = info.currentPss = dinfo.getTotalPss();
+                info.uss[info.head] = info.currentUss = dinfo.getTotalPrivateDirty();
+                if (info.currentPss > info.max) info.max = info.currentPss;
+                if (info.currentUss > info.max) info.max = info.currentUss;
+                if (info.currentPss == 0) {
+                    Log.v(TAG, "update: pid " + pid + " has pss=0, it probably died");
+                    mData.remove(pid);
+                }
+            }
+            for (int i = mPids.size() - 1; i >= 0; i--) {
+                final long pid = mPids.get(i).intValue();
+                if (mData.get(pid) == null) {
+                    mPids.remove(i);
+                    updatePidsArrayL();
+                }
+            }
+        }
+        if (mQSTile != null) mQSTile.update();
+    }
+
+    private void setTile(MemoryTile tile) {
+        mQSTile = tile;
+        if (tile != null) tile.update();
+    }
+
+    private static String formatBytes(long b) {
+        String[] SUFFIXES = {"B", "K", "M", "G", "T"};
+        int i;
+        for (i = 0; i < SUFFIXES.length; i++) {
+            if (b < 1024) break;
+            b /= 1024;
+        }
+        return b + SUFFIXES[i];
+    }
+
+    private void dumpHprofAndShare() {
+        final Intent share = mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent();
+        mContext.startActivity(share);
+    }
+
+    private static class MemoryIconDrawable extends Drawable {
+        long pss, limit;
+        final Drawable baseIcon;
+        final Paint paint = new Paint();
+        final float dp;
+
+        MemoryIconDrawable(Context context) {
+            baseIcon = context.getDrawable(R.drawable.ic_memory).mutate();
+            dp = context.getResources().getDisplayMetrics().density;
+            paint.setColor(QSTileImpl.getColorForState(context, Tile.STATE_ACTIVE));
+        }
+
+        public void setPss(long pss) {
+            if (pss != this.pss) {
+                this.pss = pss;
+                invalidateSelf();
+            }
+        }
+
+        public void setLimit(long limit) {
+            if (limit != this.limit) {
+                this.limit = limit;
+                invalidateSelf();
+            }
+        }
+
+        @Override
+        public void draw(Canvas canvas) {
+            baseIcon.draw(canvas);
+
+            if (limit > 0 && pss > 0) {
+                float frac = Math.min(1f, (float) pss / limit);
+
+                final Rect bounds = getBounds();
+                canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp);
+                //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
+                canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint);
+            }
+        }
+
+        @Override
+        public void setBounds(int left, int top, int right, int bottom) {
+            super.setBounds(left, top, right, bottom);
+            baseIcon.setBounds(left, top, right, bottom);
+        }
+
+        @Override
+        public int getIntrinsicHeight() {
+            return baseIcon.getIntrinsicHeight();
+        }
+
+        @Override
+        public int getIntrinsicWidth() {
+            return baseIcon.getIntrinsicWidth();
+        }
+
+        @Override
+        public void setAlpha(int i) {
+            baseIcon.setAlpha(i);
+        }
+
+        @Override
+        public void setColorFilter(ColorFilter colorFilter) {
+            baseIcon.setColorFilter(colorFilter);
+            paint.setColorFilter(colorFilter);
+        }
+
+        @Override
+        public void setTint(int tint) {
+            super.setTint(tint);
+            baseIcon.setTint(tint);
+        }
+
+        @Override
+        public void setTintList(ColorStateList tint) {
+            super.setTintList(tint);
+            baseIcon.setTintList(tint);
+        }
+
+        @Override
+        public void setTintMode(PorterDuff.Mode tintMode) {
+            super.setTintMode(tintMode);
+            baseIcon.setTintMode(tintMode);
+        }
+
+        @Override
+        public int getOpacity() {
+            return PixelFormat.TRANSLUCENT;
+        }
+    }
+
+    private static class MemoryGraphIcon extends QSTile.Icon {
+        long pss, limit;
+
+        public void setPss(long pss) {
+            this.pss = pss;
+        }
+
+        public void setHeapLimit(long limit) {
+            this.limit = limit;
+        }
+
+        @Override
+        public Drawable getDrawable(Context context) {
+            final MemoryIconDrawable drawable = new MemoryIconDrawable(context);
+            drawable.setPss(pss);
+            drawable.setLimit(limit);
+            return drawable;
+        }
+    }
+
+    public static class MemoryTile extends QSTileImpl<QSTile.State> {
+        public static final String TILE_SPEC = "dbg:mem";
+
+        private final GarbageMonitor gm;
+        private ProcessMemInfo pmi;
+
+        public MemoryTile(QSHost host) {
+            super(host);
+            gm = Dependency.get(GarbageMonitor.class);
+        }
+
+        @Override
+        public State newTileState() {
+            return new QSTile.State();
+        }
+
+        @Override
+        public Intent getLongClickIntent() {
+            return new Intent();
+        }
+
+        @Override
+        protected void handleClick() {
+            getHost().collapsePanels();
+            mHandler.post(gm::dumpHprofAndShare);
+        }
+
+        @Override
+        public int getMetricsCategory() {
+            return VIEW_UNKNOWN;
+        }
+
+        @Override
+        public void handleSetListening(boolean listening) {
+            if (gm != null) gm.setTile(listening ? this : null);
+
+            final ActivityManager am = mContext.getSystemService(ActivityManager.class);
+            if (listening && gm.mHeapLimit > 0) {
+                am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes?
+            } else {
+                am.clearWatchHeapLimit();
+            }
+        }
+
+        @Override
+        public CharSequence getTileLabel() {
+            return getState().label;
+        }
+
+        @Override
+        protected void handleUpdateState(State state, Object arg) {
+            pmi = gm.getMemInfo(Process.myPid());
+            final MemoryGraphIcon icon = new MemoryGraphIcon();
+            icon.setHeapLimit(gm.mHeapLimit);
+            if (pmi != null) {
+                icon.setPss(pmi.currentPss);
+                state.label = mContext.getString(R.string.heap_dump_tile_name);
+                state.secondaryLabel =
+                        String.format(
+                                "pss: %s / %s",
+                                formatBytes(pmi.currentPss * 1024),
+                                formatBytes(gm.mHeapLimit * 1024));
+            } else {
+                icon.setPss(0);
+                state.label = "Dump SysUI";
+                state.secondaryLabel = null;
+            }
+            state.icon = icon;
+        }
+
+        public void update() {
+            refreshState();
+        }
+
+        public long getPss() {
+            return pmi != null ? pmi.currentPss : 0;
+        }
+
+        public long getHeapLimit() {
+            return gm != null ? gm.mHeapLimit : 0;
+        }
+    }
+
+    public static class ProcessMemInfo {
+        public long pid;
+        public String name;
+        public long startTime;
+        public long currentPss, currentUss;
+        public long[] pss = new long[256];
+        public long[] uss = new long[256];
+        public long max = 1;
+        public int head = 0;
+
+        public ProcessMemInfo(long pid, String name, long start) {
+            this.pid = pid;
+            this.name = name;
+            this.startTime = start;
+        }
+
+        public long getUptime() {
+            return System.currentTimeMillis() - startTime;
+        }
+    }
+
     public static class Service extends SystemUI {
-
-        // TODO(b/35345376): Turn this back on for debuggable builds after known leak fixed.
-        private static final boolean ENABLED = Build.IS_DEBUGGABLE
-                && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
-        private static final String FORCE_ENABLE = "sysui_force_garbage_monitor";
-
         private GarbageMonitor mGarbageMonitor;
 
         @Override
         public void start() {
-            boolean forceEnable = Settings.Secure.getInt(mContext.getContentResolver(),
-                    FORCE_ENABLE, 0) != 0;
-            if (!ENABLED && !forceEnable) {
-                return;
-            }
+            boolean forceEnable =
+                    Settings.Secure.getInt(
+                                    mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0)
+                            != 0;
             mGarbageMonitor = Dependency.get(GarbageMonitor.class);
-            mGarbageMonitor.start();
+            if (LEAK_REPORTING_ENABLED || forceEnable) {
+                mGarbageMonitor.startLeakMonitor();
+            }
+            if (HEAP_TRACKING_ENABLED || forceEnable) {
+                mGarbageMonitor.startHeapTracking();
+            }
+        }
+    }
+
+    private class BackgroundHeapCheckHandler extends Handler {
+        BackgroundHeapCheckHandler(Looper onLooper) {
+            super(onLooper);
+            if (Looper.getMainLooper().equals(onLooper)) {
+                throw new RuntimeException(
+                        "BackgroundHeapCheckHandler may not run on the ui thread");
+            }
+        }
+
+        @Override
+        public void handleMessage(Message m) {
+            switch (m.what) {
+                case DO_GARBAGE_INSPECTION:
+                    if (gcAndCheckGarbage()) {
+                        postDelayed(GarbageMonitor.this::reinspectGarbageAfterGc, 100);
+                    }
+
+                    removeMessages(DO_GARBAGE_INSPECTION);
+                    sendEmptyMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
+                    break;
+
+                case DO_HEAP_TRACK:
+                    update();
+                    removeMessages(DO_HEAP_TRACK);
+                    sendEmptyMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
+                    break;
+            }
         }
     }
 }