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 
GarbageMonitor( Context context, Looper bgLooper, LeakDetector leakDetector, LeakReporter leakReporter)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 
startLeakMonitor()108     public void startLeakMonitor() {
109         if (mTrackedGarbage == null) {
110             return;
111         }
112 
113         mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION);
114     }
115 
startHeapTracking()116     public void startHeapTracking() {
117         startTrackingProcess(
118                 android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
119         mHandler.sendEmptyMessage(DO_HEAP_TRACK);
120     }
121 
gcAndCheckGarbage()122     private boolean gcAndCheckGarbage() {
123         if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
124             Runtime.getRuntime().gc();
125             return true;
126         }
127         return false;
128     }
129 
reinspectGarbageAfterGc()130     void reinspectGarbageAfterGc() {
131         int count = mTrackedGarbage.countOldGarbage();
132         if (count > GARBAGE_ALLOWANCE) {
133             mLeakReporter.dumpLeak(count);
134         }
135     }
136 
getMemInfo(int pid)137     public ProcessMemInfo getMemInfo(int pid) {
138         return mData.get(pid);
139     }
140 
getTrackedProcesses()141     public int[] getTrackedProcesses() {
142         return mPidsArray;
143     }
144 
startTrackingProcess(long pid, String name, long start)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 
updatePidsArrayL()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 
update()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 
setTile(MemoryTile tile)201     private void setTile(MemoryTile tile) {
202         mQSTile = tile;
203         if (tile != null) tile.update();
204     }
205 
formatBytes(long b)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 
dumpHprofAndShare()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 
MemoryIconDrawable(Context context)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 
setPss(long pss)233         public void setPss(long pss) {
234             if (pss != this.pss) {
235                 this.pss = pss;
236                 invalidateSelf();
237             }
238         }
239 
setLimit(long limit)240         public void setLimit(long limit) {
241             if (limit != this.limit) {
242                 this.limit = limit;
243                 invalidateSelf();
244             }
245         }
246 
247         @Override
draw(Canvas canvas)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
setBounds(int left, int top, int right, int bottom)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
getIntrinsicHeight()268         public int getIntrinsicHeight() {
269             return baseIcon.getIntrinsicHeight();
270         }
271 
272         @Override
getIntrinsicWidth()273         public int getIntrinsicWidth() {
274             return baseIcon.getIntrinsicWidth();
275         }
276 
277         @Override
setAlpha(int i)278         public void setAlpha(int i) {
279             baseIcon.setAlpha(i);
280         }
281 
282         @Override
setColorFilter(ColorFilter colorFilter)283         public void setColorFilter(ColorFilter colorFilter) {
284             baseIcon.setColorFilter(colorFilter);
285             paint.setColorFilter(colorFilter);
286         }
287 
288         @Override
setTint(int tint)289         public void setTint(int tint) {
290             super.setTint(tint);
291             baseIcon.setTint(tint);
292         }
293 
294         @Override
setTintList(ColorStateList tint)295         public void setTintList(ColorStateList tint) {
296             super.setTintList(tint);
297             baseIcon.setTintList(tint);
298         }
299 
300         @Override
setTintMode(PorterDuff.Mode tintMode)301         public void setTintMode(PorterDuff.Mode tintMode) {
302             super.setTintMode(tintMode);
303             baseIcon.setTintMode(tintMode);
304         }
305 
306         @Override
getOpacity()307         public int getOpacity() {
308             return PixelFormat.TRANSLUCENT;
309         }
310     }
311 
312     private static class MemoryGraphIcon extends QSTile.Icon {
313         long pss, limit;
314 
setPss(long pss)315         public void setPss(long pss) {
316             this.pss = pss;
317         }
318 
setHeapLimit(long limit)319         public void setHeapLimit(long limit) {
320             this.limit = limit;
321         }
322 
323         @Override
getDrawable(Context context)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 
MemoryTile(QSHost host)338         public MemoryTile(QSHost host) {
339             super(host);
340             gm = Dependency.get(GarbageMonitor.class);
341         }
342 
343         @Override
newTileState()344         public State newTileState() {
345             return new QSTile.State();
346         }
347 
348         @Override
getLongClickIntent()349         public Intent getLongClickIntent() {
350             return new Intent();
351         }
352 
353         @Override
handleClick()354         protected void handleClick() {
355             getHost().collapsePanels();
356             mHandler.post(gm::dumpHprofAndShare);
357         }
358 
359         @Override
getMetricsCategory()360         public int getMetricsCategory() {
361             return VIEW_UNKNOWN;
362         }
363 
364         @Override
handleSetListening(boolean listening)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
getTileLabel()377         public CharSequence getTileLabel() {
378             return getState().label;
379         }
380 
381         @Override
handleUpdateState(State state, Object arg)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 
update()402         public void update() {
403             refreshState();
404         }
405 
getPss()406         public long getPss() {
407             return pmi != null ? pmi.currentPss : 0;
408         }
409 
getHeapLimit()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 
ProcessMemInfo(long pid, String name, long start)425         public ProcessMemInfo(long pid, String name, long start) {
426             this.pid = pid;
427             this.name = name;
428             this.startTime = start;
429         }
430 
getUptime()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
start()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 {
BackgroundHeapCheckHandler(Looper onLooper)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
handleMessage(Message m)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