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