blob: 2e0c035b8547a4e02504f07541141e1673f360df [file] [log] [blame]
/*
* 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 android.service.quicksettings.Tile.STATE_ACTIVE;
import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE;
import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN;
import android.annotation.Nullable;
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.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.os.SystemProperties;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.util.Log;
import android.util.LongSparseArray;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
*/
@Singleton
public class GarbageMonitor implements Dumpable {
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;
// whether to use ActivityManager.setHeapLimit
private static final boolean ENABLE_AM_HEAP_LIMIT = Build.IS_DEBUGGABLE;
// heap limit value, in KB (overrides R.integer.watch_heap_limit)
private static final String SETTINGS_KEY_AM_HEAP_LIMIT = "systemui_am_heap_limit";
private static final String TAG = "GarbageMonitor";
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 HEAP_TRACK_HISTORY_LEN = 720; // 12 hours
private static final int DO_GARBAGE_INSPECTION = 1000;
private static final int DO_HEAP_TRACK = 3000;
private static final int GARBAGE_ALLOWANCE = 5;
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
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;
private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
private final ArrayList<Long> mPids = new ArrayList<>();
private long mHeapLimit;
/**
*/
@Inject
public GarbageMonitor(
Context context,
@Background Looper bgLooper,
LeakDetector leakDetector,
LeakReporter leakReporter) {
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 = Settings.Global.getInt(context.getContentResolver(),
SETTINGS_KEY_AM_HEAP_LIMIT,
mContext.getResources().getInteger(R.integer.watch_heap_limit));
}
}
public void startLeakMonitor() {
if (mTrackedGarbage == null) {
return;
}
mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION);
}
public void startHeapTracking() {
startTrackingProcess(
android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
mHandler.sendEmptyMessage(DO_HEAP_TRACK);
}
private boolean gcAndCheckGarbage() {
if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
Runtime.getRuntime().gc();
return true;
}
return false;
}
void reinspectGarbageAfterGc() {
int count = mTrackedGarbage.countOldGarbage();
if (count > GARBAGE_ALLOWANCE) {
mLeakReporter.dumpLeak(count);
}
}
public ProcessMemInfo getMemInfo(int pid) {
return mData.get(pid);
}
public List<Long> getTrackedProcesses() {
return mPids;
}
public void startTrackingProcess(long pid, String name, long start) {
synchronized (mPids) {
if (mPids.contains(pid)) return;
mPids.add(pid);
logPids();
mData.put(pid, new ProcessMemInfo(pid, name, start));
}
}
private void logPids() {
if (DEBUG) {
StringBuffer sb = new StringBuffer("Now tracking processes: ");
for (int i = 0; i < mPids.size(); i++) {
final int p = mPids.get(i).intValue();
sb.append(" ");
}
Log.v(TAG, sb.toString());
}
}
private void update() {
synchronized (mPids) {
for (int i = 0; i < mPids.size(); i++) {
final int pid = mPids.get(i).intValue();
// rssValues contains [VmRSS, RssFile, RssAnon, VmSwap].
long[] rssValues = Process.getRss(pid);
if (rssValues == null && rssValues.length == 0) {
if (DEBUG) Log.e(TAG, "update: Process.getRss() didn't provide any values.");
break;
}
long rss = rssValues[0];
final ProcessMemInfo info = mData.get(pid);
info.rss[info.head] = info.currentRss = rss;
info.head = (info.head + 1) % info.rss.length;
if (info.currentRss > info.max) info.max = info.currentRss;
if (info.currentRss == 0) {
if (DEBUG) Log.v(TAG, "update: pid " + pid + " has rss=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);
logPids();
}
}
}
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 Intent dumpHprofAndGetShareIntent() {
return mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent();
}
@Override
public void dump(@Nullable FileDescriptor fd, PrintWriter pw, @Nullable String[] args) {
pw.println("GarbageMonitor params:");
pw.println(String.format(" mHeapLimit=%d KB", mHeapLimit));
pw.println(String.format(" GARBAGE_INSPECTION_INTERVAL=%d (%.1f mins)",
GARBAGE_INSPECTION_INTERVAL,
(float) GARBAGE_INSPECTION_INTERVAL / DateUtils.MINUTE_IN_MILLIS));
final float htiMins = HEAP_TRACK_INTERVAL / DateUtils.MINUTE_IN_MILLIS;
pw.println(String.format(" HEAP_TRACK_INTERVAL=%d (%.1f mins)",
HEAP_TRACK_INTERVAL,
htiMins));
pw.println(String.format(" HEAP_TRACK_HISTORY_LEN=%d (%.1f hr total)",
HEAP_TRACK_HISTORY_LEN,
(float) HEAP_TRACK_HISTORY_LEN * htiMins / 60f));
pw.println("GarbageMonitor tracked processes:");
for (long pid : mPids) {
final ProcessMemInfo pmi = mData.get(pid);
if (pmi != null) {
pmi.dump(fd, pw, args);
}
}
}
private static class MemoryIconDrawable extends Drawable {
long rss, 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, STATE_ACTIVE));
}
public void setRss(long rss) {
if (rss != this.rss) {
this.rss = rss;
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 && rss > 0) {
float frac = Math.min(1f, (float) rss / 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 rss, limit;
public void setRss(long rss) {
this.rss = rss;
}
public void setHeapLimit(long limit) {
this.limit = limit;
}
@Override
public Drawable getDrawable(Context context) {
final MemoryIconDrawable drawable = new MemoryIconDrawable(context);
drawable.setRss(rss);
drawable.setLimit(limit);
return drawable;
}
}
public static class MemoryTile extends QSTileImpl<QSTile.State> {
public static final String TILE_SPEC = "dbg:mem";
// Tell QSTileHost.java to toss this into the default tileset?
public static final boolean ADD_TO_DEFAULT_ON_DEBUGGABLE_BUILDS = true;
private final GarbageMonitor gm;
private final ActivityStarter mActivityStarter;
private ProcessMemInfo pmi;
private boolean dumpInProgress;
@Inject
public MemoryTile(QSHost host, GarbageMonitor monitor, ActivityStarter starter) {
super(host);
gm = monitor;
mActivityStarter = starter;
}
@Override
public State newTileState() {
return new QSTile.State();
}
@Override
public Intent getLongClickIntent() {
return new Intent();
}
@Override
protected void handleClick() {
if (dumpInProgress) return;
dumpInProgress = true;
refreshState();
new Thread("HeapDumpThread") {
@Override
public void run() {
try {
// wait for animations & state changes
Thread.sleep(500);
} catch (InterruptedException ignored) { }
final Intent shareIntent = gm.dumpHprofAndGetShareIntent();
mHandler.post(() -> {
dumpInProgress = false;
refreshState();
getHost().collapsePanels();
mActivityStarter.postStartActivityDismissingKeyguard(shareIntent, 0);
});
}
}.start();
}
@Override
public int getMetricsCategory() {
return VIEW_UNKNOWN;
}
@Override
public void handleSetListening(boolean listening) {
super.handleSetListening(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);
state.state = dumpInProgress ? STATE_UNAVAILABLE : STATE_ACTIVE;
state.label = dumpInProgress
? "Dumping..."
: mContext.getString(R.string.heap_dump_tile_name);
if (pmi != null) {
icon.setRss(pmi.currentRss);
state.secondaryLabel =
String.format(
"rss: %s / %s",
formatBytes(pmi.currentRss * 1024),
formatBytes(gm.mHeapLimit * 1024));
} else {
icon.setRss(0);
state.secondaryLabel = null;
}
state.icon = icon;
}
public void update() {
refreshState();
}
public long getRss() {
return pmi != null ? pmi.currentRss : 0;
}
public long getHeapLimit() {
return gm != null ? gm.mHeapLimit : 0;
}
}
/** */
public static class ProcessMemInfo implements Dumpable {
public long pid;
public String name;
public long startTime;
public long currentRss;
public long[] rss = new long[HEAP_TRACK_HISTORY_LEN];
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;
}
@Override
public void dump(@Nullable FileDescriptor fd, PrintWriter pw, @Nullable String[] args) {
pw.print("{ \"pid\": ");
pw.print(pid);
pw.print(", \"name\": \"");
pw.print(name.replace('"', '-'));
pw.print("\", \"start\": ");
pw.print(startTime);
pw.print(", \"rss\": [");
// write rss values starting from the oldest, which is rss[head], wrapping around to
// rss[(head-1) % rss.length]
for (int i = 0; i < rss.length; i++) {
if (i > 0) pw.print(",");
pw.print(rss[(head + i) % rss.length]);
}
pw.println("] }");
}
}
/** */
@Singleton
public static class Service extends SystemUI implements Dumpable {
private final GarbageMonitor mGarbageMonitor;
@Inject
public Service(Context context, GarbageMonitor garbageMonitor) {
super(context);
mGarbageMonitor = garbageMonitor;
}
@Override
public void start() {
boolean forceEnable =
Settings.Secure.getInt(
mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0)
!= 0;
if (LEAK_REPORTING_ENABLED || forceEnable) {
mGarbageMonitor.startLeakMonitor();
}
if (HEAP_TRACKING_ENABLED || forceEnable) {
mGarbageMonitor.startHeapTracking();
}
}
@Override
public void dump(@Nullable FileDescriptor fd, PrintWriter pw, @Nullable String[] args) {
if (mGarbageMonitor != null) mGarbageMonitor.dump(fd, pw, args);
}
}
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;
}
}
}
}