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