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
This commit is contained in:
Jernej Virag
2022-12-02 18:15:24 +01:00
parent 4ba0f81576
commit 9907eb6cd0
9 changed files with 746 additions and 183 deletions

View File

@@ -17,10 +17,14 @@
package com.android.systemui.statusbar.notification.logging package com.android.systemui.statusbar.notification.logging
import android.app.Notification
/** Describes usage of a notification. */ /** Describes usage of a notification. */
data class NotificationMemoryUsage( data class NotificationMemoryUsage(
val packageName: String, val packageName: String,
val uid: Int,
val notificationKey: String, val notificationKey: String,
val notification: Notification,
val objectUsage: NotificationObjectUsage, val objectUsage: NotificationObjectUsage,
val viewUsage: List<NotificationViewUsage> val viewUsage: List<NotificationViewUsage>
) )
@@ -34,7 +38,8 @@ data class NotificationObjectUsage(
val smallIcon: Int, val smallIcon: Int,
val largeIcon: Int, val largeIcon: Int,
val extras: Int, val extras: Int,
val style: String?, /** Style type, integer from [android.stats.sysui.NotificationEnums] */
val style: Int,
val styleIcon: Int, val styleIcon: Int,
val bigPicture: Int, val bigPicture: Int,
val extender: 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 package com.android.systemui.statusbar.notification.logging
import android.app.Notification 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.app.Person
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.os.Bundle import android.os.Bundle
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.stats.sysui.NotificationEnums
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.android.systemui.statusbar.notification.NotificationUtils import com.android.systemui.statusbar.notification.NotificationUtils
import com.android.systemui.statusbar.notification.collection.NotificationEntry 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 TV_EXTENSIONS = "android.tv.EXTENSIONS"
private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS" private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
private const val WEARABLE_EXTENSIONS_BACKGROUND = "background" 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. */ /** Returns a list of memory use entries for currently shown notifications. */
@WorkerThread @WorkerThread
@@ -29,12 +38,15 @@ internal object NotificationMemoryMeter {
.asSequence() .asSequence()
.map { entry -> .map { entry ->
val packageName = entry.sbn.packageName val packageName = entry.sbn.packageName
val uid = entry.sbn.uid
val notificationObjectUsage = val notificationObjectUsage =
notificationMemoryUse(entry.sbn.notification, hashSetOf()) notificationMemoryUse(entry.sbn.notification, hashSetOf())
val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row) val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row)
NotificationMemoryUsage( NotificationMemoryUsage(
packageName, packageName,
uid,
NotificationUtils.logKey(entry.sbn.key), NotificationUtils.logKey(entry.sbn.key),
entry.sbn.notification,
notificationObjectUsage, notificationObjectUsage,
notificationViewUsage notificationViewUsage
) )
@@ -49,7 +61,9 @@ internal object NotificationMemoryMeter {
): NotificationMemoryUsage { ): NotificationMemoryUsage {
return NotificationMemoryUsage( return NotificationMemoryUsage(
entry.sbn.packageName, entry.sbn.packageName,
entry.sbn.uid,
NotificationUtils.logKey(entry.sbn.key), NotificationUtils.logKey(entry.sbn.key),
entry.sbn.notification,
notificationMemoryUse(entry.sbn.notification, seenBitmaps), notificationMemoryUse(entry.sbn.notification, seenBitmaps),
NotificationMemoryViewWalker.getViewUsage(entry.row) NotificationMemoryViewWalker.getViewUsage(entry.row)
) )
@@ -116,7 +130,13 @@ internal object NotificationMemoryMeter {
val wearExtenderBackground = val wearExtenderBackground =
computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps) 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 hasCustomView = notification.contentView != null || notification.bigContentView != null
val extrasSize = computeBundleSize(extras) val extrasSize = computeBundleSize(extras)
@@ -124,7 +144,7 @@ internal object NotificationMemoryMeter {
smallIcon = smallIconUse, smallIcon = smallIconUse,
largeIcon = largeIconUse, largeIcon = largeIconUse,
extras = extrasSize, extras = extrasSize,
style = style?.simpleName, style = style,
styleIcon = styleIcon =
bigPictureIconUse + bigPictureIconUse +
peopleUse + 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 * Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
* bitmaps). Can be slow. * 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 * @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) { when (icon?.type) {
Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps) Icon.TYPE_BITMAP -> computeBitmapUse(icon.bitmap, seenBitmaps)
Icon.TYPE_ADAPTIVE_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 package com.android.systemui.statusbar.notification.logging
import android.util.Log import android.util.Log
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FeatureFlags
import com.android.systemui.statusbar.notification.collection.NotifPipeline import com.android.systemui.flags.Flags
import java.io.PrintWriter import dagger.Lazy
import javax.inject.Inject import javax.inject.Inject
/** This class monitors and logs current Notification memory use. */ /** This class monitors and logs current Notification memory use. */
@@ -30,9 +29,10 @@ import javax.inject.Inject
class NotificationMemoryMonitor class NotificationMemoryMonitor
@Inject @Inject
constructor( constructor(
val notificationPipeline: NotifPipeline, private val featureFlags: FeatureFlags,
val dumpManager: DumpManager, private val notificationMemoryDumper: NotificationMemoryDumper,
) : Dumpable { private val notificationMemoryLogger: Lazy<NotificationMemoryLogger>,
) {
companion object { companion object {
private const val TAG = "NotificationMemory" private const val TAG = "NotificationMemory"
@@ -40,127 +40,10 @@ constructor(
fun init() { fun init() {
Log.d(TAG, "NotificationMemoryMonitor initialized.") Log.d(TAG, "NotificationMemoryMonitor initialized.")
dumpManager.registerDumpable(javaClass.simpleName, this) notificationMemoryDumper.init()
} if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_LOGGING_ENABLED)) {
Log.d(TAG, "Notification memory logging enabled.")
override fun dump(pw: PrintWriter, args: Array<out String>) { notificationMemoryLogger.get().init()
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
)
}
// 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 * 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> { fun getViewUsage(row: ExpandableNotificationRow?): List<NotificationViewUsage> {
if (row == null) { if (row == null) {
@@ -58,42 +62,72 @@ internal object NotificationMemoryViewWalker {
} }
// The ordering here is significant since it determines deduplication of seen drawables. // The ordering here is significant since it determines deduplication of seen drawables.
return listOf( val perViewUsages =
listOf(
getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild), getViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, row.privateLayout?.expandedChild),
getViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, row.privateLayout?.contractedChild), getViewUsage(
ViewType.PRIVATE_CONTRACTED_VIEW,
row.privateLayout?.contractedChild
),
getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild), getViewUsage(ViewType.PRIVATE_HEADS_UP_VIEW, row.privateLayout?.headsUpChild),
getViewUsage(ViewType.PUBLIC_VIEW, row.publicLayout), getViewUsage(
getTotalUsage(row) 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 * Calculate total usage of all views - we need to do a separate traversal to make sure we don't
* double count fields. * double count fields.
*/ */
private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage { private fun getTotalUsage(row: ExpandableNotificationRow): NotificationViewUsage? {
val totalUsage = UsageBuilder()
val seenObjects = hashSetOf<Int>() val seenObjects = hashSetOf<Int>()
return getViewUsage(
row.publicLayout?.let { computeViewHierarchyUse(it, totalUsage, seenObjects) } ViewType.TOTAL,
row.privateLayout?.let { child -> row.privateLayout?.expandedChild,
for (view in listOf(child.expandedChild, child.contractedChild, child.headsUpChild)) { row.privateLayout?.contractedChild,
(view as? ViewGroup)?.let { v -> row.privateLayout?.headsUpChild,
computeViewHierarchyUse(v, totalUsage, seenObjects) row.publicLayout?.expandedChild,
} row.publicLayout?.contractedChild,
} row.publicLayout?.headsUpChild,
} seenObjects = seenObjects
return totalUsage.build(ViewType.TOTAL) )
} }
private fun getViewUsage( private fun getViewUsage(
type: ViewType, type: ViewType,
rootView: View?, vararg rootViews: View?,
seenObjects: HashSet<Int> = hashSetOf() seenObjects: HashSet<Int> = hashSetOf()
): NotificationViewUsage { ): NotificationViewUsage? {
val usageBuilder = UsageBuilder() val usageBuilder = lazy { UsageBuilder() }
(rootView as? ViewGroup)?.let { computeViewHierarchyUse(it, usageBuilder, seenObjects) } rootViews.forEach { rootView ->
return usageBuilder.build(type) (rootView as? ViewGroup)?.let { rootViewGroup ->
computeViewHierarchyUse(rootViewGroup, usageBuilder.value, seenObjects)
}
}
return if (usageBuilder.isInitialized()) {
usageBuilder.value.build(type)
} else {
null
}
} }
private fun computeViewHierarchyUse( 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.content.Intent
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.drawable.Icon import android.graphics.drawable.Icon
import android.stats.sysui.NotificationEnums
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
@@ -50,7 +51,27 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 3316, extras = 3316,
bigPicture = 0, bigPicture = 0,
extender = 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, styleIcon = 0,
hasCustomView = false, hasCustomView = false,
) )
@@ -69,7 +90,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 3316, extras = 3316,
bigPicture = 0, bigPicture = 0,
extender = 0, extender = 0,
style = null, style = NotificationEnums.STYLE_NONE,
styleIcon = 0, styleIcon = 0,
hasCustomView = false, hasCustomView = false,
) )
@@ -92,7 +113,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 3384, extras = 3384,
bigPicture = 0, bigPicture = 0,
extender = 0, extender = 0,
style = null, style = NotificationEnums.STYLE_NONE,
styleIcon = 0, styleIcon = 0,
hasCustomView = true, hasCustomView = true,
) )
@@ -112,7 +133,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 3212, extras = 3212,
bigPicture = 0, bigPicture = 0,
extender = 0, extender = 0,
style = null, style = NotificationEnums.STYLE_NONE,
styleIcon = 0, styleIcon = 0,
hasCustomView = false, hasCustomView = false,
) )
@@ -141,7 +162,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 4092, extras = 4092,
bigPicture = bigPicture.bitmap.allocationByteCount, bigPicture = bigPicture.bitmap.allocationByteCount,
extender = 0, extender = 0,
style = "BigPictureStyle", style = NotificationEnums.STYLE_BIG_PICTURE,
styleIcon = bigPictureIcon.bitmap.allocationByteCount, styleIcon = bigPictureIcon.bitmap.allocationByteCount,
hasCustomView = false, hasCustomView = false,
) )
@@ -167,7 +188,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 4084, extras = 4084,
bigPicture = 0, bigPicture = 0,
extender = 0, extender = 0,
style = "CallStyle", style = NotificationEnums.STYLE_CALL,
styleIcon = personIcon.bitmap.allocationByteCount, styleIcon = personIcon.bitmap.allocationByteCount,
hasCustomView = false, hasCustomView = false,
) )
@@ -203,7 +224,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 5024, extras = 5024,
bigPicture = 0, bigPicture = 0,
extender = 0, extender = 0,
style = "MessagingStyle", style = NotificationEnums.STYLE_MESSAGING,
styleIcon = styleIcon =
personIcon.bitmap.allocationByteCount + personIcon.bitmap.allocationByteCount +
historicPersonIcon.bitmap.allocationByteCount, historicPersonIcon.bitmap.allocationByteCount,
@@ -225,7 +246,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 3612, extras = 3612,
bigPicture = 0, bigPicture = 0,
extender = 556656, extender = 556656,
style = null, style = NotificationEnums.STYLE_NONE,
styleIcon = 0, styleIcon = 0,
hasCustomView = false, hasCustomView = false,
) )
@@ -246,7 +267,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras = 3820, extras = 3820,
bigPicture = 0, bigPicture = 0,
extender = 388 + wearBackground.allocationByteCount, extender = 388 + wearBackground.allocationByteCount,
style = null, style = NotificationEnums.STYLE_NONE,
styleIcon = 0, styleIcon = 0,
hasCustomView = false, hasCustomView = false,
) )
@@ -272,7 +293,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
extras: Int, extras: Int,
bigPicture: Int, bigPicture: Int,
extender: Int, extender: Int,
style: String?, style: Int,
styleIcon: Int, styleIcon: Int,
hasCustomView: Boolean, hasCustomView: Boolean,
) { ) {
@@ -282,11 +303,7 @@ class NotificationMemoryMeterTest : SysuiTestCase() {
assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon) assertThat(memoryUse.objectUsage.smallIcon).isEqualTo(smallIcon)
assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon) assertThat(memoryUse.objectUsage.largeIcon).isEqualTo(largeIcon)
assertThat(memoryUse.objectUsage.bigPicture).isEqualTo(bigPicture) 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.styleIcon).isEqualTo(styleIcon)
assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView) assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
} }

