Merge "Implement class to evaluate current use of Notification object memory" into tm-qpr-dev

This commit is contained in:
Jernej Virag
2022-10-06 17:20:35 +00:00
committed by Android (Google) Code Review
5 changed files with 616 additions and 0 deletions

View File

@@ -68,6 +68,9 @@ public class Flags {
public static final UnreleasedFlag INSTANT_VOICE_REPLY = new UnreleasedFlag(111, true);
public static final UnreleasedFlag NOTIFICATION_MEMORY_MONITOR_ENABLED = new UnreleasedFlag(112,
false);
// next id: 112
/***************************************/

View File

@@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.init
import android.service.notification.StatusBarNotification
import com.android.systemui.ForegroundServiceNotificationListener
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.people.widget.PeopleSpaceWidgetManager
import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper.SnoozeOption
import com.android.systemui.statusbar.NotificationListener
@@ -36,6 +38,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.Co
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.render.NotifStackController
import com.android.systemui.statusbar.notification.interruption.HeadsUpViewBinder
import com.android.systemui.statusbar.notification.logging.NotificationMemoryMonitor
import com.android.systemui.statusbar.notification.row.NotifBindPipelineInitializer
import com.android.systemui.statusbar.notification.stack.NotificationListContainer
import com.android.systemui.statusbar.phone.CentralSurfaces
@@ -68,6 +71,8 @@ class NotificationsControllerImpl @Inject constructor(
private val peopleSpaceWidgetManager: PeopleSpaceWidgetManager,
private val bubblesOptional: Optional<Bubbles>,
private val fgsNotifListener: ForegroundServiceNotificationListener,
private val memoryMonitor: Lazy<NotificationMemoryMonitor>,
private val featureFlags: FeatureFlags
) : NotificationsController {
override fun initialize(
@@ -107,6 +112,9 @@ class NotificationsControllerImpl @Inject constructor(
peopleSpaceWidgetManager.attach(notificationListener)
fgsNotifListener.init()
if (featureFlags.isEnabled(Flags.NOTIFICATION_MEMORY_MONITOR_ENABLED)) {
memoryMonitor.get().init()
}
}
// TODO: Convert all functions below this line into listeners instead of public methods

View File

@@ -0,0 +1,41 @@
/*
*
* 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
/** Describes usage of a notification. */
data class NotificationMemoryUsage(
val packageName: String,
val notificationId: String,
val objectUsage: NotificationObjectUsage,
)
/**
* Describes current memory usage of a [android.app.Notification] object.
*
* The values are in bytes.
*/
data class NotificationObjectUsage(
val smallIcon: Int,
val largeIcon: Int,
val extras: Int,
val style: String?,
val styleIcon: Int,
val bigPicture: Int,
val extender: Int,
val hasCustomView: Boolean,
)

View File

@@ -0,0 +1,243 @@
/*
*
* 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.Person
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.os.Bundle
import android.os.Parcel
import android.os.Parcelable
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.core.util.contains
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.notification.NotificationUtils
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import java.io.PrintWriter
import javax.inject.Inject
/** This class monitors and logs current Notification memory use. */
@SysUISingleton
class NotificationMemoryMonitor
@Inject
constructor(
val notificationPipeline: NotifPipeline,
val dumpManager: DumpManager,
) : Dumpable {
companion object {
private const val TAG = "NotificationMemMonitor"
private const val CAR_EXTENSIONS = "android.car.EXTENSIONS"
private const val CAR_EXTENSIONS_LARGE_ICON = "large_icon"
private const val TV_EXTENSIONS = "android.tv.EXTENSIONS"
private const val WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"
private const val WEARABLE_EXTENSIONS_BACKGROUND = "background"
}
fun init() {
Log.d(TAG, "NotificationMemoryMonitor initialized.")
dumpManager.registerDumpable(javaClass.simpleName, this)
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
currentNotificationMemoryUse().forEach { use -> pw.println(use.toString()) }
}
@WorkerThread
fun currentNotificationMemoryUse(): List<NotificationMemoryUsage> {
return notificationMemoryUse(notificationPipeline.allNotifs)
}
/** Returns a list of memory use entries for currently shown notifications. */
@WorkerThread
fun notificationMemoryUse(
notifications: Collection<NotificationEntry>
): List<NotificationMemoryUsage> {
return notifications
.asSequence()
.map { entry ->
val packageName = entry.sbn.packageName
val notificationObjectUsage =
computeNotificationObjectUse(entry.sbn.notification, hashSetOf())
NotificationMemoryUsage(
packageName,
NotificationUtils.logKey(entry.sbn.key),
notificationObjectUsage
)
}
.toList()
}
/**
* Computes the estimated memory usage of a given [Notification] object. It'll attempt to
* inspect Bitmaps in the object and provide summary of memory usage.
*/
private fun computeNotificationObjectUse(
notification: Notification,
seenBitmaps: HashSet<Int>
): NotificationObjectUsage {
val extras = notification.extras
val smallIconUse = computeIconUse(notification.smallIcon, seenBitmaps)
val largeIconUse = computeIconUse(notification.getLargeIcon(), seenBitmaps)
// Collect memory usage of extra styles
// Big Picture
val bigPictureIconUse =
computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps) +
computeParcelableUse(extras, Notification.EXTRA_LARGE_ICON_BIG, seenBitmaps)
val bigPictureUse =
computeParcelableUse(extras, Notification.EXTRA_PICTURE, seenBitmaps) +
computeParcelableUse(extras, Notification.EXTRA_PICTURE_ICON, seenBitmaps)
// People
val peopleList = extras.getParcelableArrayList<Person>(Notification.EXTRA_PEOPLE_LIST)
val peopleUse =
peopleList?.sumOf { person -> computeIconUse(person.icon, seenBitmaps) } ?: 0
// Calling
val callingPersonUse =
computeParcelableUse(extras, Notification.EXTRA_CALL_PERSON, seenBitmaps)
val verificationIconUse =
computeParcelableUse(extras, Notification.EXTRA_VERIFICATION_ICON, seenBitmaps)
// Messages
val messages =
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
extras.getParcelableArray(Notification.EXTRA_MESSAGES)
)
val messagesUse =
messages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
val historicMessages =
Notification.MessagingStyle.Message.getMessagesFromBundleArray(
extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
)
val historyicMessagesUse =
historicMessages.sumOf { msg -> computeIconUse(msg.senderPerson?.icon, seenBitmaps) }
// Extenders
val carExtender = extras.getBundle(CAR_EXTENSIONS)
val carExtenderSize = carExtender?.let { computeBundleSize(it) } ?: 0
val carExtenderIcon =
computeParcelableUse(carExtender, CAR_EXTENSIONS_LARGE_ICON, seenBitmaps)
val tvExtender = extras.getBundle(TV_EXTENSIONS)
val tvExtenderSize = tvExtender?.let { computeBundleSize(it) } ?: 0
val wearExtender = extras.getBundle(WEARABLE_EXTENSIONS)
val wearExtenderSize = wearExtender?.let { computeBundleSize(it) } ?: 0
val wearExtenderBackground =
computeParcelableUse(wearExtender, WEARABLE_EXTENSIONS_BACKGROUND, seenBitmaps)
val style = notification.notificationStyle
val hasCustomView = notification.contentView != null || notification.bigContentView != null
val extrasSize = computeBundleSize(extras)
return NotificationObjectUsage(
smallIconUse,
largeIconUse,
extrasSize,
style?.simpleName,
bigPictureIconUse +
peopleUse +
callingPersonUse +
verificationIconUse +
messagesUse +
historyicMessagesUse,
bigPictureUse,
carExtenderSize +
carExtenderIcon +
tvExtenderSize +
wearExtenderSize +
wearExtenderBackground,
hasCustomView
)
}
/**
* Calculates size of the bundle data (excluding FDs and other shared objects like ashmem
* bitmaps). Can be slow.
*/
private fun computeBundleSize(extras: Bundle): Int {
val parcel = Parcel.obtain()
try {
extras.writeToParcel(parcel, 0)
return parcel.dataSize()
} finally {
parcel.recycle()
}
}
/**
* Deserializes [Icon], [Bitmap] or [Person] from extras and computes its memory use. Returns 0
* if the key does not exist in extras.
*/
private fun computeParcelableUse(extras: Bundle?, key: String, seenBitmaps: HashSet<Int>): Int {
return when (val parcelable = extras?.getParcelable<Parcelable>(key)) {
is Bitmap -> computeBitmapUse(parcelable, seenBitmaps)
is Icon -> computeIconUse(parcelable, seenBitmaps)
is Person -> computeIconUse(parcelable.icon, seenBitmaps)
else -> 0
}
}
/**
* Calculates the byte size of bitmaps or data in the Icon object. Returns 0 if the icon is
* defined via Uri or a resource.
*
* @return memory usage in bytes or 0 if the icon is Uri/Resource based
*/
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)
Icon.TYPE_DATA -> computeDataUse(icon, seenBitmaps)
else -> 0
}
/**
* Returns the amount of memory a given bitmap is using. If the bitmap reference is part of
* seenBitmaps set, this method returns 0 to avoid double counting.
*
* @return memory usage of the bitmap in bytes
*/
private fun computeBitmapUse(bitmap: Bitmap, seenBitmaps: HashSet<Int>? = null): Int {
val refId = System.identityHashCode(bitmap)
if (seenBitmaps?.contains(refId) == true) {
return 0
}
seenBitmaps?.add(refId)
return bitmap.allocationByteCount
}
private fun computeDataUse(icon: Icon, seenBitmaps: HashSet<Int>): Int {
val refId = System.identityHashCode(icon.dataBytes)
if (seenBitmaps.contains(refId)) {
return 0
}
seenBitmaps.add(refId)
return icon.dataLength
}
}

