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 android.service.quicksettings.Tile.STATE_ACTIVE;
20 import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE;
21 
22 import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN;
23 
24 import android.annotation.Nullable;
25 import android.app.ActivityManager;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.res.ColorStateList;
29 import android.graphics.Canvas;
30 import android.graphics.Color;
31 import android.graphics.ColorFilter;
32 import android.graphics.Paint;
33 import android.graphics.PixelFormat;
34 import android.graphics.PorterDuff;
35 import android.graphics.Rect;
36 import android.graphics.drawable.Drawable;
37 import android.os.Build;
38 import android.os.Handler;
39 import android.os.Looper;
40 import android.os.Process;
41 import android.os.SystemProperties;
42 import android.provider.Settings;
43 import android.text.format.DateUtils;
44 import android.util.Log;
45 import android.util.LongSparseArray;
46 import android.view.View;
47 
48 import com.android.internal.logging.MetricsLogger;
49 import com.android.systemui.CoreStartable;
50 import com.android.systemui.Dumpable;
51 import com.android.systemui.R;
52 import com.android.systemui.dagger.SysUISingleton;
53 import com.android.systemui.dagger.qualifiers.Background;
54 import com.android.systemui.dagger.qualifiers.Main;
55 import com.android.systemui.dump.DumpManager;
56 import com.android.systemui.plugins.ActivityStarter;
57 import com.android.systemui.plugins.FalsingManager;
58 import com.android.systemui.plugins.qs.QSTile;
59 import com.android.systemui.plugins.statusbar.StatusBarStateController;
60 import com.android.systemui.qs.QSHost;
61 import com.android.systemui.qs.QsEventLogger;
62 import com.android.systemui.qs.logging.QSLogger;
63 import com.android.systemui.qs.pipeline.domain.interactor.PanelInteractor;
64 import com.android.systemui.qs.tileimpl.QSTileImpl;
65 import com.android.systemui.util.concurrency.DelayableExecutor;
66 import com.android.systemui.util.concurrency.MessageRouter;
67 
68 import java.io.PrintWriter;
69 import java.util.ArrayList;
70 import java.util.List;
71 
72 import javax.inject.Inject;
73 
74 /**
75  * Suite of tools to periodically inspect the System UI heap and possibly prompt the user to
76  * capture heap dumps and report them. Includes the implementation of the "Dump SysUI Heap"
77  * quick settings tile.
78  */
79 @SysUISingleton
80 public class GarbageMonitor implements Dumpable {
81     // Feature switches
82     // ================
83 
84     // Whether to use TrackedGarbage to trigger LeakReporter. Off by default unless you set the
85     // appropriate sysprop on a userdebug device.
86     public static final boolean LEAK_REPORTING_ENABLED = Build.IS_DEBUGGABLE
87             && SystemProperties.getBoolean("debug.enable_leak_reporting", false);
88     public static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting";
89 
90     // Heap tracking: watch the current memory levels and update the MemoryTile if available.
91     // On for all userdebug devices.
92     public static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE;
93 
94     // Tell QSTileHost.java to toss this into the default tileset?
95     public static final boolean ADD_MEMORY_TILE_TO_DEFAULT_ON_DEBUGGABLE_BUILDS = true;
96 
97     // whether to use ActivityManager.setHeapLimit (and post a notification to the user asking
98     // to dump the heap). Off by default unless you set the appropriate sysprop on userdebug
99     private static final boolean ENABLE_AM_HEAP_LIMIT = Build.IS_DEBUGGABLE
100             && SystemProperties.getBoolean("debug.enable_sysui_heap_limit", false);
101 
102     // Tuning params
103     // =============
104 
105     // threshold for setHeapLimit(), in KB (overrides R.integer.watch_heap_limit)
106     private static final String SETTINGS_KEY_AM_HEAP_LIMIT = "systemui_am_heap_limit";
107 
108     private static final long GARBAGE_INSPECTION_INTERVAL =
109             15 * DateUtils.MINUTE_IN_MILLIS; // 15 min
110     private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min
111     private static final int HEAP_TRACK_HISTORY_LEN = 720; // 12 hours
112 
113     private static final int DO_GARBAGE_INSPECTION = 1000;
114     private static final int DO_HEAP_TRACK = 3000;
115 
116     static final int GARBAGE_ALLOWANCE = 5;
117 
118     private static final String TAG = "GarbageMonitor";
119     private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
120 
121     private final MessageRouter mMessageRouter;
122     private final TrackedGarbage mTrackedGarbage;
123     private final LeakReporter mLeakReporter;
124     private final Context mContext;
125     private final DelayableExecutor mDelayableExecutor;
126     private MemoryTile mQSTile;
127     private final DumpTruck mDumpTruck;
128 
129     private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>();
130     private final ArrayList<Long> mPids = new ArrayList<>();
131 
132     private long mHeapLimit;
133 
134     /**
135      */
136     @Inject
GarbageMonitor( Context context, @Background DelayableExecutor delayableExecutor, @Background MessageRouter messageRouter, LeakDetector leakDetector, LeakReporter leakReporter, DumpManager dumpManager)137     public GarbageMonitor(
138             Context context,
139             @Background DelayableExecutor delayableExecutor,
140             @Background MessageRouter messageRouter,
141             LeakDetector leakDetector,
142             LeakReporter leakReporter,
143             DumpManager dumpManager) {
144         mContext = context.getApplicationContext();
145 
146         mDelayableExecutor = delayableExecutor;
147         mMessageRouter = messageRouter;
148         mMessageRouter.subscribeTo(DO_GARBAGE_INSPECTION, this::doGarbageInspection);
149         mMessageRouter.subscribeTo(DO_HEAP_TRACK, this::doHeapTrack);
150 
151         mTrackedGarbage = leakDetector.getTrackedGarbage();
152         mLeakReporter = leakReporter;
153 
154         mDumpTruck = new DumpTruck(mContext, this);
155 
156         dumpManager.registerDumpable(getClass().getSimpleName(), this);
157 
158         if (ENABLE_AM_HEAP_LIMIT) {
159             mHeapLimit = Settings.Global.getInt(context.getContentResolver(),
160                     SETTINGS_KEY_AM_HEAP_LIMIT,
161                     mContext.getResources().getInteger(R.integer.watch_heap_limit));
162         }
163     }
164 
startLeakMonitor()165     public void startLeakMonitor() {
166         if (mTrackedGarbage == null) {
167             return;
168         }
169 
170         mMessageRouter.sendMessage(DO_GARBAGE_INSPECTION);
171     }
172 
startHeapTracking()173     public void startHeapTracking() {
174         startTrackingProcess(
175                 android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis());
176         mMessageRouter.sendMessage(DO_HEAP_TRACK);
177     }
178 
gcAndCheckGarbage()179     private boolean gcAndCheckGarbage() {
180         if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
181             Runtime.getRuntime().gc();
182             return true;
183         }
184         return false;
185     }
186 
reinspectGarbageAfterGc()187     void reinspectGarbageAfterGc() {
188         int count = mTrackedGarbage.countOldGarbage();
189         if (count > GARBAGE_ALLOWANCE) {
190             mLeakReporter.dumpLeak(count);
191         }
192     }
193 
getMemInfo(int pid)194     public ProcessMemInfo getMemInfo(int pid) {
195         return mData.get(pid);
196     }
197 
getTrackedProcesses()198     public List<Long> getTrackedProcesses() {
199         return mPids;
200     }
201 
startTrackingProcess(long pid, String name, long start)202     public void startTrackingProcess(long pid, String name, long start) {
203         synchronized (mPids) {
204             if (mPids.contains(pid)) return;
205 
206             mPids.add(pid);
207             logPids();
208 
209             mData.put(pid, new ProcessMemInfo(pid, name, start));
210         }
211     }
212 
logPids()213     private void logPids() {
214         if (DEBUG) {
215             StringBuffer sb = new StringBuffer("Now tracking processes: ");
216             for (int i = 0; i < mPids.size(); i++) {
217                 final int p = mPids.get(i).intValue();
218                 sb.append(" ");
219             }
220             Log.v(TAG, sb.toString());
221         }
222     }
223 
update()224     private void update() {
225         synchronized (mPids) {
226             for (int i = 0; i < mPids.size(); i++) {
227                 final int pid = mPids.get(i).intValue();
228                 // rssValues contains [VmRSS, RssFile, RssAnon, VmSwap].
229                 long[] rssValues = Process.getRss(pid);
230                 if (rssValues == null && rssValues.length == 0) {
231                     if (DEBUG) Log.e(TAG, "update: Process.getRss() didn't provide any values.");
232                     break;
233                 }
234                 long rss = rssValues[0];
235                 final ProcessMemInfo info = mData.get(pid);
236                 info.rss[info.head] = info.currentRss = rss;
237                 info.head = (info.head + 1) % info.rss.length;
238                 if (info.currentRss > info.max) info.max = info.currentRss;
239                 if (info.currentRss == 0) {
240                     if (DEBUG) Log.v(TAG, "update: pid " + pid + " has rss=0, it probably died");
241                     mData.remove(pid);
242                 }
243             }
244             for (int i = mPids.size() - 1; i >= 0; i--) {
245                 final long pid = mPids.get(i).intValue();
246                 if (mData.get(pid) == null) {
247                     mPids.remove(i);
248                     logPids();
249                 }
250             }
251         }
252         if (mQSTile != null) mQSTile.update();
253     }
254 
setTile(MemoryTile tile)255     private void setTile(MemoryTile tile) {
256         mQSTile = tile;
257         if (tile != null) tile.update();
258     }
259 
formatBytes(long b)260     private static String formatBytes(long b) {
261         String[] SUFFIXES = {"B", "K", "M", "G", "T"};
262         int i;
263         for (i = 0; i < SUFFIXES.length; i++) {
264             if (b < 1024) break;
265             b /= 1024;
266         }
267         return b + SUFFIXES[i];
268     }
269 
dumpHprofAndGetShareIntent()270     private Intent dumpHprofAndGetShareIntent() {
271         return mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent();
272     }
273 
274     @Override
dump(PrintWriter pw, @Nullable String[] args)275     public void dump(PrintWriter pw, @Nullable String[] args) {
276         pw.println("GarbageMonitor params:");
277         pw.println(String.format("   mHeapLimit=%d KB", mHeapLimit));
278         pw.println(String.format("   GARBAGE_INSPECTION_INTERVAL=%d (%.1f mins)",
279                 GARBAGE_INSPECTION_INTERVAL,
280                 (float) GARBAGE_INSPECTION_INTERVAL / DateUtils.MINUTE_IN_MILLIS));
281         final float htiMins = HEAP_TRACK_INTERVAL / DateUtils.MINUTE_IN_MILLIS;
282         pw.println(String.format("   HEAP_TRACK_INTERVAL=%d (%.1f mins)",
283                 HEAP_TRACK_INTERVAL,
284                 htiMins));
285         pw.println(String.format("   HEAP_TRACK_HISTORY_LEN=%d (%.1f hr total)",
286                 HEAP_TRACK_HISTORY_LEN,
287                 (float) HEAP_TRACK_HISTORY_LEN * htiMins / 60f));
288 
289         pw.println("GarbageMonitor tracked processes:");
290 
291         for (long pid : mPids) {
292             final ProcessMemInfo pmi = mData.get(pid);
293             if (pmi != null) {
294                 pmi.dump(pw, args);
295             }
296         }
297     }
298 
299 
300     private static class MemoryIconDrawable extends Drawable {
301         long rss, limit;
302         final Drawable baseIcon;
303         final Paint paint = new Paint();
304         final float dp;
305 
MemoryIconDrawable(Context context)306         MemoryIconDrawable(Context context) {
307             baseIcon = context.getDrawable(R.drawable.ic_memory).mutate();
308             dp = context.getResources().getDisplayMetrics().density;
309             paint.setColor(Color.WHITE);
310         }
311 
setRss(long rss)312         public void setRss(long rss) {
313             if (rss != this.rss) {
314                 this.rss = rss;
315                 invalidateSelf();
316             }
317         }
318 
setLimit(long limit)319         public void setLimit(long limit) {
320             if (limit != this.limit) {
321                 this.limit = limit;
322                 invalidateSelf();
323             }
324         }
325 
326         @Override
draw(Canvas canvas)327         public void draw(Canvas canvas) {
328             baseIcon.draw(canvas);
329 
330             if (limit > 0 && rss > 0) {
331                 float frac = Math.min(1f, (float) rss / limit);
332 
333                 final Rect bounds = getBounds();
334                 canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp);
335                 //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z"
336                 canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint);
337             }
338         }
339 
340         @Override
setBounds(int left, int top, int right, int bottom)341         public void setBounds(int left, int top, int right, int bottom) {
342             super.setBounds(left, top, right, bottom);
343             baseIcon.setBounds(left, top, right, bottom);
344         }
345 
346         @Override
getIntrinsicHeight()347         public int getIntrinsicHeight() {
348             return baseIcon.getIntrinsicHeight();
349         }
350 
351         @Override
getIntrinsicWidth()352         public int getIntrinsicWidth() {
353             return baseIcon.getIntrinsicWidth();
354         }
355 
356         @Override
setAlpha(int i)357         public void setAlpha(int i) {
358             baseIcon.setAlpha(i);
359         }
360 
361         @Override
setColorFilter(ColorFilter colorFilter)362         public void setColorFilter(ColorFilter colorFilter) {
363             baseIcon.setColorFilter(colorFilter);
364             paint.setColorFilter(colorFilter);
365         }
366 
367         @Override
setTint(int tint)368         public void setTint(int tint) {
369             super.setTint(tint);
370             baseIcon.setTint(tint);
371         }
372 
373         @Override
setTintList(ColorStateList tint)374         public void setTintList(ColorStateList tint) {
375             super.setTintList(tint);
376             baseIcon.setTintList(tint);
377         }
378 
379         @Override
setTintMode(PorterDuff.Mode tintMode)380         public void setTintMode(PorterDuff.Mode tintMode) {
381             super.setTintMode(tintMode);
382             baseIcon.setTintMode(tintMode);
383         }
384 
385         @Override
getOpacity()386         public int getOpacity() {
387             return PixelFormat.TRANSLUCENT;
388         }
389     }
390 
391     private static class MemoryGraphIcon extends QSTile.Icon {
392         long rss, limit;
393 
setRss(long rss)394         public void setRss(long rss) {
395             this.rss = rss;
396         }
397 
setHeapLimit(long limit)398         public void setHeapLimit(long limit) {
399             this.limit = limit;
400         }
401 
402         @Override
getDrawable(Context context)403         public Drawable getDrawable(Context context) {
404             final MemoryIconDrawable drawable = new MemoryIconDrawable(context);
405             drawable.setRss(rss);
406             drawable.setLimit(limit);
407             return drawable;
408         }
409     }
410 
411     public static class MemoryTile extends QSTileImpl<QSTile.State> {
412         public static final String TILE_SPEC = "dbg:mem";
413 
414         private final GarbageMonitor gm;
415         private ProcessMemInfo pmi;
416         private boolean dumpInProgress;
417         private final PanelInteractor mPanelInteractor;
418 
419         @Inject
MemoryTile( QSHost host, QsEventLogger uiEventLogger, @Background Looper backgroundLooper, @Main Handler mainHandler, FalsingManager falsingManager, MetricsLogger metricsLogger, StatusBarStateController statusBarStateController, ActivityStarter activityStarter, QSLogger qsLogger, GarbageMonitor monitor, PanelInteractor panelInteractor )420         public MemoryTile(
421                 QSHost host,
422                 QsEventLogger uiEventLogger,
423                 @Background Looper backgroundLooper,
424                 @Main Handler mainHandler,
425                 FalsingManager falsingManager,
426                 MetricsLogger metricsLogger,
427                 StatusBarStateController statusBarStateController,
428                 ActivityStarter activityStarter,
429                 QSLogger qsLogger,
430                 GarbageMonitor monitor,
431                 PanelInteractor panelInteractor
432         ) {
433             super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger,
434                     statusBarStateController, activityStarter, qsLogger);
435             gm = monitor;
436             mPanelInteractor = panelInteractor;
437         }
438 
439         @Override
newTileState()440         public State newTileState() {
441             return new QSTile.State();
442         }
443 
444         @Override
getLongClickIntent()445         public Intent getLongClickIntent() {
446             return new Intent();
447         }
448 
449         @Override
handleClick(@ullable View view)450         protected void handleClick(@Nullable View view) {
451             if (dumpInProgress) return;
452 
453             dumpInProgress = true;
454             refreshState();
455             new Thread("HeapDumpThread") {
456                 @Override
457                 public void run() {
458                     try {
459                         // wait for animations & state changes
460                         Thread.sleep(500);
461                     } catch (InterruptedException ignored) { }
462                     final Intent shareIntent = gm.dumpHprofAndGetShareIntent();
463                     mHandler.post(() -> {
464                         dumpInProgress = false;
465                         refreshState();
466                         mPanelInteractor.collapsePanels();
467                         mActivityStarter.postStartActivityDismissingKeyguard(shareIntent, 0);
468                     });
469                 }
470             }.start();
471         }
472 
473         @Override
getMetricsCategory()474         public int getMetricsCategory() {
475             return VIEW_UNKNOWN;
476         }
477 
478         @Override
handleSetListening(boolean listening)479         public void handleSetListening(boolean listening) {
480             super.handleSetListening(listening);
481             if (gm != null) gm.setTile(listening ? this : null);
482 
483             final ActivityManager am = mContext.getSystemService(ActivityManager.class);
484             if (listening && gm.mHeapLimit > 0) {
485                 am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes?
486             } else {
487                 am.clearWatchHeapLimit();
488             }
489         }
490 
491         @Override
getTileLabel()492         public CharSequence getTileLabel() {
493             return getState().label;
494         }
495 
496         @Override
handleUpdateState(State state, Object arg)497         protected void handleUpdateState(State state, Object arg) {
498             pmi = gm.getMemInfo(Process.myPid());
499             final MemoryGraphIcon icon = new MemoryGraphIcon();
500             icon.setHeapLimit(gm.mHeapLimit);
501             state.state = dumpInProgress ? STATE_UNAVAILABLE : STATE_ACTIVE;
502             state.label = dumpInProgress
503                     ? "Dumping..."
504                     : mContext.getString(R.string.heap_dump_tile_name);
505             if (pmi != null) {
506                 icon.setRss(pmi.currentRss);
507                 state.secondaryLabel =
508                         String.format(
509                                 "rss: %s / %s",
510                                 formatBytes(pmi.currentRss * 1024),
511                                 formatBytes(gm.mHeapLimit * 1024));
512             } else {
513                 icon.setRss(0);
514                 state.secondaryLabel = null;
515             }
516             state.icon = icon;
517         }
518 
update()519         public void update() {
520             refreshState();
521         }
522 
getRss()523         public long getRss() {
524             return pmi != null ? pmi.currentRss : 0;
525         }
526 
getHeapLimit()527         public long getHeapLimit() {
528             return gm != null ? gm.mHeapLimit : 0;
529         }
530     }
531 
532     /** */
533     public static class ProcessMemInfo implements Dumpable {
534         public long pid;
535         public String name;
536         public long startTime;
537         public long currentRss;
538         public long[] rss = new long[HEAP_TRACK_HISTORY_LEN];
539         public long max = 1;
540         public int head = 0;
541 
ProcessMemInfo(long pid, String name, long start)542         public ProcessMemInfo(long pid, String name, long start) {
543             this.pid = pid;
544             this.name = name;
545             this.startTime = start;
546         }
547 
getUptime()548         public long getUptime() {
549             return System.currentTimeMillis() - startTime;
550         }
551 
552         @Override
dump(PrintWriter pw, @Nullable String[] args)553         public void dump(PrintWriter pw, @Nullable String[] args) {
554             pw.print("{ \"pid\": ");
555             pw.print(pid);
556             pw.print(", \"name\": \"");
557             pw.print(name.replace('"', '-'));
558             pw.print("\", \"start\": ");
559             pw.print(startTime);
560             pw.print(", \"rss\": [");
561             // write rss values starting from the oldest, which is rss[head], wrapping around to
562             // rss[(head-1) % rss.length]
563             for (int i = 0; i < rss.length; i++) {
564                 if (i > 0) pw.print(",");
565                 pw.print(rss[(head + i) % rss.length]);
566             }
567             pw.println("] }");
568         }
569     }
570 
571     /** */
572     @SysUISingleton
573     public static class Service implements CoreStartable,  Dumpable {
574         private final Context mContext;
575         private final GarbageMonitor mGarbageMonitor;
576 
577         @Inject
Service(Context context, GarbageMonitor garbageMonitor)578         public Service(Context context, GarbageMonitor garbageMonitor) {
579             mContext = context;
580             mGarbageMonitor = garbageMonitor;
581         }
582 
583         @Override
start()584         public void start() {
585             boolean forceEnable =
586                     Settings.Secure.getInt(
587                                     mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0)
588                             != 0;
589             if (LEAK_REPORTING_ENABLED || forceEnable) {
590                 mGarbageMonitor.startLeakMonitor();
591             }
592             if (HEAP_TRACKING_ENABLED || forceEnable) {
593                 mGarbageMonitor.startHeapTracking();
594             }
595         }
596 
597         @Override
dump(PrintWriter pw, @Nullable String[] args)598         public void dump(PrintWriter pw, @Nullable String[] args) {
599             if (mGarbageMonitor != null) mGarbageMonitor.dump(pw, args);
600         }
601     }
602 
doGarbageInspection(int id)603     private void doGarbageInspection(int id) {
604         if (gcAndCheckGarbage()) {
605             mDelayableExecutor.executeDelayed(this::reinspectGarbageAfterGc, 100);
606         }
607 
608         mMessageRouter.cancelMessages(DO_GARBAGE_INSPECTION);
609         mMessageRouter.sendMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL);
610     }
611 
doHeapTrack(int id)612     private void doHeapTrack(int id) {
613         update();
614         mMessageRouter.cancelMessages(DO_HEAP_TRACK);
615         mMessageRouter.sendMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL);
616     }
617 }
618