View File

@@ -8,6 +8,7 @@ import android.testing.TestableLooper
import android.widget.RemoteViews import android.widget.RemoteViews
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase 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.statusbar.notification.row.NotificationTestHelper
import com.android.systemui.tests.R import com.android.systemui.tests.R
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
@@ -39,16 +40,84 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() {
fun testViewWalker_plainNotification() { fun testViewWalker_plainNotification() {
val row = testHelper.createRow() val row = testHelper.createRow()
val result = NotificationMemoryViewWalker.getViewUsage(row) val result = NotificationMemoryViewWalker.getViewUsage(row)
assertThat(result).hasSize(5) assertThat(result).hasSize(3)
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) assertThat(result)
.contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0)) .contains(NotificationViewUsage(ViewType.PRIVATE_EXPANDED_VIEW, 0, 0, 0, 0, 0, 0))
assertThat(result) assertThat(result)
.contains(NotificationViewUsage(ViewType.PRIVATE_CONTRACTED_VIEW, 0, 0, 0, 0, 0, 0)) .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) 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 @Test
@@ -67,7 +136,7 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() {
.build() .build()
) )
val result = NotificationMemoryViewWalker.getViewUsage(row) val result = NotificationMemoryViewWalker.getViewUsage(row)
assertThat(result).hasSize(5) assertThat(result).hasSize(3)
assertThat(result) assertThat(result)
.contains( .contains(
NotificationViewUsage( NotificationViewUsage(
@@ -95,8 +164,20 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() {
icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount icon.bitmap.allocationByteCount + largeIcon.bitmap.allocationByteCount
) )
) )
// Due to deduplication, this should all be 0. assertThat(result)
assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) .contains(
NotificationViewUsage(
ViewType.TOTAL,
icon.bitmap.allocationByteCount,
largeIcon.bitmap.allocationByteCount,
0,
bigPicture.allocationByteCount,
0,
bigPicture.allocationByteCount +
icon.bitmap.allocationByteCount +
largeIcon.bitmap.allocationByteCount
)
)
} }
@Test @Test
@@ -117,7 +198,7 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() {
.build() .build()
) )
val result = NotificationMemoryViewWalker.getViewUsage(row) val result = NotificationMemoryViewWalker.getViewUsage(row)
assertThat(result).hasSize(5) assertThat(result).hasSize(3)
assertThat(result) assertThat(result)
.contains( .contains(
NotificationViewUsage( NotificationViewUsage(
@@ -142,7 +223,17 @@ class NotificationMemoryViewWalkerTest : SysuiTestCase() {
bitmap.allocationByteCount + icon.bitmap.allocationByteCount bitmap.allocationByteCount + icon.bitmap.allocationByteCount
) )
) )
// Due to deduplication, this should all be 0. assertThat(result)
assertThat(result).contains(NotificationViewUsage(ViewType.PUBLIC_VIEW, 0, 0, 0, 0, 0, 0)) .contains(
NotificationViewUsage(
ViewType.TOTAL,
icon.bitmap.allocationByteCount,
0,
0,
0,
bitmap.allocationByteCount,
bitmap.allocationByteCount + icon.bitmap.allocationByteCount
)
)
} }
} }