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/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 7d19784..beb3c53 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -247,10 +247,14 @@
getDependency(LeakDetector.class),
getDependency(LEAK_REPORT_EMAIL)));
- mProviders.put(GarbageMonitor.class, () -> new GarbageMonitor(
- getDependency(BG_LOOPER),
- getDependency(LeakDetector.class),
- getDependency(LeakReporter.class)));
+ mProviders.put(
+ GarbageMonitor.class,
+ () ->
+ new GarbageMonitor(
+ mContext,
+ getDependency(BG_LOOPER),
+ getDependency(LeakDetector.class),
+ getDependency(LeakReporter.class)));
mProviders.put(TunerService.class, () ->
new TunerServiceImpl(mContext));
diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
index 9593b0f..53a576d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java
@@ -24,6 +24,7 @@
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
+import android.os.Build;
import android.os.Handler;
import android.service.quicksettings.TileService;
import android.text.TextUtils;
@@ -37,8 +38,10 @@
import com.android.systemui.qs.QSTileHost;
import com.android.systemui.qs.external.CustomTile;
import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon;
+import com.android.systemui.util.leak.GarbageMonitor;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collection;
import java.util.List;
@@ -68,7 +71,6 @@
// Enqueue jobs to fetch every system tile and then ever package tile.
addStockTiles(host);
addPackageTiles(host);
- // TODO: Live?
}
public boolean isFinished() {
@@ -77,10 +79,14 @@
private void addStockTiles(QSTileHost host) {
String possible = mContext.getString(R.string.quick_settings_tiles_stock);
- String[] possibleTiles = possible.split(",");
+ final ArrayList<String> possibleTiles = new ArrayList<>();
+ possibleTiles.addAll(Arrays.asList(possible.split(",")));
+ if (Build.IS_DEBUGGABLE) {
+ possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC);
+ }
+
final ArrayList<QSTile> tilesToAdd = new ArrayList<>();
- for (int i = 0; i < possibleTiles.length; i++) {
- final String spec = possibleTiles[i];
+ for (String spec : possibleTiles) {
final QSTile tile = host.createTile(spec);
if (tile == null) {
continue;
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
index 8d48890..ac7ef5d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java
@@ -15,6 +15,7 @@
package com.android.systemui.qs.tileimpl;
import android.content.Context;
+import android.os.Build;
import android.util.Log;
import android.view.ContextThemeWrapper;
@@ -41,6 +42,7 @@
import com.android.systemui.qs.tiles.WifiTile;
import com.android.systemui.qs.tiles.WorkModeTile;
import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.util.leak.GarbageMonitor;
public class QSFactoryImpl implements QSFactory {
@@ -60,30 +62,58 @@
}
private QSTileImpl createTileInternal(String tileSpec) {
- if (tileSpec.equals("wifi")) return new WifiTile(mHost);
- else if (tileSpec.equals("bt")) return new BluetoothTile(mHost);
- else if (tileSpec.equals("cell")) return new CellularTile(mHost);
- else if (tileSpec.equals("dnd")) return new DndTile(mHost);
- else if (tileSpec.equals("inversion")) return new ColorInversionTile(mHost);
- else if (tileSpec.equals("airplane")) return new AirplaneModeTile(mHost);
- else if (tileSpec.equals("work")) return new WorkModeTile(mHost);
- else if (tileSpec.equals("rotation")) return new RotationLockTile(mHost);
- else if (tileSpec.equals("flashlight")) return new FlashlightTile(mHost);
- else if (tileSpec.equals("location")) return new LocationTile(mHost);
- else if (tileSpec.equals("cast")) return new CastTile(mHost);
- else if (tileSpec.equals("hotspot")) return new HotspotTile(mHost);
- else if (tileSpec.equals("user")) return new UserTile(mHost);
- else if (tileSpec.equals("battery")) return new BatterySaverTile(mHost);
- else if (tileSpec.equals("saver")) return new DataSaverTile(mHost);
- else if (tileSpec.equals("night")) return new NightDisplayTile(mHost);
- else if (tileSpec.equals("nfc")) return new NfcTile(mHost);
- // Intent tiles.
- else if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(mHost, tileSpec);
- else if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec);
- else {
- Log.w(TAG, "Bad tile spec: " + tileSpec);
- return null;
+ // Stock tiles.
+ switch (tileSpec) {
+ case "wifi":
+ return new WifiTile(mHost);
+ case "bt":
+ return new BluetoothTile(mHost);
+ case "cell":
+ return new CellularTile(mHost);
+ case "dnd":
+ return new DndTile(mHost);
+ case "inversion":
+ return new ColorInversionTile(mHost);
+ case "airplane":
+ return new AirplaneModeTile(mHost);
+ case "work":
+ return new WorkModeTile(mHost);
+ case "rotation":
+ return new RotationLockTile(mHost);
+ case "flashlight":
+ return new FlashlightTile(mHost);
+ case "location":
+ return new LocationTile(mHost);
+ case "cast":
+ return new CastTile(mHost);
+ case "hotspot":
+ return new HotspotTile(mHost);
+ case "user":
+ return new UserTile(mHost);
+ case "battery":
+ return new BatterySaverTile(mHost);
+ case "saver":
+ return new DataSaverTile(mHost);
+ case "night":
+ return new NightDisplayTile(mHost);
+ case "nfc":
+ return new NfcTile(mHost);
}
+
+ // Intent tiles.
+ if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(mHost, tileSpec);
+ if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec);
+
+ // Debug tiles.
+ if (Build.IS_DEBUGGABLE) {
+ if (tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC)) {
+ return new GarbageMonitor.MemoryTile(mHost);
+ }
+ }
+
+ // Broken tiles.
+ Log.w(TAG, "Bad tile spec: " + tileSpec);
+ return null;
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
new file mode 100644
index 0000000..2995eba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java
@@ -0,0 +1,175 @@
+/*
+ * 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.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Build;
+import android.support.v4.content.FileProvider;
+import android.util.Log;
+
+import com.android.systemui.Dependency;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * Utility class for dumping, compressing, sending, and serving heap dump files.
+ *
+ * <p>Unlike the Internet, this IS a big truck you can dump something on.
+ */
+public class DumpTruck {
+ private static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
+ private static final String FILEPROVIDER_PATH = "leak";
+
+ private static final String TAG = "DumpTruck";
+ private static final int BUFSIZ = 512 * 1024; // 512K
+
+ private final Context context;
+ private Uri hprofUri;
+ final StringBuilder body = new StringBuilder();
+
+ public DumpTruck(Context context) {
+ this.context = context;
+ }
+
+ /**
+ * Capture memory for the given processes and zip them up for sharing.
+ *
+ * @param pids
+ * @return this, for chaining
+ */
+ public DumpTruck captureHeaps(int[] pids) {
+ final GarbageMonitor gm = Dependency.get(GarbageMonitor.class);
+
+ final File dumpDir = new File(context.getCacheDir(), FILEPROVIDER_PATH);
+ dumpDir.mkdirs();
+ hprofUri = null;
+
+ body.setLength(0);
+ body.append("Build: ").append(Build.DISPLAY).append("\n\nProcesses:\n");
+
+ final ArrayList<String> paths = new ArrayList<String>();
+ final int myPid = android.os.Process.myPid();
+
+ final int[] pids_copy = Arrays.copyOf(pids, pids.length);
+ for (int pid : pids_copy) {
+ body.append(" pid ").append(pid);
+ if (gm != null) {
+ GarbageMonitor.ProcessMemInfo info = gm.getMemInfo(pid);
+ if (info != null) {
+ body.append(":")
+ .append(" up=")
+ .append(info.getUptime())
+ .append(" pss=")
+ .append(info.currentPss)
+ .append(" uss=")
+ .append(info.currentUss);
+ }
+ }
+ if (pid == myPid) {
+ final String path =
+ new File(dumpDir, String.format("heap-%d.ahprof", pid)).getPath();
+ Log.v(TAG, "Dumping memory info for process " + pid + " to " + path);
+ try {
+ android.os.Debug.dumpHprofData(path); // will block
+ paths.add(path);
+ body.append(" (hprof attached)");
+ } catch (IOException e) {
+ Log.e(TAG, "error dumping memory:", e);
+ body.append("\n** Could not dump heap: \n").append(e.toString()).append("\n");
+ }
+ }
+ body.append("\n");
+ }
+
+ try {
+ final String zipfile =
+ new File(dumpDir, String.format("hprof-%d.zip", System.currentTimeMillis()))
+ .getCanonicalPath();
+ if (DumpTruck.zipUp(zipfile, paths)) {
+ final File pathFile = new File(zipfile);
+ hprofUri = FileProvider.getUriForFile(context, FILEPROVIDER_AUTHORITY, pathFile);
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "unable to zip up heapdumps", e);
+ body.append("\n** Could not zip up files: \n").append(e.toString()).append("\n");
+ }
+
+ return this;
+ }
+
+ /**
+ * Get the Uri of the current heap dump. Be sure to call captureHeaps first.
+ *
+ * @return Uri to the dump served by the SystemUI file provider
+ */
+ public Uri getDumpUri() {
+ return hprofUri;
+ }
+
+ /**
+ * Get an ACTION_SEND intent suitable for startActivity() or attaching to a Notification.
+ *
+ * @return share intent
+ */
+ public Intent createShareIntent() {
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+ shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ shareIntent.putExtra(Intent.EXTRA_SUBJECT, "SystemUI memory dump");
+
+ shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString());
+
+ if (hprofUri != null) {
+ shareIntent.setType("application/zip");
+ shareIntent.putExtra(Intent.EXTRA_STREAM, hprofUri);
+ }
+ return shareIntent;
+ }
+
+ private static boolean zipUp(String zipfilePath, ArrayList<String> paths) {
+ try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfilePath))) {
+ final byte[] buf = new byte[BUFSIZ];
+
+ for (String filename : paths) {
+ try (InputStream is = new BufferedInputStream(new FileInputStream(filename))) {
+ ZipEntry entry = new ZipEntry(filename);
+ zos.putNextEntry(entry);
+ int len;
+ while (0 < (len = is.read(buf, 0, BUFSIZ))) {
+ zos.write(buf, 0, len);
+ }
+ zos.closeEntry();
+ }
+ }
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "error zipping up profile data", e);
+ }
+ return false;
+ }
+}
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;
+ }
}
}
}