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