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