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