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