Home | History | Annotate | Download | only in leak
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.systemui.util.leak;
     18 
     19 import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN;
     20 
     21 import android.app.ActivityManager;
     22 import android.content.Context;
     23 import android.content.Intent;
     24 import android.content.res.ColorStateList;
     25 import android.graphics.Canvas;
     26 import android.graphics.ColorFilter;
     27 import android.graphics.Paint;
     28 import android.graphics.PixelFormat;
     29 import android.graphics.PorterDuff;
     30 import android.graphics.Rect;
     31 import android.graphics.drawable.Drawable;
     32 import android.os.Build;
     33 import android.os.Debug;
     34 import android.os.Handler;
     35 import android.os.Looper;
     36 import android.os.Message;
     37 import android.os.Process;
     38 import android.os.SystemProperties;
     39 import android.provider.Settings;
     40 import android.service.quicksettings.Tile;
     41 import android.text.format.DateUtils;
     42 import android.util.Log;
     43 import android.util.LongSparseArray;
     44 
     45 import com.android.systemui.Dependency;
     46 import com.android.systemui.R;
     47 import com.android.systemui.SystemUI;
     48 import com.android.systemui.plugins.qs.QSTile;
     49 import com.android.systemui.qs.QSHost;
     50 import com.android.systemui.qs.tileimpl.QSTileImpl;
     51 
     52 import java.util.ArrayList;
     53 
     54 public class GarbageMonitor {
     55     private static final boolean LEAK_REPORTING_ENABLED =
     56             Build.IS_DEBUGGABLE
     57                     && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
     58     private static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting";
     59 
     60     private static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE;
     61     private static final boolean ENABLE_AM_HEAP_LIMIT = true; // use ActivityManager.setHeapLimit
     62 
     63     private static final String TAG = "GarbageMonitor";
     64 
     65     private static final long GARBAGE_INSPECTION_INTERVAL =
     66             15 * DateUtils.MINUTE_IN_MILLIS; // 15 min
     67     private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min
     68 
     69     private static final int DO_GARBAGE_INSPECTION = 1000;
     70     private static final int DO_HEAP_TRACK = 3000;
     71 
     72     private static final int GARBAGE_ALLOWANCE = 5;
     73 
     74     private final Handler mHandler;
     75     private final TrackedGarbage mTrackedGarbage;
     76     private final LeakReporter mLeakReporter;
     77     private final Context mContext;
     78     private final ActivityManager mAm;
     79     private MemoryTile mQSTile;
     80     private DumpTruck mDumpTruck;
     81 
     82     private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
     83     private final ArrayList<Long> mPids = new ArrayList<>();
     84     private int[] mPidsArray = new int[1];
     85 
     86     private long mHeapLimit;
     87 
     88     public GarbageMonitor(
     89             Context context,
     90             Looper bgLooper,
     91             LeakDetector leakDetector,
     92             LeakReporter leakReporter) {
     93         mContext = context.getApplicationContext();
     94         mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
     95 
     96         mHandler = new BackgroundHeapCheckHandler(bgLooper);
     97 
     98         mTrackedGarbage = leakDetector.getTrackedGarbage();
     99         mLeakReporter = leakReporter;
    100 
    101         mDumpTruck = new DumpTruck(mContext);
    102 
    103         if (ENABLE_AM_HEAP_LIMIT) {
    104             mHeapLimit = mContext.getResources().getInteger(R.integer.watch_heap_limit);
    105         }
    106     }
    107 
    108     public void startLeakMonitor() {
    109         if (mTrackedGarbage == null) {
    110             return;
    111         }
    112 
    113         mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION);
    114     }
    115 
    116     public void startHeapTracking() {
    117         startTrackingProcess(
    118                 android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
    119         mHandler.sendEmptyMessage(DO_HEAP_TRACK);
    120     }
    121 
    122     private boolean gcAndCheckGarbage() {
    123         if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
    124             Runtime.getRuntime().gc();
    125             return true;
    126         }
    127         return false;
    128     }
    129 
    130     void reinspectGarbageAfterGc() {
    131         int count = mTrackedGarbage.countOldGarbage();
    132         if (count > GARBAGE_ALLOWANCE) {
    133             mLeakReporter.dumpLeak(count);
    134         }
    135     }
    136 
    137     public ProcessMemInfo getMemInfo(int pid) {
    138         return mData.get(pid);
    139     }
    140 
    141     public int[] getTrackedProcesses() {
    142         return mPidsArray;
    143     }
    144 
    145     public void startTrackingProcess(long pid, String name, long start) {
    146         synchronized (mPids) {
    147             if (mPids.contains(pid)) return;
    148 
    149             mPids.add(pid);
    150             updatePidsArrayL();
    151 
    152             mData.put(pid, new ProcessMemInfo(pid, name, start));
    153         }
    154     }
    155 
    156     private void updatePidsArrayL() {
    157         final int N = mPids.size();
    158         mPidsArray = new int[N];
    159         StringBuffer sb = new StringBuffer("Now tracking processes: ");
    160         for (int i = 0; i < N; i++) {
    161             final int p = mPids.get(i).intValue();
    162             mPidsArray[i] = p;
    163             sb.append(p);
    164             sb.append(" ");
    165         }
    166         Log.v(TAG, sb.toString());
    167     }
    168 
    169     private void update() {
    170         synchronized (mPids) {
    171             Debug.MemoryInfo[] dinfos = mAm.getProcessMemoryInfo(mPidsArray);
    172             for (int i = 0; i < dinfos.length; i++) {
    173                 Debug.MemoryInfo dinfo = dinfos[i];
    174                 if (i > mPids.size()) {
    175                     Log.e(TAG, "update: unknown process info received: " + dinfo);
    176                     break;
    177                 }
    178                 final long pid = mPids.get(i).intValue();
    179                 final ProcessMemInfo info = mData.get(pid);
    180                 info.head = (info.head + 1) % info.pss.length;
    181                 info.pss[info.head] = info.currentPss = dinfo.getTotalPss();
    182                 info.uss[info.head] = info.currentUss = dinfo.getTotalPrivateDirty();
    183                 if (info.currentPss > info.max) info.max = info.currentPss;
    184                 if (info.currentUss > info.max) info.max = info.currentUss;
    185                 if (info.currentPss == 0) {
    186                     Log.v(TAG, "update: pid " + pid + " has pss=0, it probably died");
    187                     mData.remove(pid);
    188                 }
    189             }
    190             for (int i = mPids.size() - 1; i >= 0; i--) {
    191                 final long pid = mPids.get(i).intValue();
    192                 if (mData.get(pid) == null) {
    193                     mPids.remove(i);
    194                     updatePidsArrayL();
    195                 }
    196             }
    197         }
    198         if (mQSTile != null) mQSTile.update();
    199     }
    200 
    201     private void setTile(MemoryTile tile) {
    202         mQSTile = tile;
    203         if (tile != null) tile.update();
    204     }
    205 
    206     private static String formatBytes(long b) {
    207         String[] SUFFIXES = {"B", "K", "M", "G", "T"};
    208         int i;
    209         for (i = 0; i < SUFFIXES.length; i++) {
    210             if (b < 1024) break;
    211             b /= 1024;
    212         }
    213         return b + SUFFIXES[i];
    214     }
    215 
    216     private void dumpHprofAndShare() {
    217         final Intent share = mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent();
    218         mContext.startActivity(share);
    219     }
    220 
    221     private static class MemoryIconDrawable extends Drawable {
    222         long pss, limit;
    223         final Drawable baseIcon;
    224         final Paint paint = new Paint();
    225         final float dp;
    226 
    227         MemoryIconDrawable(Context context) {
    228             baseIcon = context.getDrawable(R.drawable.ic_memory).mutate();
    229             dp = context.getResources().getDisplayMetrics().density;
    230             paint.setColor(QSTileImpl.getColorForState(context, Tile.STATE_ACTIVE));
    231         }
    232 
    233         public void setPss(long pss) {
    234             if (pss != this.pss) {
    235                 this.pss = pss;
    236                 invalidateSelf();
    237             }
    238         }
    239 
    240         public void setLimit(long limit) {
    241             if (limit != this.limit) {
    242                 this.limit = limit;
    243                 invalidateSelf();
    244             }
    245         }
    246 
    247         @Override
    248         public void draw(Canvas canvas) {
    249             baseIcon.draw(canvas);
    250 
    251             if (limit > 0 && pss > 0) {
    252                 float frac = Math.min(1f, (float) pss / limit);
    253 
    254                 final Rect bounds = getBounds();
    255                 canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp);
    256                 //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
    257                 canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint);
    258             }
    259         }
    260 
    261         @Override
    262         public void setBounds(int left, int top, int right, int bottom) {
    263             super.setBounds(left, top, right, bottom);
    264             baseIcon.setBounds(left, top, right, bottom);
    265         }
    266 
    267         @Override
    268         public int getIntrinsicHeight() {
    269             return baseIcon.getIntrinsicHeight();
    270         }
    271 
    272         @Override
    273         public int getIntrinsicWidth() {
    274             return baseIcon.getIntrinsicWidth();
    275         }
    276 
    277         @Override
    278         public void setAlpha(int i) {
    279             baseIcon.setAlpha(i);
    280         }
    281 
    282         @Override
    283         public void setColorFilter(ColorFilter colorFilter) {
    284             baseIcon.setColorFilter(colorFilter);
    285             paint.setColorFilter(colorFilter);
    286         }
    287 
    288         @Override
    289         public void setTint(int tint) {
    290             super.setTint(tint);
    291             baseIcon.setTint(tint);
    292         }
    293 
    294         @Override
    295         public void setTintList(ColorStateList tint) {
    296             super.setTintList(tint);
    297             baseIcon.setTintList(tint);
    298         }
    299 
    300         @Override
    301         public void setTintMode(PorterDuff.Mode tintMode) {
    302             super.setTintMode(tintMode);
    303             baseIcon.setTintMode(tintMode);
    304         }
    305 
    306         @Override
    307         public int getOpacity() {
    308             return PixelFormat.TRANSLUCENT;
    309         }
    310     }
    311 
    312     private static class MemoryGraphIcon extends QSTile.Icon {
    313         long pss, limit;
    314 
    315         public void setPss(long pss) {
    316             this.pss = pss;
    317         }
    318 
    319         public void setHeapLimit(long limit) {
    320             this.limit = limit;
    321         }
    322 
    323         @Override
    324         public Drawable getDrawable(Context context) {
    325             final MemoryIconDrawable drawable = new MemoryIconDrawable(context);
    326             drawable.setPss(pss);
    327             drawable.setLimit(limit);
    328             return drawable;
    329         }
    330     }
    331 
    332     public static class MemoryTile extends QSTileImpl<QSTile.State> {
    333         public static final String TILE_SPEC = "dbg:mem";
    334 
    335         private final GarbageMonitor gm;
    336         private ProcessMemInfo pmi;
    337 
    338         public MemoryTile(QSHost host) {
    339             super(host);
    340             gm = Dependency.get(GarbageMonitor.class);
    341         }
    342 
    343         @Override
    344         public State newTileState() {
    345             return new QSTile.State();
    346         }
    347 
    348         @Override
    349         public Intent getLongClickIntent() {
    350             return new Intent();
    351         }
    352 
    353         @Override
    354         protected void handleClick() {
    355             getHost().collapsePanels();
    356             mHandler.post(gm::dumpHprofAndShare);
    357         }
    358 
    359         @Override
    360         public int getMetricsCategory() {
    361             return VIEW_UNKNOWN;
    362         }
    363 
    364         @Override
    365         public void handleSetListening(boolean listening) {
    366             if (gm != null) gm.setTile(listening ? this : null);
    367 
    368             final ActivityManager am = mContext.getSystemService(ActivityManager.class);
    369             if (listening && gm.mHeapLimit > 0) {
    370                 am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes?
    371             } else {
    372                 am.clearWatchHeapLimit();
    373             }
    374         }
    375 
    376         @Override
    377         public CharSequence getTileLabel() {
    378             return getState().label;
    379         }
    380 
    381         @Override
    382         protected void handleUpdateState(State state, Object arg) {
    383             pmi = gm.getMemInfo(Process.myPid());
    384             final MemoryGraphIcon icon = new MemoryGraphIcon();
    385             icon.setHeapLimit(gm.mHeapLimit);
    386             if (pmi != null) {
    387                 icon.setPss(pmi.currentPss);
    388                 state.label = mContext.getString(R.string.heap_dump_tile_name);
    389                 state.secondaryLabel =
    390                         String.format(
    391                                 "pss: %s / %s",
    392                                 formatBytes(pmi.currentPss * 1024),
    393                                 formatBytes(gm.mHeapLimit * 1024));
    394             } else {
    395                 icon.setPss(0);
    396                 state.label = "Dump SysUI";
    397                 state.secondaryLabel = null;
    398             }
    399             state.icon = icon;
    400         }
    401 
    402         public void update() {
    403             refreshState();
    404         }
    405 
    406         public long getPss() {
    407             return pmi != null ? pmi.currentPss : 0;
    408         }
    409 
    410         public long getHeapLimit() {
    411             return gm != null ? gm.mHeapLimit : 0;
    412         }
    413     }
    414 
    415     public static class ProcessMemInfo {
    416         public long pid;
    417         public String name;
    418         public long startTime;
    419         public long currentPss, currentUss;
    420         public long[] pss = new long[256];
    421         public long[] uss = new long[256];
    422         public long max = 1;
    423         public int head = 0;
    424 
    425         public ProcessMemInfo(long pid, String name, long start) {
    426             this.pid = pid;
    427             this.name = name;
    428             this.startTime = start;
    429         }
    430 
    431         public long getUptime() {
    432             return System.currentTimeMillis() - startTime;
    433         }
    434     }
    435 
    436     public static class Service extends SystemUI {
    437         private GarbageMonitor mGarbageMonitor;
    438 
    439         @Override
    440         public void start() {
    441             boolean forceEnable =
    442                     Settings.Secure.getInt(
    443                                     mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0)
    444                             != 0;
    445             mGarbageMonitor = Dependency.get(GarbageMonitor.class);
    446             if (LEAK_REPORTING_ENABLED || forceEnable) {
    447                 mGarbageMonitor.startLeakMonitor();
    448             }
    449             if (HEAP_TRACKING_ENABLED || forceEnable) {
    450                 mGarbageMonitor.startHeapTracking();
    451             }
    452         }
    453     }
    454 
    455     private class BackgroundHeapCheckHandler extends Handler {
    456         BackgroundHeapCheckHandler(Looper onLooper) {
    457             super(onLooper);
    458             if (Looper.getMainLooper().equals(onLooper)) {
    459                 throw new RuntimeException(
    460                         "BackgroundHeapCheckHandler may not run on the ui thread");
    461             }
    462         }
    463 
    464         @Override
    465         public void handleMessage(Message m) {
    466             switch (m.what) {
    467                 case DO_GARBAGE_INSPECTION:
    468                     if (gcAndCheckGarbage()) {
    469                         postDelayed(GarbageMonitor.this::reinspectGarbageAfterGc, 100);
    470                     }
    471 
    472                     removeMessages(DO_GARBAGE_INSPECTION);
    473                     sendEmptyMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
    474                     break;
    475 
    476                 case DO_HEAP_TRACK:
    477                     update();
    478                     removeMessages(DO_HEAP_TRACK);
    479                     sendEmptyMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
    480                     break;
    481             }
    482         }
    483     }
    484 }
    485