From 9907eb6cd0e520231d8730a5450c8ca8d3af54bb Mon Sep 17 00:00:00 2001 From: Jernej Virag Date: Fri, 2 Dec 2022 18:15:24 +0100 Subject: [PATCH] Implement pull logging for Notification Memory This implements a pull StatsD logger which will aggregate and log current memory consumption of displayed notifications. Bug: 235451049 Test: statsd_testdrive 10173 Change-Id: I671c8819c25795d2a18639808830802af3bbf1eb --- .../logging/NotificationMemory.kt | 7 +- .../logging/NotificationMemoryDumper.kt | 173 ++++++++++++++++ .../logging/NotificationMemoryLogger.kt | 194 ++++++++++++++++++ .../logging/NotificationMemoryMeter.kt | 45 +++- .../logging/NotificationMemoryMonitor.kt | 139 +------------ .../logging/NotificationMemoryViewWalker.kt | 84 +++++--- .../logging/NotificationMemoryLoggerTest.kt | 127 ++++++++++++ .../logging/NotificationMemoryMeterTest.kt | 47 +++-- .../NotificationMemoryViewWalkerTest.kt | 113 +++++++++- 9 files changed, 746 insertions(+), 183 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt index 0380fff1e2afb..1fcf17fe7553e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemory.kt @@ -17,10 +17,14 @@ package com.android.systemui.statusbar.notification.logging +import android.app.Notification + /** Describes usage of a notification. */ data class NotificationMemoryUsage( val packageName: String, + val uid: Int, val notificationKey: String, + val notification: Notification, val objectUsage: NotificationObjectUsage, val viewUsage: List ) @@ -34,7 +38,8 @@ data class NotificationObjectUsage( val smallIcon: Int, val largeIcon: Int, val extras: Int, - val style: String?, + /** Style type, integer from [android.stats.sysui.NotificationEnums] */ + val style: Int, val styleIcon: Int, val bigPicture: Int, val extender: Int, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt new file mode 100644 index 0000000000000..ffd931c1bde29 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryDumper.kt @@ -0,0 +1,173 @@ +/* + * + * Copyright (C) 2022 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.statusbar.notification.logging + +import android.stats.sysui.NotificationEnums +import com.android.systemui.Dumpable +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import java.io.PrintWriter +import javax.inject.Inject + +/** Dumps current notification memory use to bug reports for easier debugging. */ +@SysUISingleton +class NotificationMemoryDumper +@Inject +constructor(val dumpManager: DumpManager, val notificationPipeline: NotifPipeline) : Dumpable { + + fun init() { + dumpManager.registerNormalDumpable(javaClass.simpleName, this) + } + + override fun dump(pw: PrintWriter, args: Array) { + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs) + .sortedWith(compareBy({ it.packageName }, { it.notificationKey })) + dumpNotificationObjects(pw, memoryUse) + dumpNotificationViewUsage(pw, memoryUse) + } + + /** Renders a table of notification object usage into passed [PrintWriter]. */ + private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List) { + pw.println("Notification Object Usage") + pw.println("-----------") + pw.println( + "Package".padEnd(35) + + "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom" + ) + pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView") + pw.println() + + memoryUse.forEach { use -> + pw.println( + use.packageName.padEnd(35) + + "\t\t" + + "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" + + (styleEnumToString(use.objectUsage.style).take(15) ?: "").padEnd(15) + + "\t\t${use.objectUsage.styleIcon}\t" + + "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" + + "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" + + use.notificationKey + ) + } + + // Calculate totals for easily glanceable summary. + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var styleIcon: Int = 0, + var bigPicture: Int = 0, + var extender: Int = 0, + var extras: Int = 0, + ) + + val totals = + memoryUse.fold(Totals()) { t, usage -> + t.smallIcon += usage.objectUsage.smallIcon + t.largeIcon += usage.objectUsage.largeIcon + t.styleIcon += usage.objectUsage.styleIcon + t.bigPicture += usage.objectUsage.bigPicture + t.extender += usage.objectUsage.extender + t.extras += usage.objectUsage.extras + t + } + + pw.println() + pw.println("TOTALS") + pw.println( + "".padEnd(35) + + "\t\t" + + "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" + + "".padEnd(15) + + "\t\t${toKb(totals.styleIcon)}\t" + + "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" + + toKb(totals.extras) + ) + pw.println() + } + + /** Renders a table of notification view usage into passed [PrintWriter] */ + private fun dumpNotificationViewUsage( + pw: PrintWriter, + memoryUse: List, + ) { + + data class Totals( + var smallIcon: Int = 0, + var largeIcon: Int = 0, + var style: Int = 0, + var customViews: Int = 0, + var softwareBitmapsPenalty: Int = 0, + ) + + val totals = Totals() + pw.println("Notification View Usage") + pw.println("-----------") + pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware") + pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps") + pw.println() + memoryUse + .filter { it.viewUsage.isNotEmpty() } + .forEach { use -> + pw.println(use.packageName + " " + use.notificationKey) + use.viewUsage.forEach { view -> + pw.println( + " ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" + + "\t${view.largeIcon}\t${view.style}" + + "\t${view.customViews}\t${view.softwareBitmapsPenalty}" + ) + + if (view.viewType == ViewType.TOTAL) { + totals.smallIcon += view.smallIcon + totals.largeIcon += view.largeIcon + totals.style += view.style + totals.customViews += view.customViews + totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty + } + } + } + pw.println() + pw.println("TOTALS") + pw.println( + " ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" + + "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" + + "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}" + ) + pw.println() + } + + private fun styleEnumToString(styleEnum: Int): String = + when (styleEnum) { + NotificationEnums.STYLE_UNSPECIFIED -> "Unspecified" + NotificationEnums.STYLE_NONE -> "None" + NotificationEnums.STYLE_BIG_PICTURE -> "BigPicture" + NotificationEnums.STYLE_BIG_TEXT -> "BigText" + NotificationEnums.STYLE_CALL -> "Call" + NotificationEnums.STYLE_DECORATED_CUSTOM_VIEW -> "DCustomView" + NotificationEnums.STYLE_INBOX -> "Inbox" + NotificationEnums.STYLE_MEDIA -> "Media" + NotificationEnums.STYLE_MESSAGING -> "Messaging" + NotificationEnums.STYLE_RANKER_GROUP -> "RankerGroup" + else -> "Unknown" + } + + private fun toKb(bytes: Int): String { + return (bytes / 1024).toString() + " KB" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt new file mode 100644 index 0000000000000..ec8501a79fa5c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLogger.kt @@ -0,0 +1,194 @@ +/* + * + * Copyright (C) 2022 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.statusbar.notification.logging + +import android.app.StatsManager +import android.util.StatsEvent +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.util.traceSection +import java.util.concurrent.Executor +import javax.inject.Inject +import kotlin.math.roundToInt +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.runBlocking + +/** Periodically logs current state of notification memory consumption. */ +@SysUISingleton +class NotificationMemoryLogger +@Inject +constructor( + private val notificationPipeline: NotifPipeline, + private val statsManager: StatsManager, + @Main private val mainDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor +) : StatsManager.StatsPullAtomCallback { + + /** + * This class is used to accumulate and aggregate data - the fields mirror values in statd Atom + * with ONE IMPORTANT difference - the values are in bytes, not KB! + */ + internal data class NotificationMemoryUseAtomBuilder(val uid: Int, val style: Int) { + var count: Int = 0 + var countWithInflatedViews: Int = 0 + var smallIconObject: Int = 0 + var smallIconBitmapCount: Int = 0 + var largeIconObject: Int = 0 + var largeIconBitmapCount: Int = 0 + var bigPictureObject: Int = 0 + var bigPictureBitmapCount: Int = 0 + var extras: Int = 0 + var extenders: Int = 0 + var smallIconViews: Int = 0 + var largeIconViews: Int = 0 + var systemIconViews: Int = 0 + var styleViews: Int = 0 + var customViews: Int = 0 + var softwareBitmaps: Int = 0 + var seenCount = 0 + } + + fun init() { + statsManager.setPullAtomCallback( + SysUiStatsLog.NOTIFICATION_MEMORY_USE, + null, + backgroundExecutor, + this + ) + } + + /** Called by statsd to pull data. */ + override fun onPullAtom(atomTag: Int, data: MutableList): Int = + traceSection("NML#onPullAtom") { + if (atomTag != SysUiStatsLog.NOTIFICATION_MEMORY_USE) { + return StatsManager.PULL_SKIP + } + + // Notifications can only be retrieved on the main thread, so switch to that thread. + val notifications = getAllNotificationsOnMainThread() + val notificationMemoryUse = + NotificationMemoryMeter.notificationMemoryUse(notifications) + .sortedWith( + compareBy( + { it.packageName }, + { it.objectUsage.style }, + { it.notificationKey } + ) + ) + val usageData = aggregateMemoryUsageData(notificationMemoryUse) + usageData.forEach { (_, use) -> + data.add( + SysUiStatsLog.buildStatsEvent( + SysUiStatsLog.NOTIFICATION_MEMORY_USE, + use.uid, + use.style, + use.count, + use.countWithInflatedViews, + toKb(use.smallIconObject), + use.smallIconBitmapCount, + toKb(use.largeIconObject), + use.largeIconBitmapCount, + toKb(use.bigPictureObject), + use.bigPictureBitmapCount, + toKb(use.extras), + toKb(use.extenders), + toKb(use.smallIconViews), + toKb(use.largeIconViews), + toKb(use.systemIconViews), + toKb(use.styleViews), + toKb(use.customViews), + toKb(use.softwareBitmaps), + use.seenCount + ) + ) + } + + return StatsManager.PULL_SUCCESS + } + + private fun getAllNotificationsOnMainThread() = + runBlocking(mainDispatcher) { + traceSection("NML#getNotifications") { notificationPipeline.allNotifs } + } + + /** Aggregates memory usage data by package and style, returning sums. */ + private fun aggregateMemoryUsageData( + notificationMemoryUse: List + ): Map, NotificationMemoryUseAtomBuilder> { + return notificationMemoryUse + .groupingBy { Pair(it.packageName, it.objectUsage.style) } + .aggregate { + _, + accumulator: NotificationMemoryUseAtomBuilder?, + element: NotificationMemoryUsage, + first -> + val use = + if (first) { + NotificationMemoryUseAtomBuilder(element.uid, element.objectUsage.style) + } else { + accumulator!! + } + + use.count++ + // If the views of the notification weren't inflated, the list of memory usage + // parameters will be empty. + if (element.viewUsage.isNotEmpty()) { + use.countWithInflatedViews++ + } + + use.smallIconObject += element.objectUsage.smallIcon + if (element.objectUsage.smallIcon > 0) { + use.smallIconBitmapCount++ + } + + use.largeIconObject += element.objectUsage.largeIcon + if (element.objectUsage.largeIcon > 0) { + use.largeIconBitmapCount++ + } + + use.bigPictureObject += element.objectUsage.bigPicture + if (element.objectUsage.bigPicture > 0) { + use.bigPictureBitmapCount++ + } + + use.extras += element.objectUsage.extras + use.extenders += element.objectUsage.extender + + // Use totals count which are more accurate when aggregated + // in this manner. + element.viewUsage + .firstOrNull { vu -> vu.viewType == ViewType.TOTAL } + ?.let { + use.smallIconViews += it.smallIcon + use.largeIconViews += it.largeIcon + use.systemIconViews += it.systemIcons + use.styleViews += it.style + use.customViews += it.style + use.softwareBitmaps += it.softwareBitmapsPenalty + } + + return@aggregate use + } + } + + /** Rounds the passed value to the nearest KB - e.g. 700B rounds to 1KB. */ + private fun toKb(value: Int): Int = (value.toFloat() / 1024f).roundToInt() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt index 7d39e18ab3492..41fb91e3093e7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeter.kt @@ -1,12 +1,20 @@ package com.android.systemui.statusbar.notification.logging import android.app.Notification +import android.app.Notification.BigPictureStyle +import android.app.Notification.BigTextStyle +import android.app.Notification.CallStyle +import android.app.Notification.DecoratedCustomViewStyle +import android.app.Notification.InboxStyle +import android.app.Notification.MediaStyle +import android.app.Notification.MessagingStyle import android.app.Person import android.graphics.Bitmap import android.graphics.drawable.Icon import android.os.Bundle import android.os.Parcel import android.os.Parcelable +import android.stats.sysui.NotificationEnums import androidx.annotation.WorkerThread import com.android.systemui.statusbar.notification.NotificationUtils import com.android.systemui.statusbar.notification.collection.NotificationEntry @@ -19,6 +27,7 @@ internal object NotificationMemoryMeter { private const val TV_EXTENSIONS = "android.tv.EXTENSIONS" private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" + private const val AUTOGROUP_KEY = "ranker_group" /** Returns a list of memory use entries for currently shown notifications. */ @WorkerThread @@ -29,12 +38,15 @@ internal object NotificationMemoryMeter { .asSequence() .map { entry -> val packageName = entry.sbn.packageName + val uid = entry.sbn.uid val notificationObjectUsage = notificationMemoryUse(entry.sbn.notification, hashSetOf()) val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row) NotificationMemoryUsage( packageName, + uid, NotificationUtils.logKey(entry.sbn.key), + entry.sbn.notification, notificationObjectUsage, notificationViewUsage ) @@ -49,7 +61,9 @@ internal object NotificationMemoryMeter { ): NotificationMemoryUsage { return NotificationMemoryUsage( entry.sbn.packageName, + entry.sbn.uid, NotificationUtils.logKey(entry.sbn.key), + entry.sbn.notification, notificationMemoryUse(entry.sbn.notification, seenBitmaps), NotificationMemoryViewWalker.getViewUsage(entry.row) ) @@ -116,7 +130,13 @@ internal object NotificationMemoryMeter { val wearExtenderBackground = computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) - val style = notification.notificationStyle + val style = + if (notification.group == AUTOGROUP_KEY) { + NotificationEnums.STYLE_RANKER_GROUP + } else { + styleEnum(notification.notificationStyle) + } + val hasCustomView = notification.contentView != null || notification.bigContentView != null val extrasSize = computeBundleSize(extras) @@ -124,7 +144,7 @@ internal object NotificationMemoryMeter { smallIcon = smallIconUse, largeIcon = largeIconUse, extras = extrasSize, - style = style?.simpleName, + style = style, styleIcon = bigPictureIconUse + peopleUse + @@ -143,6 +163,25 @@ internal object NotificationMemoryMeter { ) } + /** + * Returns logging style enum based on current style class. + * + * @return style value in [NotificationEnums] + */ + private fun styleEnum(style: Class?): Int = + when (style?.name) { + null -> NotificationEnums.STYLE_NONE + BigTextStyle::class.java.name -> NotificationEnums.STYLE_BIG_TEXT + BigPictureStyle::class.java.name -> NotificationEnums.STYLE_BIG_PICTURE + InboxStyle::class.java.name -> NotificationEnums.STYLE_INBOX + MediaStyle::class.java.name -> NotificationEnums.STYLE_MEDIA + DecoratedCustomViewStyle::class.java.name -> + NotificationEnums.STYLE_DECORATED_CUSTOM_VIEW + MessagingStyle::class.java.name -> NotificationEnums.STYLE_MESSAGING + CallStyle::class.java.name -> NotificationEnums.STYLE_CALL + else -> NotificationEnums.STYLE_UNSPECIFIED + } + /** * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem * bitmaps). Can be slow. @@ -176,7 +215,7 @@ internal object NotificationMemoryMeter { * * @return memory usage in bytes or 0 if the icon is Uri/Resource based */ - private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet) = + private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet): Int = when (icon?.type) { Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt index c09cc4306ceda..f38c1e557b255 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMonitor.kt @@ -18,11 +18,10 @@ package com.android.systemui.statusbar.notification.logging import android.util.Log -import com.android.systemui.Dumpable import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dump.DumpManager -import com.android.systemui.statusbar.notification.collection.NotifPipeline -import java.io.PrintWriter +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import dagger.Lazy import javax.inject.Inject /** This class monitors and logs current Notification memory use. */ @@ -30,9 +29,10 @@ import javax.inject.Inject class NotificationMemoryMonitor @Inject constructor( - val notificationPipeline: NotifPipeline, - val dumpManager: DumpManager, -) : Dumpable { + private val featureFlags: FeatureFlags, + private val notificationMemoryDumper: NotificationMemoryDumper, + private val notificationMemoryLogger: Lazy, +) { companion object { private const val TAG = "NotificationMemory" @@ -40,127 +40,10 @@ constructor( fun init() { Log.d(TAG, "NotificationMemoryMonitor initialized.") - dumpManager.registerDumpable(javaClass.simpleName, this) - } - - override fun dump(pw: PrintWriter, args: Array) { - val memoryUse = - NotificationMemoryMeter.notificationMemoryUse(notificationPipeline.allNotifs) - .sortedWith(compareBy({ it.packageName }, { it.notificationKey })) - dumpNotificationObjects(pw, memoryUse) - dumpNotificationViewUsage(pw, memoryUse) - } - - /** Renders a table of notification object usage into passed [PrintWriter]. */ - private fun dumpNotificationObjects(pw: PrintWriter, memoryUse: List) { - pw.println("Notification Object Usage") - pw.println("-----------") - pw.println( - "Package".padEnd(35) + - "\t\tSmall\tLarge\t${"Style".padEnd(15)}\t\tStyle\tBig\tExtend.\tExtras\tCustom" - ) - pw.println("".padEnd(35) + "\t\tIcon\tIcon\t${"".padEnd(15)}\t\tIcon\tPicture\t \t \tView") - pw.println() - - memoryUse.forEach { use -> - pw.println( - use.packageName.padEnd(35) + - "\t\t" + - "${use.objectUsage.smallIcon}\t${use.objectUsage.largeIcon}\t" + - (use.objectUsage.style?.take(15) ?: "").padEnd(15) + - "\t\t${use.objectUsage.styleIcon}\t" + - "${use.objectUsage.bigPicture}\t${use.objectUsage.extender}\t" + - "${use.objectUsage.extras}\t${use.objectUsage.hasCustomView}\t" + - use.notificationKey - ) + notificationMemoryDumper.init() + if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_LOGGING_ENABLED)) { + Log.d(TAG, "Notification memory logging enabled.") + notificationMemoryLogger.get().init() } - - // Calculate totals for easily glanceable summary. - data class Totals( - var smallIcon: Int = 0, - var largeIcon: Int = 0, - var styleIcon: Int = 0, - var bigPicture: Int = 0, - var extender: Int = 0, - var extras: Int = 0, - ) - - val totals = - memoryUse.fold(Totals()) { t, usage -> - t.smallIcon += usage.objectUsage.smallIcon - t.largeIcon += usage.objectUsage.largeIcon - t.styleIcon += usage.objectUsage.styleIcon - t.bigPicture += usage.objectUsage.bigPicture - t.extender += usage.objectUsage.extender - t.extras += usage.objectUsage.extras - t - } - - pw.println() - pw.println("TOTALS") - pw.println( - "".padEnd(35) + - "\t\t" + - "${toKb(totals.smallIcon)}\t${toKb(totals.largeIcon)}\t" + - "".padEnd(15) + - "\t\t${toKb(totals.styleIcon)}\t" + - "${toKb(totals.bigPicture)}\t${toKb(totals.extender)}\t" + - toKb(totals.extras) - ) - pw.println() - } - - /** Renders a table of notification view usage into passed [PrintWriter] */ - private fun dumpNotificationViewUsage( - pw: PrintWriter, - memoryUse: List, - ) { - - data class Totals( - var smallIcon: Int = 0, - var largeIcon: Int = 0, - var style: Int = 0, - var customViews: Int = 0, - var softwareBitmapsPenalty: Int = 0, - ) - - val totals = Totals() - pw.println("Notification View Usage") - pw.println("-----------") - pw.println("View Type".padEnd(24) + "\tSmall\tLarge\tStyle\tCustom\tSoftware") - pw.println("".padEnd(24) + "\tIcon\tIcon\tUse\tView\tBitmaps") - pw.println() - memoryUse - .filter { it.viewUsage.isNotEmpty() } - .forEach { use -> - pw.println(use.packageName + " " + use.notificationKey) - use.viewUsage.forEach { view -> - pw.println( - " ${view.viewType.toString().padEnd(24)}\t${view.smallIcon}" + - "\t${view.largeIcon}\t${view.style}" + - "\t${view.customViews}\t${view.softwareBitmapsPenalty}" - ) - - if (view.viewType == ViewType.TOTAL) { - totals.smallIcon += view.smallIcon - totals.largeIcon += view.largeIcon - totals.style += view.style - totals.customViews += view.customViews - totals.softwareBitmapsPenalty += view.softwareBitmapsPenalty - } - } - } - pw.println() - pw.println("TOTALS") - pw.println( - " ${"".padEnd(24)}\t${toKb(totals.smallIcon)}" + - "\t${toKb(totals.largeIcon)}\t${toKb(totals.style)}" + - "\t${toKb(totals.customViews)}\t${toKb(totals.softwareBitmapsPenalty)}" - ) - pw.println() - } - - private fun toKb(bytes: Int): String { - return (bytes / 1024).toString() + " KB" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt index a0bee1502f517..2d042118b3d7e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalker.kt @@ -50,7 +50,11 @@ internal object NotificationMemoryViewWalker { /** * Returns memory usage of public and private views contained in passed - * [ExpandableNotificationRow] + * [ExpandableNotificationRow]. Each entry will correspond to one of the [ViewType] values with + * [ViewType.TOTAL] totalling all memory use. If a type of view is missing, the corresponding + * entry will not appear in resulting list. + * + * This will return an empty list if the ExpandableNotificationRow has no views inflated. */ fun getViewUsage(row: ExpandableNotificationRow?): List { if (row == null) { @@ -58,42 +62,72 @@ internal object NotificationMemoryViewWalker { } // The ordering here is significant since it determines deduplication of seen drawables. - return listOf( - getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), - getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild), - getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), - getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout), - getTotalUsage(row) - ) + val perViewUsages = + listOf( + getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), + getViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + row.privateLayout?.contractedChild + ), + getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), + getViewUsage( + ViewType.PUBLIC_VIEW, + row.publicLayout?.expandedChild, + row.publicLayout?.contractedChild, + row.publicLayout?.headsUpChild + ), + ) + .filterNotNull() + + return if (perViewUsages.isNotEmpty()) { + // Attach summed totals field only if there was any view actually measured. + // This reduces bug report noise and makes checks for collapsed views easier. + val totals = getTotalUsage(row) + if (totals == null) { + perViewUsages + } else { + perViewUsages + totals + } + } else { + listOf() + } } /** * Calculate total usage of all views - we need to do a separate traversal to make sure we don't * double count fields. */ - private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage { - val totalUsage = UsageBuilder() + private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage? { val seenObjects = hashSetOf() - - row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) } - row.privateLayout?.let { child -> - for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) { - (view as? ViewGroup)?.let { v -> - computeViewHierarchyUse(v, totalUsage, seenObjects) - } - } - } - return totalUsage.build(ViewType.TOTAL) + return getViewUsage( + ViewType.TOTAL, + row.privateLayout?.expandedChild, + row.privateLayout?.contractedChild, + row.privateLayout?.headsUpChild, + row.publicLayout?.expandedChild, + row.publicLayout?.contractedChild, + row.publicLayout?.headsUpChild, + seenObjects = seenObjects + ) } private fun getViewUsage( type: ViewType, - rootView: View?, + vararg rootViews: View?, seenObjects: HashSet = hashSetOf() - ): NotificationViewUsage { - val usageBuilder = UsageBuilder() - (rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) } - return usageBuilder.build(type) + ): NotificationViewUsage? { + val usageBuilder = lazy { UsageBuilder() } + rootViews.forEach { rootView -> + (rootView as? ViewGroup)?.let { rootViewGroup -> + computeViewHierarchyUse(rootViewGroup, usageBuilder.value, seenObjects) + } + } + + return if (usageBuilder.isInitialized()) { + usageBuilder.value.build(type) + } else { + null + } } private fun computeViewHierarchyUse( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt new file mode 100644 index 0000000000000..33b94e39c0196 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryLoggerTest.kt @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2022 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.statusbar.notification.logging + +import android.app.Notification +import android.app.StatsManager +import android.graphics.Bitmap +import android.graphics.drawable.Icon +import android.testing.AndroidTestingRunner +import android.util.StatsEvent +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.shared.system.SysUiStatsLog +import com.android.systemui.statusbar.notification.collection.NotifPipeline +import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.Dispatchers +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class NotificationMemoryLoggerTest : SysuiTestCase() { + + private val bgExecutor = FakeExecutor(FakeSystemClock()) + private val immediate = Dispatchers.Main.immediate + + @Mock private lateinit var statsManager: StatsManager + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun onInit_registersCallback() { + val logger = createLoggerWithNotifications(listOf()) + logger.init() + verify(statsManager) + .setPullAtomCallback(SysUiStatsLog.NOTIFICATION_MEMORY_USE, null, bgExecutor, logger) + } + + @Test + fun onPullAtom_wrongAtomId_returnsSkip() { + val logger = createLoggerWithNotifications(listOf()) + val data: MutableList = mutableListOf() + assertThat(logger.onPullAtom(111, data)).isEqualTo(StatsManager.PULL_SKIP) + assertThat(data).isEmpty() + } + + @Test + fun onPullAtom_emptyNotifications_returnsZeros() { + val logger = createLoggerWithNotifications(listOf()) + val data: MutableList = mutableListOf() + assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data)) + .isEqualTo(StatsManager.PULL_SUCCESS) + assertThat(data).isEmpty() + } + + @Test + fun onPullAtom_notificationPassed_populatesData() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)) + val notification = + Notification.Builder(context).setSmallIcon(icon).setContentTitle("title").build() + val logger = createLoggerWithNotifications(listOf(notification)) + val data: MutableList = mutableListOf() + + assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data)) + .isEqualTo(StatsManager.PULL_SUCCESS) + assertThat(data).hasSize(1) + } + + @Test + fun onPullAtom_multipleNotificationsPassed_populatesData() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)) + val notification = + Notification.Builder(context).setSmallIcon(icon).setContentTitle("title").build() + val iconTwo = Icon.createWithBitmap(Bitmap.createBitmap(300, 300, Bitmap.Config.ARGB_8888)) + + val notificationTwo = + Notification.Builder(context) + .setStyle(Notification.BigTextStyle().bigText("text")) + .setSmallIcon(iconTwo) + .setContentTitle("titleTwo") + .build() + val logger = createLoggerWithNotifications(listOf(notification, notificationTwo)) + val data: MutableList = mutableListOf() + + assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data)) + .isEqualTo(StatsManager.PULL_SUCCESS) + assertThat(data).hasSize(2) + } + + private fun createLoggerWithNotifications( + notifications: List + ): NotificationMemoryLogger { + val pipeline: NotifPipeline = mock() + val notifications = + notifications.map { notification -> + NotificationEntryBuilder().setTag("test").setNotification(notification).build() + } + whenever(pipeline.allNotifs).thenReturn(notifications) + return NotificationMemoryLogger(pipeline, statsManager, immediate, bgExecutor) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt index f69839b7087cf..072a497f1a65d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryMeterTest.kt @@ -23,6 +23,7 @@ import android.app.Person import android.content.Intent import android.graphics.Bitmap import android.graphics.drawable.Icon +import android.stats.sysui.NotificationEnums import android.testing.AndroidTestingRunner import android.widget.RemoteViews import androidx.test.filters.SmallTest @@ -50,7 +51,27 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3316, bigPicture = 0, extender = 0, - style = null, + style = NotificationEnums.STYLE_NONE, + styleIcon = 0, + hasCustomView = false, + ) + } + + @Test + fun currentNotificationMemoryUse_rankerGroupNotification() { + val notification = createBasicNotification().build() + val memoryUse = + NotificationMemoryMeter.notificationMemoryUse( + createNotificationEntry(createBasicNotification().setGroup("ranker_group").build()) + ) + assertNotificationObjectSizes( + memoryUse, + smallIcon = notification.smallIcon.bitmap.allocationByteCount, + largeIcon = notification.getLargeIcon().bitmap.allocationByteCount, + extras = 3316, + bigPicture = 0, + extender = 0, + style = NotificationEnums.STYLE_RANKER_GROUP, styleIcon = 0, hasCustomView = false, ) @@ -69,7 +90,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3316, bigPicture = 0, extender = 0, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = false, ) @@ -92,7 +113,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3384, bigPicture = 0, extender = 0, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = true, ) @@ -112,7 +133,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3212, bigPicture = 0, extender = 0, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = false, ) @@ -141,7 +162,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 4092, bigPicture = bigPicture.bitmap.allocationByteCount, extender = 0, - style = "BigPictureStyle", + style = NotificationEnums.STYLE_BIG_PICTURE, styleIcon = bigPictureIcon.bitmap.allocationByteCount, hasCustomView = false, ) @@ -167,7 +188,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 4084, bigPicture = 0, extender = 0, - style = "CallStyle", + style = NotificationEnums.STYLE_CALL, styleIcon = personIcon.bitmap.allocationByteCount, hasCustomView = false, ) @@ -203,7 +224,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 5024, bigPicture = 0, extender = 0, - style = "MessagingStyle", + style = NotificationEnums.STYLE_MESSAGING, styleIcon = personIcon.bitmap.allocationByteCount + historicPersonIcon.bitmap.allocationByteCount, @@ -225,7 +246,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3612, bigPicture = 0, extender = 556656, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = false, ) @@ -246,7 +267,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras = 3820, bigPicture = 0, extender = 388 + wearBackground.allocationByteCount, - style = null, + style = NotificationEnums.STYLE_NONE, styleIcon = 0, hasCustomView = false, ) @@ -272,7 +293,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { extras: Int, bigPicture: Int, extender: Int, - style: String?, + style: Int, styleIcon: Int, hasCustomView: Boolean, ) { @@ -282,11 +303,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() { assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon) assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon) assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture) - if (style == null) { - assertThat(memoryUse.objectUsage.style).isNull() - } else { - assertThat(memoryUse.objectUsage.style).isEqualTo(style) - } + assertThat(memoryUse.objectUsage.style).isEqualTo(style) assertThat(memoryUse.objectUsage.styleIcon).isEqualTo(styleIcon) assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt index 3a16fb33388bb..a0f50486ffffd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationMemoryViewWalkerTest.kt @@ -8,6 +8,7 @@ import android.testing.TestableLooper import android.widget.RemoteViews import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder import com.android.systemui.statusbar.notification.row.NotificationTestHelper import com.android.systemui.tests.R import com.google.common.truth.Truth.assertThat @@ -39,16 +40,84 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { fun testViewWalker_plainNotification() { val row = testHelper.createRow() val result = NotificationMemoryViewWalker.getViewUsage(row) - assertThat(result).hasSize(5) - assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) - assertThat(result) - .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result).hasSize(3) assertThat(result) .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0)) assertThat(result) .contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result).contains(NotificationViewUsage(ViewType.TOTAL, 0, 0, 0, 0, 0, 0)) + } + + @Test + fun testViewWalker_plainNotification_withPublicView() { + val icon = Icon.createWithBitmap(Bitmap.createBitmap(20, 20, Bitmap.Config.ARGB_8888)) + val publicIcon = Icon.createWithBitmap(Bitmap.createBitmap(40, 40, Bitmap.Config.ARGB_8888)) + testHelper.setDefaultInflationFlags(NotificationRowContentBinder.FLAG_CONTENT_VIEW_ALL) + val row = + testHelper.createRow( + Notification.Builder(mContext) + .setContentText("Test") + .setContentTitle("title") + .setSmallIcon(icon) + .setPublicVersion( + Notification.Builder(mContext) + .setContentText("Public Test") + .setContentTitle("title") + .setSmallIcon(publicIcon) + .build() + ) + .build() + ) + val result = NotificationMemoryViewWalker.getViewUsage(row) + assertThat(result).hasSize(4) assertThat(result) - .contains(NotificationViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, 0, 0, 0, 0, 0, 0)) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_EXPANDED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PRIVATE_CONTRACTED_VIEW, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.PUBLIC_VIEW, + publicIcon.bitmap.allocationByteCount, + 0, + 0, + 0, + 0, + publicIcon.bitmap.allocationByteCount + ) + ) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.TOTAL, + icon.bitmap.allocationByteCount + publicIcon.bitmap.allocationByteCount, + 0, + 0, + 0, + 0, + icon.bitmap.allocationByteCount + publicIcon.bitmap.allocationByteCount + ) + ) } @Test @@ -67,7 +136,7 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { .build() ) val result = NotificationMemoryViewWalker.getViewUsage(row) - assertThat(result).hasSize(5) + assertThat(result).hasSize(3) assertThat(result) .contains( NotificationViewUsage( @@ -95,8 +164,20 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount ) ) - // Due to deduplication, this should all be 0. - assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.TOTAL, + icon.bitmap.allocationByteCount, + largeIcon.bitmap.allocationByteCount, + 0, + bigPicture.allocationByteCount, + 0, + bigPicture.allocationByteCount + + icon.bitmap.allocationByteCount + + largeIcon.bitmap.allocationByteCount + ) + ) } @Test @@ -117,7 +198,7 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { .build() ) val result = NotificationMemoryViewWalker.getViewUsage(row) - assertThat(result).hasSize(5) + assertThat(result).hasSize(3) assertThat(result) .contains( NotificationViewUsage( @@ -142,7 +223,17 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() { bitmap.allocationByteCount + icon.bitmap.allocationByteCount ) ) - // Due to deduplication, this should all be 0. - assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) + assertThat(result) + .contains( + NotificationViewUsage( + ViewType.TOTAL, + icon.bitmap.allocationByteCount, + 0, + 0, + 0, + bitmap.allocationByteCount, + bitmap.allocationByteCount + icon.bitmap.allocationByteCount + ) + ) } }