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