1 /* 2 * 3 * Copyright (C) 2022 The Android Open Source Project 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.systemui.statusbar.notification.logging 19 20 import android.app.StatsManager 21 import android.util.Log 22 import android.util.StatsEvent 23 import androidx.annotation.VisibleForTesting 24 import com.android.systemui.dagger.SysUISingleton 25 import com.android.systemui.dagger.qualifiers.Background 26 import com.android.systemui.dagger.qualifiers.Main 27 import com.android.systemui.shared.system.SysUiStatsLog 28 import com.android.systemui.statusbar.notification.collection.NotifPipeline 29 import com.android.systemui.util.traceSection 30 import java.lang.Exception 31 import java.util.concurrent.Executor 32 import javax.inject.Inject 33 import kotlin.math.roundToInt 34 import kotlinx.coroutines.CoroutineDispatcher 35 import kotlinx.coroutines.runBlocking 36 37 /** Periodically logs current state of notification memory consumption. */ 38 @SysUISingleton 39 class NotificationMemoryLogger 40 @Inject 41 constructor( 42 private val notificationPipeline: NotifPipeline, 43 private val statsManager: StatsManager, 44 @Main private val mainDispatcher: CoroutineDispatcher, 45 @Background private val backgroundExecutor: Executor 46 ) : StatsManager.StatsPullAtomCallback { 47 48 /** 49 * This class is used to accumulate and aggregate data - the fields mirror values in statd Atom 50 * with ONE IMPORTANT difference - the values are in bytes, not KB! 51 */ 52 internal data class NotificationMemoryUseAtomBuilder(val uid: Int, val style: Int) { 53 var count: Int = 0 54 var countWithInflatedViews: Int = 0 55 var smallIconObject: Int = 0 56 var smallIconBitmapCount: Int = 0 57 var largeIconObject: Int = 0 58 var largeIconBitmapCount: Int = 0 59 var bigPictureObject: Int = 0 60 var bigPictureBitmapCount: Int = 0 61 var extras: Int = 0 62 var extenders: Int = 0 63 var smallIconViews: Int = 0 64 var largeIconViews: Int = 0 65 var systemIconViews: Int = 0 66 var styleViews: Int = 0 67 var customViews: Int = 0 68 var softwareBitmaps: Int = 0 69 var seenCount = 0 70 } 71 72 fun init() { 73 statsManager.setPullAtomCallback( 74 SysUiStatsLog.NOTIFICATION_MEMORY_USE, 75 null, 76 backgroundExecutor, 77 this 78 ) 79 } 80 81 /** Called by statsd to pull data. */ 82 override fun onPullAtom(atomTag: Int, data: MutableList<StatsEvent>): Int = 83 traceSection("NML#onPullAtom") { 84 if (atomTag != SysUiStatsLog.NOTIFICATION_MEMORY_USE) { 85 return StatsManager.PULL_SKIP 86 } 87 88 try { 89 // Notifications can only be retrieved on the main thread, so switch to that thread. 90 val notifications = getAllNotificationsOnMainThread() 91 val notificationMemoryUse = 92 NotificationMemoryMeter.notificationMemoryUse(notifications) 93 .sortedWith( 94 compareBy( 95 { it.packageName }, 96 { it.objectUsage.style }, 97 { it.notificationKey } 98 ) 99 ) 100 val usageData = aggregateMemoryUsageData(notificationMemoryUse) 101 usageData.forEach { (_, use) -> 102 data.add( 103 SysUiStatsLog.buildStatsEvent( 104 SysUiStatsLog.NOTIFICATION_MEMORY_USE, 105 use.uid, 106 use.style, 107 use.count, 108 use.countWithInflatedViews, 109 toKb(use.smallIconObject), 110 use.smallIconBitmapCount, 111 toKb(use.largeIconObject), 112 use.largeIconBitmapCount, 113 toKb(use.bigPictureObject), 114 use.bigPictureBitmapCount, 115 toKb(use.extras), 116 toKb(use.extenders), 117 toKb(use.smallIconViews), 118 toKb(use.largeIconViews), 119 toKb(use.systemIconViews), 120 toKb(use.styleViews), 121 toKb(use.customViews), 122 toKb(use.softwareBitmaps), 123 use.seenCount 124 ) 125 ) 126 } 127 } catch (e: InterruptedException) { 128 // This can happen if the device is sleeping or view walking takes too long. 129 // The statsd collector will interrupt the thread and we need to handle it 130 // gracefully. 131 Log.w(NotificationLogger.TAG, "Timed out when measuring notification memory.", e) 132 return@traceSection StatsManager.PULL_SKIP 133 } catch (e: Exception) { 134 // Error while collecting data, this should not crash prod SysUI. Just 135 // log WTF and move on. 136 Log.wtf(NotificationLogger.TAG, "Failed to measure notification memory.", e) 137 return@traceSection StatsManager.PULL_SKIP 138 } 139 140 return StatsManager.PULL_SUCCESS 141 } 142 143 private fun getAllNotificationsOnMainThread() = 144 runBlocking(mainDispatcher) { 145 traceSection("NML#getNotifications") { notificationPipeline.allNotifs } 146 } 147 } 148 149 /** Aggregates memory usage data by package and style, returning sums. */ 150 @VisibleForTesting 151 internal fun aggregateMemoryUsageData( 152 notificationMemoryUse: List<NotificationMemoryUsage> 153 ): Map<Pair<String, Int>, NotificationMemoryLogger.NotificationMemoryUseAtomBuilder> { 154 return notificationMemoryUse 155 .groupingBy { Pair(it.packageName, it.objectUsage.style) } 156 .aggregate { 157 _, 158 accumulator: NotificationMemoryLogger.NotificationMemoryUseAtomBuilder?, 159 element: NotificationMemoryUsage, 160 first -> 161 val use = 162 if (first) { 163 NotificationMemoryLogger.NotificationMemoryUseAtomBuilder( 164 element.uid, 165 element.objectUsage.style 166 ) 167 } else { 168 accumulator!! 169 } 170 171 use.count++ 172 // If the views of the notification weren't inflated, the list of memory usage 173 // parameters will be empty. 174 if (element.viewUsage.isNotEmpty()) { 175 use.countWithInflatedViews++ 176 } 177 178 use.smallIconObject += element.objectUsage.smallIcon 179 if (element.objectUsage.smallIcon > 0) { 180 use.smallIconBitmapCount++ 181 } 182 183 use.largeIconObject += element.objectUsage.largeIcon 184 if (element.objectUsage.largeIcon > 0) { 185 use.largeIconBitmapCount++ 186 } 187 188 use.bigPictureObject += element.objectUsage.bigPicture 189 if (element.objectUsage.bigPicture > 0) { 190 use.bigPictureBitmapCount++ 191 } 192 193 use.extras += element.objectUsage.extras 194 use.extenders += element.objectUsage.extender 195 196 // Use totals count which are more accurate when aggregated 197 // in this manner. 198 element.viewUsage 199 .firstOrNull { vu -> vu.viewType == ViewType.TOTAL } 200 ?.let { 201 use.smallIconViews += it.smallIcon 202 use.largeIconViews += it.largeIcon 203 use.systemIconViews += it.systemIcons 204 use.styleViews += it.style 205 use.customViews += it.customViews 206 use.softwareBitmaps += it.softwareBitmapsPenalty 207 } 208 209 return@aggregate use 210 } 211 } 212 /** Rounds the passed value to the nearest KB - e.g. 700B rounds to 1KB. */ 213 private fun toKb(value: Int): Int = (value.toFloat() / 1024f).roundToInt() 214