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;
+ }
}
}
}