View File

@@ -0,0 +1,321 @@
/*
*
* 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.PendingIntent
import android.app.Person
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Icon
import android.testing.AndroidTestingRunner
import android.widget.RemoteViews
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.NotificationUtils
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.MockitoAnnotations
@SmallTest
@RunWith(AndroidTestingRunner::class)
class NotificationMemoryMonitorTest : SysuiTestCase() {
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
}
@Test
fun currentNotificationMemoryUse_plainNotification() {
val notification = createBasicNotification().build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse,
smallIcon = notification.smallIcon.bitmap.allocationByteCount,
largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
extras = 3316,
bigPicture = 0,
extender = 0,
style = null,
styleIcon = 0,
hasCustomView = false,
)
}
@Test
fun currentNotificationMemoryUse_plainNotification_dontDoubleCountSameBitmap() {
val icon = Icon.createWithBitmap(Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888))
val notification = createBasicNotification().setLargeIcon(icon).setSmallIcon(icon).build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse = memoryUse,
smallIcon = notification.smallIcon.bitmap.allocationByteCount,
largeIcon = 0,
extras = 3316,
bigPicture = 0,
extender = 0,
style = null,
styleIcon = 0,
hasCustomView = false,
)
}
@Test
fun currentNotificationMemoryUse_customViewNotification_marksTrue() {
val notification =
createBasicNotification()
.setCustomContentView(
RemoteViews(context.packageName, android.R.layout.list_content)
)
.build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse = memoryUse,
smallIcon = notification.smallIcon.bitmap.allocationByteCount,
largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
extras = 3384,
bigPicture = 0,
extender = 0,
style = null,
styleIcon = 0,
hasCustomView = true,
)
}
@Test
fun currentNotificationMemoryUse_notificationWithDataIcon_calculatesCorrectly() {
val dataIcon = Icon.createWithData(ByteArray(444444), 0, 444444)
val notification =
createBasicNotification().setLargeIcon(dataIcon).setSmallIcon(dataIcon).build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse = memoryUse,
smallIcon = 444444,
largeIcon = 0,
extras = 3212,
bigPicture = 0,
extender = 0,
style = null,
styleIcon = 0,
hasCustomView = false,
)
}
@Test
fun currentNotificationMemoryUse_bigPictureStyle() {
val bigPicture =
Icon.createWithBitmap(Bitmap.createBitmap(600, 400, Bitmap.Config.ARGB_8888))
val bigPictureIcon =
Icon.createWithAdaptiveBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
val notification =
createBasicNotification()
.setStyle(
Notification.BigPictureStyle()
.bigPicture(bigPicture)
.bigLargeIcon(bigPictureIcon)
)
.build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse = memoryUse,
smallIcon = notification.smallIcon.bitmap.allocationByteCount,
largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
extras = 4092,
bigPicture = bigPicture.bitmap.allocationByteCount,
extender = 0,
style = "BigPictureStyle",
styleIcon = bigPictureIcon.bitmap.allocationByteCount,
hasCustomView = false,
)
}
@Test
fun currentNotificationMemoryUse_callingStyle() {
val personIcon =
Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
val person = Person.Builder().setIcon(personIcon).setName("Person").build()
val fakeIntent =
PendingIntent.getActivity(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
val notification =
createBasicNotification()
.setStyle(Notification.CallStyle.forIncomingCall(person, fakeIntent, fakeIntent))
.build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse = memoryUse,
smallIcon = notification.smallIcon.bitmap.allocationByteCount,
largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
extras = 4084,
bigPicture = 0,
extender = 0,
style = "CallStyle",
styleIcon = personIcon.bitmap.allocationByteCount,
hasCustomView = false,
)
}
@Test
fun currentNotificationMemoryUse_messagingStyle() {
val personIcon =
Icon.createWithBitmap(Bitmap.createBitmap(386, 432, Bitmap.Config.ARGB_8888))
val person = Person.Builder().setIcon(personIcon).setName("Person").build()
val message = Notification.MessagingStyle.Message("Message!", 4323, person)
val historicPersonIcon =
Icon.createWithBitmap(Bitmap.createBitmap(348, 382, Bitmap.Config.ARGB_8888))
val historicPerson =
Person.Builder().setIcon(historicPersonIcon).setName("Historic person").build()
val historicMessage =
Notification.MessagingStyle.Message("Historic message!", 5848, historicPerson)
val notification =
createBasicNotification()
.setStyle(
Notification.MessagingStyle(person)
.addMessage(message)
.addHistoricMessage(historicMessage)
)
.build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse = memoryUse,
smallIcon = notification.smallIcon.bitmap.allocationByteCount,
largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
extras = 5024,
bigPicture = 0,
extender = 0,
style = "MessagingStyle",
styleIcon =
personIcon.bitmap.allocationByteCount +
historicPersonIcon.bitmap.allocationByteCount,
hasCustomView = false,
)
}
@Test
fun currentNotificationMemoryUse_carExtender() {
val carIcon = Bitmap.createBitmap(432, 322, Bitmap.Config.ARGB_8888)
val extender = Notification.CarExtender().setLargeIcon(carIcon)
val notification = createBasicNotification().extend(extender).build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse = memoryUse,
smallIcon = notification.smallIcon.bitmap.allocationByteCount,
largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
extras = 3612,
bigPicture = 0,
extender = 556656,
style = null,
styleIcon = 0,
hasCustomView = false,
)
}
@Test
fun currentNotificationMemoryUse_tvWearExtender() {
val tvExtender = Notification.TvExtender().setChannel("channel2")
val wearBackground = Bitmap.createBitmap(443, 433, Bitmap.Config.ARGB_8888)
val wearExtender = Notification.WearableExtender().setBackground(wearBackground)
val notification = createBasicNotification().extend(tvExtender).extend(wearExtender).build()
val nmm = createNMMWithNotifications(listOf(notification))
val memoryUse = getUseObject(nmm.currentNotificationMemoryUse())
assertNotificationObjectSizes(
memoryUse = memoryUse,
smallIcon = notification.smallIcon.bitmap.allocationByteCount,
largeIcon = notification.getLargeIcon().bitmap.allocationByteCount,
extras = 3820,
bigPicture = 0,
extender = 388 + wearBackground.allocationByteCount,
style = null,
styleIcon = 0,
hasCustomView = false,
)
}
private fun createBasicNotification(): Notification.Builder {
val smallIcon =
Icon.createWithBitmap(Bitmap.createBitmap(250, 250, Bitmap.Config.ARGB_8888))
val largeIcon =
Icon.createWithBitmap(Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888))
return Notification.Builder(context)
.setSmallIcon(smallIcon)
.setLargeIcon(largeIcon)
.setContentTitle("This is a title")
.setContentText("This is content text.")
}
/** This will generate a nicer error message than comparing objects */
private fun assertNotificationObjectSizes(
memoryUse: NotificationMemoryUsage,
smallIcon: Int,
largeIcon: Int,
extras: Int,
bigPicture: Int,
extender: Int,
style: String?,
styleIcon: Int,
hasCustomView: Boolean
) {
assertThat(memoryUse.packageName).isEqualTo("test_pkg")
assertThat(memoryUse.notificationId)
.isEqualTo(NotificationUtils.logKey("0|test_pkg|0|test|0"))
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.styleIcon).isEqualTo(styleIcon)
assertThat(memoryUse.objectUsage.hasCustomView).isEqualTo(hasCustomView)
}
private fun getUseObject(
singleItemUseList: List<NotificationMemoryUsage>
): NotificationMemoryUsage {
assertThat(singleItemUseList).hasSize(1)
return singleItemUseList[0]
}
private fun createNMMWithNotifications(
notifications: List<Notification>
): NotificationMemoryMonitor {
val notifPipeline: NotifPipeline = mock()
val notificationEntries =
notifications.map { n ->
NotificationEntryBuilder().setTag("test").setNotification(n).build()
}
whenever(notifPipeline.allNotifs).thenReturn(notificationEntries)
return NotificationMemoryMonitor(notifPipeline, mock())
}
}