SysUiLeaks: Add GarbageMonitor
Adds a service for monitoring the amount of tracked garbage.
If it exceeds reasonable levels, a notification with a leak
report is posted.
Test: runtest systemui
Change-Id: Ib55281f2aac557743b97c46bc616688261c72e9c
diff --git a/packages/SystemUI/src/com/android/systemui/Dependency.java b/packages/SystemUI/src/com/android/systemui/Dependency.java
index 8d46b43..273b5e3 100644
--- a/packages/SystemUI/src/com/android/systemui/Dependency.java
+++ b/packages/SystemUI/src/com/android/systemui/Dependency.java
@@ -67,7 +67,9 @@
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.statusbar.policy.ZenModeControllerImpl;
import com.android.systemui.tuner.TunerService;
+import com.android.systemui.util.leak.GarbageMonitor;
import com.android.systemui.util.leak.LeakDetector;
+import com.android.systemui.util.leak.LeakReporter;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -104,6 +106,12 @@
*/
public static final DependencyKey<Handler> MAIN_HANDLER = new DependencyKey<>("main_handler");
+ /**
+ * An email address to send memory leak reports to by default.
+ */
+ public static final DependencyKey<String> LEAK_REPORT_EMAIL
+ = new DependencyKey<>("leak_report_email");
+
private final ArrayMap<Object, Object> mDependencies = new ArrayMap<>();
private final ArrayMap<Object, DependencyProvider> mProviders = new ArrayMap<>();
@@ -192,6 +200,18 @@
mProviders.put(LeakDetector.class, LeakDetector::create);
+ mProviders.put(LEAK_REPORT_EMAIL, () -> null);
+
+ mProviders.put(LeakReporter.class, () -> new LeakReporter(
+ mContext,
+ getDependency(LeakDetector.class),
+ getDependency(LEAK_REPORT_EMAIL)));
+
+ mProviders.put(GarbageMonitor.class, () -> new GarbageMonitor(
+ getDependency(BG_LOOPER),
+ getDependency(LeakDetector.class),
+ getDependency(LeakReporter.class)));
+
mProviders.put(TunerService.class, () ->
new TunerService(mContext));
diff --git a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
index e5bda7e..187b557 100644
--- a/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
+++ b/packages/SystemUI/src/com/android/systemui/SystemUIApplication.java
@@ -48,6 +48,7 @@
import com.android.systemui.tuner.TunerService;
import com.android.systemui.usb.StorageNotification;
import com.android.systemui.util.NotificationChannels;
+import com.android.systemui.util.leak.GarbageMonitor;
import com.android.systemui.volume.VolumeUI;
import java.util.HashMap;
@@ -81,6 +82,7 @@
PipUI.class,
ShortcutKeyDispatcher.class,
VendorServices.class,
+ GarbageMonitor.Service.class,
LatencyTester.class,
};
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
new file mode 100644
index 0000000..ef743e3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/GarbageMonitor.java
@@ -0,0 +1,94 @@
+/*
+ * 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.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.VisibleForTesting;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.SystemUI;
+
+public class GarbageMonitor {
+
+ private static final String TAG = "GarbageMonitor";
+
+ private static final long GARBAGE_INSPECTION_INTERVAL = 5 * 60 * 1000; // 5min
+ private static final int GARBAGE_ALLOWANCE = 5;
+
+ private final Handler mHandler;
+ private final TrackedGarbage mTrackedGarbage;
+ private final LeakReporter mLeakReporter;
+
+ public GarbageMonitor(Looper bgLooper, LeakDetector leakDetector,
+ LeakReporter leakReporter) {
+ mHandler = bgLooper != null ? new Handler(bgLooper): null;
+ mTrackedGarbage = leakDetector.getTrackedGarbage();
+ mLeakReporter = leakReporter;
+ }
+
+ public void start() {
+ if (mTrackedGarbage == null) {
+ return;
+ }
+
+ scheduleInspectGarbage(this::inspectGarbage);
+ }
+
+ @VisibleForTesting
+ void scheduleInspectGarbage(Runnable runnable) {
+ mHandler.postDelayed(runnable, GARBAGE_INSPECTION_INTERVAL);
+ }
+
+ private void inspectGarbage() {
+ if (mTrackedGarbage.countOldGarbage() > GARBAGE_ALLOWANCE) {
+ Runtime.getRuntime().gc();
+
+ // Allow some time to for ReferenceQueue to catch up.
+ scheduleReinspectGarbage(this::reinspectGarbageAfterGc);
+ }
+ scheduleInspectGarbage(this::inspectGarbage);
+ }
+
+ @VisibleForTesting
+ void scheduleReinspectGarbage(Runnable runnable) {
+ mHandler.postDelayed(runnable, (long) 100);
+ }
+
+ private void reinspectGarbageAfterGc() {
+ int count = mTrackedGarbage.countOldGarbage();
+ if (count > GARBAGE_ALLOWANCE) {
+ mLeakReporter.dumpLeak(count);
+ }
+ }
+
+ public static class Service extends SystemUI {
+
+ private GarbageMonitor mGarbageMonitor;
+
+ @Override
+ public void start() {
+ if (!Build.IS_DEBUGGABLE) {
+ return;
+ }
+ mGarbageMonitor = Dependency.get(GarbageMonitor.class);
+ mGarbageMonitor.start();
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
index a0f8659..574fdb98 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakDetector.java
@@ -96,6 +96,10 @@
}
}
+ TrackedGarbage getTrackedGarbage() {
+ return mTrackedGarbage;
+ }
+
@Override
public void dump(FileDescriptor df, PrintWriter w, String[] args) {
IndentingPrintWriter pw = new IndentingPrintWriter(w, " ");
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
new file mode 100644
index 0000000..a247f3f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/LeakReporter.java
@@ -0,0 +1,142 @@
+/*
+ * 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.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Debug;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.support.v4.content.FileProvider;
+import android.util.Log;
+
+import com.google.android.collect.Lists;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Dumps data to debug leaks and posts a notification to share the data.
+ */
+public class LeakReporter {
+
+ static final String TAG = "LeakReporter";
+
+ public static final String FILEPROVIDER_AUTHORITY = "com.android.systemui.fileprovider";
+
+ static final String LEAK_DIR = "leak";
+ static final String LEAK_HPROF = "leak.hprof";
+ static final String LEAK_DUMP = "leak.dump";
+
+ private final Context mContext;
+ private final LeakDetector mLeakDetector;
+ private final String mLeakReportEmail;
+
+ public LeakReporter(Context context, LeakDetector leakDetector, String leakReportEmail) {
+ mContext = context;
+ mLeakDetector = leakDetector;
+ mLeakReportEmail = leakReportEmail;
+ }
+
+ public void dumpLeak(int garbageCount) {
+ try {
+ File leakDir = new File(mContext.getCacheDir(), LEAK_DIR);
+ leakDir.mkdir();
+
+ File hprofFile = new File(leakDir, LEAK_HPROF);
+ Debug.dumpHprofData(hprofFile.getAbsolutePath());
+
+ File dumpFile = new File(leakDir, LEAK_DUMP);
+ try (FileOutputStream fos = new FileOutputStream(dumpFile)) {
+ PrintWriter w = new PrintWriter(fos);
+ w.print("Build: "); w.println(SystemProperties.get("ro.build.description"));
+ w.println();
+ w.flush();
+ mLeakDetector.dump(fos.getFD(), w, new String[0]);
+ w.close();
+ }
+
+ NotificationManager notiMan = mContext.getSystemService(NotificationManager.class);
+
+ NotificationChannel channel = new NotificationChannel("leak", "Leak Alerts",
+ NotificationManager.IMPORTANCE_HIGH);
+ channel.enableVibration(true);
+
+ notiMan.createNotificationChannel(channel);
+ Notification.Builder builder = new Notification.Builder(mContext, channel.getId())
+ .setAutoCancel(true)
+ .setShowWhen(true)
+ .setContentTitle("Memory Leak Detected")
+ .setContentText(String.format(
+ "SystemUI has detected %d leaked objects. Tap to send", garbageCount))
+ .setSmallIcon(com.android.internal.R.drawable.stat_sys_adb)
+ .setContentIntent(PendingIntent.getActivityAsUser(mContext, 0,
+ getIntent(hprofFile, dumpFile),
+ PendingIntent.FLAG_UPDATE_CURRENT, null, UserHandle.CURRENT));
+ notiMan.notify(TAG, 0, builder.build());
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't dump heap for leak", e);
+ }
+ }
+
+ private Intent getIntent(File hprofFile, File dumpFile) {
+ Uri dumpUri = FileProvider.getUriForFile(mContext, FILEPROVIDER_AUTHORITY, dumpFile);
+ Uri hprofUri = FileProvider.getUriForFile(mContext, FILEPROVIDER_AUTHORITY, hprofFile);
+
+ Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
+ String mimeType = "application/vnd.android.leakreport";
+
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ intent.setType(mimeType);
+
+ final String subject = "SystemUI leak report";
+ intent.putExtra(Intent.EXTRA_SUBJECT, subject);
+
+ // EXTRA_TEXT should be an ArrayList, but some clients are expecting a single String.
+ // So, to avoid an exception on Intent.migrateExtraStreamToClipData(), we need to manually
+ // create the ClipData object with the attachments URIs.
+ final StringBuilder messageBody = new StringBuilder("Build info: ")
+ .append(SystemProperties.get("ro.build.description"));
+ intent.putExtra(Intent.EXTRA_TEXT, messageBody.toString());
+ final ClipData clipData = new ClipData(null, new String[] { mimeType },
+ new ClipData.Item(null, null, null, dumpUri));
+ final ArrayList<Uri> attachments = Lists.newArrayList(dumpUri);
+
+ clipData.addItem(new ClipData.Item(null, null, null, hprofUri));
+ attachments.add(hprofUri);
+
+ intent.setClipData(clipData);
+ intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, attachments);
+
+ String leakReportEmail = mLeakReportEmail;
+ if (leakReportEmail != null) {
+ intent.putExtra(Intent.EXTRA_EMAIL, new String[] { leakReportEmail });
+ }
+
+ return intent;
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java
index fd59aee..98a6450 100644
--- a/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java
+++ b/packages/SystemUI/src/com/android/systemui/util/leak/TrackedGarbage.java
@@ -106,6 +106,20 @@
}
}
+ public synchronized int countOldGarbage() {
+ cleanUp();
+
+ long now = SystemClock.uptimeMillis();
+
+ int result = 0;
+ for (LeakReference ref : mGarbage) {
+ if (isOld(ref.createdUptimeMillis, now)) {
+ result++;
+ }
+ }
+ return result;
+ }
+
private boolean isOld(long createdUptimeMillis, long now) {
return createdUptimeMillis + GARBAGE_COLLECTION_DEADLINE_MILLIS < now;
}