1 /*
2  *
<lambda>null3  * 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.app.tracing.traceSection
25 import com.android.systemui.dagger.SysUISingleton
26 import com.android.systemui.dagger.qualifiers.Background
27 import com.android.systemui.dagger.qualifiers.Main
28 import com.android.systemui.shared.system.SysUiStatsLog
29 import com.android.systemui.statusbar.notification.collection.NotifPipeline
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.toList() }
146         }
147 }
148 
149 /** Aggregates memory usage data by package and style, returning sums. */
150 @VisibleForTesting
aggregateMemoryUsageDatanull151 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. */
toKbnull213 private fun toKb(value: Int): Int = (value.toFloat() / 1024f).roundToInt()
214