diff options
10 files changed, 721 insertions, 105 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index b5407dc0fd3a..d53f1a02b514 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -607,5 +607,6 @@ <action android:name="com.android.intent.action.SHOW_KEYBOARD_SHORTCUTS" /> </intent-filter> </receiver> + </application> </manifest> diff --git a/packages/SystemUI/res/drawable/ic_memory.xml b/packages/SystemUI/res/drawable/ic_memory.xml new file mode 100644 index 000000000000..ada36c58ff1d --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_memory.xml @@ -0,0 +1,28 @@ +<!-- +Copyright (C) 2018 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z" + android:fillAlpha="0.5" + android:fillColor="#000000"/> + <path + android:pathData="M6,9 L6,7 L4,7 L4,5 L6,5 C6,3.9 6.9,3 8,3 L16,3 C17.1,3 18,3.9 18,5 L20,5 L20,7 L18,7 L18,9 L20,9 L20,11 L18,11 L18,13 L20,13 L20,15 L18,15 L18,17 L20,17 L20,19 L18,19 C18,20.1 17.1,21 16,21 L8,21 C6.9,21 6,20.1 6,19 L4,19 L4,17 L6,17 L6,15 L4,15 L4,13 L6,13 L6,11 L4,11 L4,9 L6,9 Z M16,19 L16,5 L8,5 L8,19 L16,19 Z" + android:fillColor="#000000"/> +</vector> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 88e3331ff76c..b9cde7c31338 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -491,4 +491,6 @@ This name is in the ComponentName flattened format (package/class) --> <string name="config_screenshotEditor" translatable="false"></string> + <!-- On debuggable builds, alert the user if SystemUI PSS goes over this number (in kb) --> + <integer name="watch_heap_limit">256000</integer> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 50e7b5c82e1a..1b4b15e50902 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2227,4 +2227,9 @@ <!-- URl of the webpage that explains battery saver. --> <string name="help_uri_battery_saver_learn_more_link_target" translatable="false"></string> + + <!-- Name for a quick settings tile, used only by platform developers, to extract the SystemUI process memory and send it to another + app for debugging. Will not be seen by users. [CHAR LIMIT=20] --> + <string name="heap_dump_tile_name">Dump SysUI Heap</string> + </resources> diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java index 7d19784aad03..beb3c5398780 100644 --- a/packages/SystemUI/src/com/android/systemui/Dependency.java +++ b/packages/SystemUI/src/com/android/systemui/Dependency.java @@ -247,10 +247,14 @@ public class Dependency extends SystemUI { getDependency(LeakDetector.class), getDependency(LEAK_REPORT_EMAIL))); - mProviders.put(GarbageMonitor.class, () -> new GarbageMonitor( - getDependency(BG_LOOPER), - getDependency(LeakDetector.class), - getDependency(LeakReporter.class))); + mProviders.put( + GarbageMonitor.class, + () -> + new GarbageMonitor( + mContext, + getDependency(BG_LOOPER), + getDependency(LeakDetector.class), + getDependency(LeakReporter.class))); mProviders.put(TunerService.class, () -> new TunerServiceImpl(mContext)); diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index 9593b0fb9e9b..53a576d3519d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -24,6 +24,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.graphics.drawable.Drawable; +import android.os.Build; import android.os.Handler; import android.service.quicksettings.TileService; import android.text.TextUtils; @@ -37,8 +38,10 @@ import com.android.systemui.plugins.qs.QSTile.State; import com.android.systemui.qs.QSTileHost; import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon; +import com.android.systemui.util.leak.GarbageMonitor; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -68,7 +71,6 @@ public class TileQueryHelper { // Enqueue jobs to fetch every system tile and then ever package tile. addStockTiles(host); addPackageTiles(host); - // TODO: Live? } public boolean isFinished() { @@ -77,10 +79,14 @@ public class TileQueryHelper { private void addStockTiles(QSTileHost host) { String possible = mContext.getString(R.string.quick_settings_tiles_stock); - String[] possibleTiles = possible.split(","); + final ArrayList<String> possibleTiles = new ArrayList<>(); + possibleTiles.addAll(Arrays.asList(possible.split(","))); + if (Build.IS_DEBUGGABLE) { + possibleTiles.add(GarbageMonitor.MemoryTile.TILE_SPEC); + } + final ArrayList<QSTile> tilesToAdd = new ArrayList<>(); - for (int i = 0; i < possibleTiles.length; i++) { - final String spec = possibleTiles[i]; + for (String spec : possibleTiles) { final QSTile tile = host.createTile(spec); if (tile == null) { continue; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java index 8d488903ad49..ac7ef5dc308e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSFactoryImpl.java @@ -15,6 +15,7 @@ package com.android.systemui.qs.tileimpl; import android.content.Context; +import android.os.Build; import android.util.Log; import android.view.ContextThemeWrapper; @@ -41,6 +42,7 @@ import com.android.systemui.qs.tiles.UserTile; import com.android.systemui.qs.tiles.WifiTile; import com.android.systemui.qs.tiles.WorkModeTile; import com.android.systemui.qs.QSTileHost; +import com.android.systemui.util.leak.GarbageMonitor; public class QSFactoryImpl implements QSFactory { @@ -60,30 +62,58 @@ public class QSFactoryImpl implements QSFactory { } private QSTileImpl createTileInternal(String tileSpec) { - if (tileSpec.equals("wifi")) return new WifiTile(mHost); - else if (tileSpec.equals("bt")) return new BluetoothTile(mHost); - else if (tileSpec.equals("cell")) return new CellularTile(mHost); - else if (tileSpec.equals("dnd")) return new DndTile(mHost); - else if (tileSpec.equals("inversion")) return new ColorInversionTile(mHost); - else if (tileSpec.equals("airplane")) return new AirplaneModeTile(mHost); - else if (tileSpec.equals("work")) return new WorkModeTile(mHost); - else if (tileSpec.equals("rotation")) return new RotationLockTile(mHost); - else if (tileSpec.equals("flashlight")) return new FlashlightTile(mHost); - else if (tileSpec.equals("location")) return new LocationTile(mHost); - else if (tileSpec.equals("cast")) return new CastTile(mHost); - else if (tileSpec.equals("hotspot")) return new HotspotTile(mHost); - else if (tileSpec.equals("user")) return new UserTile(mHost); - else if (tileSpec.equals("battery")) return new BatterySaverTile(mHost); - else if (tileSpec.equals("saver")) return new DataSaverTile(mHost); - else if (tileSpec.equals("night")) return new NightDisplayTile(mHost); - else if (tileSpec.equals("nfc")) return new NfcTile(mHost); + // Stock tiles. + switch (tileSpec) { + case "wifi": + return new WifiTile(mHost); + case "bt": + return new BluetoothTile(mHost); + case "cell": + return new CellularTile(mHost); + case "dnd": + return new DndTile(mHost); + case "inversion": + return new ColorInversionTile(mHost); + case "airplane": + return new AirplaneModeTile(mHost); + case "work": + return new WorkModeTile(mHost); + case "rotation": + return new RotationLockTile(mHost); + case "flashlight": + return new FlashlightTile(mHost); + case "location": + return new LocationTile(mHost); + case "cast": + return new CastTile(mHost); + case "hotspot": + return new HotspotTile(mHost); + case "user": + return new UserTile(mHost); + case "battery": + return new BatterySaverTile(mHost); + case "saver": + return new DataSaverTile(mHost); + case "night": + return new NightDisplayTile(mHost); + case "nfc": + return new NfcTile(mHost); + } + // Intent tiles. - else if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(mHost, tileSpec); - else if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec); - else { - Log.w(TAG, "Bad tile spec: " + tileSpec); - return null; + if (tileSpec.startsWith(IntentTile.PREFIX)) return IntentTile.create(mHost, tileSpec); + if (tileSpec.startsWith(CustomTile.PREFIX)) return CustomTile.create(mHost, tileSpec); + + // Debug tiles. + if (Build.IS_DEBUGGABLE) { + if (tileSpec.equals(GarbageMonitor.MemoryTile.TILE_SPEC)) { + return new GarbageMonitor.MemoryTile(mHost); + } } + + // Broken tiles. + Log.w(TAG, "Bad tile spec: " + tileSpec); + return null; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java new file mode 100644 index 000000000000..2995eba2b8a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/leak/DumpTruck.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ + +package com.android.systemui.util.leak; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.support.v4.content.FileProvider; +import android.util.Log; + +import com.android.systemui.Dependency; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * Utility class for dumping, compressing, sending, and serving heap dump files. + * + * <p>Unlike the Internet, this IS a big truck you can dump something on. + */ +public class DumpTruck { + private static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider"; + private static final String FILEPROVIDER_PATH = "leak"; + + private static final String TAG = "DumpTruck"; + private static final int BUFSIZ = 512 * 1024; // 512K + + private final Context context; + private Uri hprofUri; + final StringBuilder body = new StringBuilder(); + + public DumpTruck(Context context) { + this.context = context; + } + + /** + * Capture memory for the given processes and zip them up for sharing. + * + * @param pids + * @return this, for chaining + */ + public DumpTruck captureHeaps(int[] pids) { + final GarbageMonitor gm = Dependency.get(GarbageMonitor.class); + + final File dumpDir = new File(context.getCacheDir(), FILEPROVIDER_PATH); + dumpDir.mkdirs(); + hprofUri = null; + + body.setLength(0); + body.append("Build: ").append(Build.DISPLAY).append("\n\nProcesses:\n"); + + final ArrayList<String> paths = new ArrayList<String>(); + final int myPid = android.os.Process.myPid(); + + final int[] pids_copy = Arrays.copyOf(pids, pids.length); + for (int pid : pids_copy) { + body.append(" pid ").append(pid); + if (gm != null) { + GarbageMonitor.ProcessMemInfo info = gm.getMemInfo(pid); + if (info != null) { + body.append(":") + .append(" up=") + .append(info.getUptime()) + .append(" pss=") + .append(info.currentPss) + .append(" uss=") + .append(info.currentUss); + } + } + if (pid == myPid) { + final String path = + new File(dumpDir, String.format("heap-%d.ahprof", pid)).getPath(); + Log.v(TAG, "Dumping memory info for process " + pid + " to " + path); + try { + android.os.Debug.dumpHprofData(path); // will block + paths.add(path); + body.append(" (hprof attached)"); + } catch (IOException e) { + Log.e(TAG, "error dumping memory:", e); + body.append("\n** Could not dump heap: \n").append(e.toString()).append("\n"); + } + } + body.append("\n"); + } + + try { + final String zipfile = + new File(dumpDir, String.format("hprof-%d.zip", System.currentTimeMillis())) + .getCanonicalPath(); + if (DumpTruck.zipUp(zipfile, paths)) { + final File pathFile = new File(zipfile); + hprofUri = FileProvider.getUriForFile(context, FILEPROVIDER_AUTHORITY, pathFile); + } + } catch (IOException e) { + Log.e(TAG, "unable to zip up heapdumps", e); + body.append("\n** Could not zip up files: \n").append(e.toString()).append("\n"); + } + + return this; + } + + /** + * Get the Uri of the current heap dump. Be sure to call captureHeaps first. + * + * @return Uri to the dump served by the SystemUI file provider + */ + public Uri getDumpUri() { + return hprofUri; + } + + /** + * Get an ACTION_SEND intent suitable for startActivity() or attaching to a Notification. + * + * @return share intent + */ + public Intent createShareIntent() { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, "SystemUI memory dump"); + + shareIntent.putExtra(Intent.EXTRA_TEXT, body.toString()); + + if (hprofUri != null) { + shareIntent.setType("application/zip"); + shareIntent.putExtra(Intent.EXTRA_STREAM, hprofUri); + } + return shareIntent; + } + + private static boolean zipUp(String zipfilePath, ArrayList<String> paths) { + try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipfilePath))) { + final byte[] buf = new byte[BUFSIZ]; + + for (String filename : paths) { + try (InputStream is = new BufferedInputStream(new FileInputStream(filename))) { + ZipEntry entry = new ZipEntry(filename); + zos.putNextEntry(entry); + int len; + while (0 < (len = is.read(buf, 0, BUFSIZ))) { + zos.write(buf, 0, len); + } + zos.closeEntry(); + } + } + return true; + } catch (IOException e) { + Log.e(TAG, "error zipping up profile data", e); + } + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java index 021f9c4f438b..b2cc2694916e 100644 --- a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java +++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java @@ -16,88 +16,469 @@ package com.android.systemui.util.leak; +import static com.android.internal.logging.MetricsLogger.VIEW_UNKNOWN; +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; import android.os.Build; +import android.os.Debug; import android.os.Handler; import android.os.Looper; +import android.os.Message; +import android.os.Process; import android.os.SystemProperties; import android.provider.Settings; -import android.support.annotation.VisibleForTesting; +import android.service.quicksettings.Tile; +import android.text.format.DateUtils; +import android.util.Log; +import android.util.LongSparseArray; import com.android.systemui.Dependency; +import com.android.systemui.R; import com.android.systemui.SystemUI; +import com.android.systemui.plugins.qs.QSTile; +import com.android.systemui.qs.QSHost; +import com.android.systemui.qs.tileimpl.QSTileImpl; + +import java.util.ArrayList; public class GarbageMonitor { + private static final boolean LEAK_REPORTING_ENABLED = + Build.IS_DEBUGGABLE + && SystemProperties.getBoolean("debug.enable_leak_reporting", false); + private static final String FORCE_ENABLE_LEAK_REPORTING = "sysui_force_enable_leak_reporting"; + + private static final boolean HEAP_TRACKING_ENABLED = Build.IS_DEBUGGABLE; + private static final boolean ENABLE_AM_HEAP_LIMIT = true; // use ActivityManager.setHeapLimit private static final String TAG = "GarbageMonitor"; - private static final long GARBAGE_INSPECTION_INTERVAL = 5 * 60 * 1000; // 5min + private static final long GARBAGE_INSPECTION_INTERVAL = + 15 * DateUtils.MINUTE_IN_MILLIS; // 15 min + private static final long HEAP_TRACK_INTERVAL = 1 * DateUtils.MINUTE_IN_MILLIS; // 1 min + + private static final int DO_GARBAGE_INSPECTION = 1000; + private static final int DO_HEAP_TRACK = 3000; + private static final int GARBAGE_ALLOWANCE = 5; private final Handler mHandler; private final TrackedGarbage mTrackedGarbage; private final LeakReporter mLeakReporter; + private final Context mContext; + private final ActivityManager mAm; + private MemoryTile mQSTile; + private DumpTruck mDumpTruck; + + private final LongSparseArray<ProcessMemInfo> mData = new LongSparseArray<>(); + private final ArrayList<Long> mPids = new ArrayList<>(); + private int[] mPidsArray = new int[1]; - public GarbageMonitor(Looper bgLooper, LeakDetector leakDetector, + private long mHeapLimit; + + public GarbageMonitor( + Context context, + Looper bgLooper, + LeakDetector leakDetector, LeakReporter leakReporter) { - mHandler = bgLooper != null ? new Handler(bgLooper): null; + mContext = context.getApplicationContext(); + mAm = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + mHandler = new BackgroundHeapCheckHandler(bgLooper); + mTrackedGarbage = leakDetector.getTrackedGarbage(); mLeakReporter = leakReporter; + + mDumpTruck = new DumpTruck(mContext); + + if (ENABLE_AM_HEAP_LIMIT) { + mHeapLimit = mContext.getResources().getInteger(R.integer.watch_heap_limit); + } } - public void start() { + public void startLeakMonitor() { if (mTrackedGarbage == null) { return; } - scheduleInspectGarbage(this::inspectGarbage); + mHandler.sendEmptyMessage(DO_GARBAGE_INSPECTION); } - @VisibleForTesting - void scheduleInspectGarbage(Runnable runnable) { - mHandler.postDelayed(runnable, GARBAGE_INSPECTION_INTERVAL); + public void startHeapTracking() { + startTrackingProcess( + android.os.Process.myPid(), mContext.getPackageName(), System.currentTimeMillis()); + mHandler.sendEmptyMessage(DO_HEAP_TRACK); } - private void inspectGarbage() { + private boolean gcAndCheckGarbage() { if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) { Runtime.getRuntime().gc(); - - // Allow some time to for ReferenceQueue to catch up. - scheduleReinspectGarbage(this::reinspectGarbageAfterGc); + return true; } - scheduleInspectGarbage(this::inspectGarbage); - } - - @VisibleForTesting - void scheduleReinspectGarbage(Runnable runnable) { - mHandler.postDelayed(runnable, (long) 100); + return false; } - private void reinspectGarbageAfterGc() { + void reinspectGarbageAfterGc() { int count = mTrackedGarbage.countOldGarbage(); if (count > GARBAGE_ALLOWANCE) { mLeakReporter.dumpLeak(count); } } - public static class Service extends SystemUI { + public ProcessMemInfo getMemInfo(int pid) { + return mData.get(pid); + } + + public int[] getTrackedProcesses() { + return mPidsArray; + } + + public void startTrackingProcess(long pid, String name, long start) { + synchronized (mPids) { + if (mPids.contains(pid)) return; + + mPids.add(pid); + updatePidsArrayL(); + + mData.put(pid, new ProcessMemInfo(pid, name, start)); + } + } + + private void updatePidsArrayL() { + final int N = mPids.size(); + mPidsArray = new int[N]; + StringBuffer sb = new StringBuffer("Now tracking processes: "); + for (int i = 0; i < N; i++) { + final int p = mPids.get(i).intValue(); + mPidsArray[i] = p; + sb.append(p); + sb.append(" "); + } + Log.v(TAG, sb.toString()); + } + + private void update() { + synchronized (mPids) { + Debug.MemoryInfo[] dinfos = mAm.getProcessMemoryInfo(mPidsArray); + for (int i = 0; i < dinfos.length; i++) { + Debug.MemoryInfo dinfo = dinfos[i]; + if (i > mPids.size()) { + Log.e(TAG, "update: unknown process info received: " + dinfo); + break; + } + final long pid = mPids.get(i).intValue(); + final ProcessMemInfo info = mData.get(pid); + info.head = (info.head + 1) % info.pss.length; + info.pss[info.head] = info.currentPss = dinfo.getTotalPss(); + info.uss[info.head] = info.currentUss = dinfo.getTotalPrivateDirty(); + if (info.currentPss > info.max) info.max = info.currentPss; + if (info.currentUss > info.max) info.max = info.currentUss; + if (info.currentPss == 0) { + Log.v(TAG, "update: pid " + pid + " has pss=0, it probably died"); + mData.remove(pid); + } + } + for (int i = mPids.size() - 1; i >= 0; i--) { + final long pid = mPids.get(i).intValue(); + if (mData.get(pid) == null) { + mPids.remove(i); + updatePidsArrayL(); + } + } + } + if (mQSTile != null) mQSTile.update(); + } + + private void setTile(MemoryTile tile) { + mQSTile = tile; + if (tile != null) tile.update(); + } + + private static String formatBytes(long b) { + String[] SUFFIXES = {"B", "K", "M", "G", "T"}; + int i; + for (i = 0; i < SUFFIXES.length; i++) { + if (b < 1024) break; + b /= 1024; + } + return b + SUFFIXES[i]; + } + + private void dumpHprofAndShare() { + final Intent share = mDumpTruck.captureHeaps(getTrackedProcesses()).createShareIntent(); + mContext.startActivity(share); + } + + private static class MemoryIconDrawable extends Drawable { + long pss, limit; + final Drawable baseIcon; + final Paint paint = new Paint(); + final float dp; + + MemoryIconDrawable(Context context) { + baseIcon = context.getDrawable(R.drawable.ic_memory).mutate(); + dp = context.getResources().getDisplayMetrics().density; + paint.setColor(QSTileImpl.getColorForState(context, Tile.STATE_ACTIVE)); + } + + public void setPss(long pss) { + if (pss != this.pss) { + this.pss = pss; + invalidateSelf(); + } + } + + public void setLimit(long limit) { + if (limit != this.limit) { + this.limit = limit; + invalidateSelf(); + } + } + + @Override + public void draw(Canvas canvas) { + baseIcon.draw(canvas); + + if (limit > 0 && pss > 0) { + float frac = Math.min(1f, (float) pss / limit); + + final Rect bounds = getBounds(); + canvas.translate(bounds.left + 8 * dp, bounds.top + 5 * dp); + //android:pathData="M16.0,5.0l-8.0,0.0l0.0,14.0l8.0,0.0z" + canvas.drawRect(0, 14 * dp * (1 - frac), 8 * dp + 1, 14 * dp + 1, paint); + } + } + + @Override + public void setBounds(int left, int top, int right, int bottom) { + super.setBounds(left, top, right, bottom); + baseIcon.setBounds(left, top, right, bottom); + } + + @Override + public int getIntrinsicHeight() { + return baseIcon.getIntrinsicHeight(); + } + + @Override + public int getIntrinsicWidth() { + return baseIcon.getIntrinsicWidth(); + } + + @Override + public void setAlpha(int i) { + baseIcon.setAlpha(i); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + baseIcon.setColorFilter(colorFilter); + paint.setColorFilter(colorFilter); + } + + @Override + public void setTint(int tint) { + super.setTint(tint); + baseIcon.setTint(tint); + } + + @Override + public void setTintList(ColorStateList tint) { + super.setTintList(tint); + baseIcon.setTintList(tint); + } + + @Override + public void setTintMode(PorterDuff.Mode tintMode) { + super.setTintMode(tintMode); + baseIcon.setTintMode(tintMode); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + } + + private static class MemoryGraphIcon extends QSTile.Icon { + long pss, limit; + + public void setPss(long pss) { + this.pss = pss; + } + + public void setHeapLimit(long limit) { + this.limit = limit; + } + + @Override + public Drawable getDrawable(Context context) { + final MemoryIconDrawable drawable = new MemoryIconDrawable(context); + drawable.setPss(pss); + drawable.setLimit(limit); + return drawable; + } + } + + public static class MemoryTile extends QSTileImpl<QSTile.State> { + public static final String TILE_SPEC = "dbg:mem"; + + private final GarbageMonitor gm; + private ProcessMemInfo pmi; + + public MemoryTile(QSHost host) { + super(host); + gm = Dependency.get(GarbageMonitor.class); + } + + @Override + public State newTileState() { + return new QSTile.State(); + } + + @Override + public Intent getLongClickIntent() { + return new Intent(); + } + + @Override + protected void handleClick() { + getHost().collapsePanels(); + mHandler.post(gm::dumpHprofAndShare); + } + + @Override + public int getMetricsCategory() { + return VIEW_UNKNOWN; + } + + @Override + public void handleSetListening(boolean listening) { + if (gm != null) gm.setTile(listening ? this : null); - // TODO(b/35345376): Turn this back on for debuggable builds after known leak fixed. - private static final boolean ENABLED = Build.IS_DEBUGGABLE - && SystemProperties.getBoolean("debug.enable_leak_reporting", false); - private static final String FORCE_ENABLE = "sysui_force_garbage_monitor"; + final ActivityManager am = mContext.getSystemService(ActivityManager.class); + if (listening && gm.mHeapLimit > 0) { + am.setWatchHeapLimit(1024 * gm.mHeapLimit); // why is this in bytes? + } else { + am.clearWatchHeapLimit(); + } + } + + @Override + public CharSequence getTileLabel() { + return getState().label; + } + + @Override + protected void handleUpdateState(State state, Object arg) { + pmi = gm.getMemInfo(Process.myPid()); + final MemoryGraphIcon icon = new MemoryGraphIcon(); + icon.setHeapLimit(gm.mHeapLimit); + if (pmi != null) { + icon.setPss(pmi.currentPss); + state.label = mContext.getString(R.string.heap_dump_tile_name); + state.secondaryLabel = + String.format( + "pss: %s / %s", + formatBytes(pmi.currentPss * 1024), + formatBytes(gm.mHeapLimit * 1024)); + } else { + icon.setPss(0); + state.label = "Dump SysUI"; + state.secondaryLabel = null; + } + state.icon = icon; + } + + public void update() { + refreshState(); + } + + public long getPss() { + return pmi != null ? pmi.currentPss : 0; + } + + public long getHeapLimit() { + return gm != null ? gm.mHeapLimit : 0; + } + } + + public static class ProcessMemInfo { + public long pid; + public String name; + public long startTime; + public long currentPss, currentUss; + public long[] pss = new long[256]; + public long[] uss = new long[256]; + public long max = 1; + public int head = 0; + + public ProcessMemInfo(long pid, String name, long start) { + this.pid = pid; + this.name = name; + this.startTime = start; + } + public long getUptime() { + return System.currentTimeMillis() - startTime; + } + } + + public static class Service extends SystemUI { private GarbageMonitor mGarbageMonitor; @Override public void start() { - boolean forceEnable = Settings.Secure.getInt(mContext.getContentResolver(), - FORCE_ENABLE, 0) != 0; - if (!ENABLED && !forceEnable) { - return; - } + boolean forceEnable = + Settings.Secure.getInt( + mContext.getContentResolver(), FORCE_ENABLE_LEAK_REPORTING, 0) + != 0; mGarbageMonitor = Dependency.get(GarbageMonitor.class); - mGarbageMonitor.start(); + if (LEAK_REPORTING_ENABLED || forceEnable) { + mGarbageMonitor.startLeakMonitor(); + } + if (HEAP_TRACKING_ENABLED || forceEnable) { + mGarbageMonitor.startHeapTracking(); + } + } + } + + private class BackgroundHeapCheckHandler extends Handler { + BackgroundHeapCheckHandler(Looper onLooper) { + super(onLooper); + if (Looper.getMainLooper().equals(onLooper)) { + throw new RuntimeException( + "BackgroundHeapCheckHandler may not run on the ui thread"); + } + } + + @Override + public void handleMessage(Message m) { + switch (m.what) { + case DO_GARBAGE_INSPECTION: + if (gcAndCheckGarbage()) { + postDelayed(GarbageMonitor.this::reinspectGarbageAfterGc, 100); + } + + removeMessages(DO_GARBAGE_INSPECTION); + sendEmptyMessageDelayed(DO_GARBAGE_INSPECTION, GARBAGE_INSPECTION_INTERVAL); + break; + + case DO_HEAP_TRACK: + update(); + removeMessages(DO_HEAP_TRACK); + sendEmptyMessageDelayed(DO_HEAP_TRACK, HEAP_TRACK_INTERVAL); + break; + } } } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java index a3b258fb2ea2..c095472e0c62 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/util/leak/GarbageMonitorTest.java @@ -16,16 +16,18 @@ package com.android.systemui.util.leak; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import android.content.Context; +import android.os.Looper; import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; import com.android.systemui.SysuiTestCase; @@ -34,7 +36,8 @@ import org.junit.Test; import org.junit.runner.RunWith; @SmallTest -@RunWith(AndroidJUnit4.class) +@RunWith(AndroidTestingRunner.class) +@RunWithLooper public class GarbageMonitorTest extends SysuiTestCase { private LeakReporter mLeakReporter; @@ -45,14 +48,17 @@ public class GarbageMonitorTest extends SysuiTestCase { public void setup() { mTrackedGarbage = mock(TrackedGarbage.class); mLeakReporter = mock(LeakReporter.class); - mGarbageMonitor = new TestableGarbageMonitor( - new LeakDetector(null, mTrackedGarbage, null), - mLeakReporter); + mGarbageMonitor = + new TestableGarbageMonitor( + mContext, + TestableLooper.get(this).getLooper(), + new LeakDetector(null, mTrackedGarbage, null), + mLeakReporter); } @Test public void testCallbacks_getScheduled() { - mGarbageMonitor.start(); + mGarbageMonitor.startLeakMonitor(); mGarbageMonitor.runCallbacksOnce(); mGarbageMonitor.runCallbacksOnce(); mGarbageMonitor.runCallbacksOnce(); @@ -62,7 +68,7 @@ public class GarbageMonitorTest extends SysuiTestCase { public void testNoGarbage_doesntDump() { when(mTrackedGarbage.countOldGarbage()).thenReturn(0); - mGarbageMonitor.start(); + mGarbageMonitor.startLeakMonitor(); mGarbageMonitor.runCallbacksOnce(); mGarbageMonitor.runCallbacksOnce(); mGarbageMonitor.runCallbacksOnce(); @@ -74,7 +80,7 @@ public class GarbageMonitorTest extends SysuiTestCase { public void testALittleGarbage_doesntDump() { when(mTrackedGarbage.countOldGarbage()).thenReturn(4); - mGarbageMonitor.start(); + mGarbageMonitor.startLeakMonitor(); mGarbageMonitor.runCallbacksOnce(); mGarbageMonitor.runCallbacksOnce(); mGarbageMonitor.runCallbacksOnce(); @@ -86,7 +92,7 @@ public class GarbageMonitorTest extends SysuiTestCase { public void testTransientGarbage_doesntDump() { when(mTrackedGarbage.countOldGarbage()).thenReturn(100); - mGarbageMonitor.start(); + mGarbageMonitor.startLeakMonitor(); mGarbageMonitor.runInspectCallback(); when(mTrackedGarbage.countOldGarbage()).thenReturn(0); @@ -100,56 +106,34 @@ public class GarbageMonitorTest extends SysuiTestCase { public void testLotsOfPersistentGarbage_dumps() { when(mTrackedGarbage.countOldGarbage()).thenReturn(100); - mGarbageMonitor.start(); + mGarbageMonitor.startLeakMonitor(); mGarbageMonitor.runCallbacksOnce(); verify(mLeakReporter).dumpLeak(anyInt()); } private static class TestableGarbageMonitor extends GarbageMonitor { - Runnable mInspectCallback; - Runnable mReinspectCallback; - - public TestableGarbageMonitor(LeakDetector leakDetector, + public TestableGarbageMonitor( + Context context, + Looper looper, + LeakDetector leakDetector, LeakReporter leakReporter) { - super(null /* bgLooper */, leakDetector, leakReporter); - } - - @Override - void scheduleInspectGarbage(Runnable runnable) { - assertNull("must not have more than one pending inspect callback", mInspectCallback); - mInspectCallback = runnable; + super(context, looper, leakDetector, leakReporter); } void runInspectCallback() { - assertNotNull("expected an inspect callback to be scheduled", mInspectCallback); - Runnable callback = mInspectCallback; - mInspectCallback = null; - callback.run(); - } - - @Override - void scheduleReinspectGarbage(Runnable runnable) { - assertNull("must not have more than one reinspect callback", mReinspectCallback); - mReinspectCallback = runnable; + startLeakMonitor(); } void runReinspectCallback() { - assertNotNull("expected a reinspect callback to be scheduled", mInspectCallback); - maybeRunReinspectCallback(); - } - - void maybeRunReinspectCallback() { - Runnable callback = mReinspectCallback; - mReinspectCallback = null; - if (callback != null) { - callback.run(); - } + reinspectGarbageAfterGc(); } void runCallbacksOnce() { + // Note that TestableLooper doesn't currently support delayed messages so we need to run + // callbacks explicitly. runInspectCallback(); - maybeRunReinspectCallback(); + runReinspectCallback(); } } }
\ No newline at end of file |