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

Commit 3e3f2823 authored by Jernej Virag's avatar Jernej Virag
Browse files

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
Merged-In: I671c8819c25795d2a18639808830802af3bbf1eb
parent dceba093
Loading
Loading
Loading
Loading
+6 −1
Original line number Diff line number Diff line
@@ -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,
+173 −0
Original line number Diff line number Diff line
/*
 *
 * 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"
    }
}
+194 −0
Original line number Diff line number Diff line
/*
 *
 * 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()
}
+42 −3
Original line number Diff line number Diff line
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)
+11 −128

File changed.

Preview size limit exceeded, changes collapsed.

Loading