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