Merge "Implement pull logging for Notification Memory" into tm-qpr-dev-plus-aosp

This commit is contained in:
Jernej Virag
2022-12-16 09:29:38 +00:00
committed by Android (Google) Code Review
9 changed files with 746 additions and 183 deletions

View File

@@ -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<NotificationViewUsage>
)
@@ -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,

View File

@@ -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<out String>) {
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<NotificationMemoryUsage>) {
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<NotificationMemoryUsage>,
) {
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"
}
}

View File

@@ -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<StatsEvent>): 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<NotificationMemoryUsage>
): Map<Pair<String, Int>, 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()
}

View File

@@ -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<out Notification.Style>?): 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<Int>) =
private fun computeIconUse(icon: Icon?, seenBitmaps: HashSet<Int>): Int =
when (icon?.type) {
Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
Icon.TYPE_ADAPTIVE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)

View File

@@ -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<NotificationMemoryLogger>,
) {
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<out String>) {
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<NotificationMemoryUsage>) {
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<NotificationMemoryUsage>,
) {
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"
}
}

View File

@@ -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<NotificationViewUsage> {
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<Int>()
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<Int> = 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(

View File

@@ -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<StatsEvent> = 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<StatsEvent> = 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<StatsEvent> = 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<StatsEvent> = mutableListOf()
assertThat(logger.onPullAtom(SysUiStatsLog.NOTIFICATION_MEMORY_USE, data))
.isEqualTo(StatsManager.PULL_SUCCESS)
assertThat(data).hasSize(2)
}
private fun createLoggerWithNotifications(
notifications: List<Notification>
): 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)
}
}

View File

@@ -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)
}

View File

@@ -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
)
)
}
}