Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a52c7538 authored by Jernej Virag's avatar Jernej Virag
Browse files

Add view walking to notification memory monitor

This adds monitor for bitmap memory usage of Notification views
- public and private versions for now. It counts both existing templates and custom views.

The code is still flag guarded.

Bug: 235451049

Test: atest com.android.systemui.statusbar.notification

Change-Id: Ibdc8caaa002622ed51303e24b7d57e225465357c
parent b0d01823
Loading
Loading
Loading
Loading
+25 −1
Original line number Diff line number Diff line
@@ -20,8 +20,9 @@ package com.android.systemui.statusbar.notification.logging
/** Describes usage of a notification. */
data class NotificationMemoryUsage(
    val packageName: String,
    val notificationId: String,
    val notificationKey: String,
    val objectUsage: NotificationObjectUsage,
    val viewUsage: List<NotificationViewUsage>
)

/**
@@ -39,3 +40,26 @@ data class NotificationObjectUsage(
    val extender: Int,
    val hasCustomView: Boolean,
)

enum class ViewType {
    PUBLIC_VIEW,
    PRIVATE_CONTRACTED_VIEW,
    PRIVATE_EXPANDED_VIEW,
    PRIVATE_HEADS_UP_VIEW,
    TOTAL
}

/**
 * Describes current memory of a notification view hierarchy.
 *
 * The values are in bytes.
 */
data class NotificationViewUsage(
    val viewType: ViewType,
    val smallIcon: Int,
    val largeIcon: Int,
    val systemIcons: Int,
    val style: Int,
    val customViews: Int,
    val softwareBitmapsPenalty: Int,
)
+212 −0
Original line number Diff line number Diff line
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 androidx.annotation.WorkerThread
import com.android.systemui.statusbar.notification.NotificationUtils
import com.android.systemui.statusbar.notification.collection.NotificationEntry

/** Calculates estimated memory usage of [Notification] and [NotificationEntry] objects. */
internal object NotificationMemoryMeter {

    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"

    /** 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 =
                    notificationMemoryUse(entry.sbn.notification, hashSetOf())
                val notificationViewUsage = NotificationMemoryViewWalker.getViewUsage(entry.row)
                NotificationMemoryUsage(
                    packageName,
                    NotificationUtils.logKey(entry.sbn.key),
                    notificationObjectUsage,
                    notificationViewUsage
                )
            }
            .toList()
    }

    @WorkerThread
    fun notificationMemoryUse(
        entry: NotificationEntry,
        seenBitmaps: HashSet<Int> = hashSetOf(),
    ): NotificationMemoryUsage {
        return NotificationMemoryUsage(
            entry.sbn.packageName,
            NotificationUtils.logKey(entry.sbn.key),
            notificationMemoryUse(entry.sbn.notification, seenBitmaps),
            NotificationMemoryViewWalker.getViewUsage(entry.row)
        )
    }

    /**
     * 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.
     */
    @WorkerThread
    fun notificationMemoryUse(
        notification: Notification,
        seenBitmaps: HashSet<Int> = hashSetOf(),
    ): 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_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(
            smallIcon = smallIconUse,
            largeIcon = largeIconUse,
            extras = extrasSize,
            style = style?.simpleName,
            styleIcon =
                bigPictureIconUse +
                    peopleUse +
                    callingPersonUse +
                    verificationIconUse +
                    messagesUse +
                    historyicMessagesUse,
            bigPicture = bigPictureUse,
            extender =
                carExtenderSize +
                    carExtenderIcon +
                    tvExtenderSize +
                    wearExtenderSize +
                    wearExtenderBackground,
            hasCustomView = 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
    }
}
+108 −185
Original line number Diff line number Diff line
@@ -17,22 +17,11 @@

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

@@ -46,12 +35,7 @@ constructor(
) : 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"
        private const val TAG = "NotificationMemory"
    }

    fun init() {
@@ -60,184 +44,123 @@ constructor(
    }

    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
        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
            )
            }
            .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)
        // 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 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

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

    /**
     * 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()
        }
    }
    /** Renders a table of notification view usage into passed [PrintWriter] */
    private fun dumpNotificationViewUsage(
        pw: PrintWriter,
        memoryUse: List<NotificationMemoryUsage>,
    ) {

    /**
     * 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
        }
    }
        data class Totals(
            var smallIcon: Int = 0,
            var largeIcon: Int = 0,
            var style: Int = 0,
            var customViews: Int = 0,
            var softwareBitmapsPenalty: Int = 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
        }
        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}"
                    )

    /**
     * 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
                    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
                    }

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

        seenBitmaps.add(refId)
        return icon.dataLength
    private fun toKb(bytes: Int): String {
        return (bytes / 1024).toString() + " KB"
    }
}
+173 −0

File added.

Preview size limit exceeded, changes collapsed.

+1 −0
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@
    limitations under the License.
-->
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/custom_view_dark_image"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="#ff000000"
Loading