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