1 /* 2 * Copyright (C) 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.power.stats; 18 19 import android.app.StatsManager; 20 import android.content.Context; 21 import android.os.SystemClock; 22 import android.util.Log; 23 import android.util.StatsEvent; 24 25 import com.android.internal.annotations.GuardedBy; 26 import com.android.internal.annotations.VisibleForTesting; 27 import com.android.internal.util.ConcurrentUtils; 28 import com.android.internal.util.FrameworkStatsLog; 29 30 import java.util.ArrayList; 31 import java.util.HashMap; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Objects; 36 37 /** A class to initialise and log metrics pulled by statsd. */ 38 public class WakelockStatsFrameworkEvents { 39 // statsd has a dimensional limit on the number of different keys it can handle. 40 // Beyond that limit, statsd will drop data. 41 // 42 // When we have seem SUMMARY_THRESHOLD distinct (uid, tag, wakeLockLevel) keys, 43 // we start summarizing new keys as (uid, OVERFLOW_TAG, OVERFLOW_LEVEL) to 44 // reduce the number of keys we pass to statsd. 45 // 46 // When we reach MAX_WAKELOCK_DIMENSIONS distinct keys, we summarize all new keys 47 // as (OVERFLOW_UID, HARD_CAP_TAG, OVERFLOW_LEVEL) to hard cap the number of 48 // distinct keys we pass to statsd. 49 @VisibleForTesting public static final int SUMMARY_THRESHOLD = 500; 50 @VisibleForTesting public static final int MAX_WAKELOCK_DIMENSIONS = 1000; 51 52 @VisibleForTesting public static final int HARD_CAP_UID = -1; 53 @VisibleForTesting public static final String OVERFLOW_TAG = "*overflow*"; 54 @VisibleForTesting public static final String HARD_CAP_TAG = "*overflow hard cap*"; 55 @VisibleForTesting public static final int OVERFLOW_LEVEL = 1; 56 57 private static class WakeLockKey { 58 private int uid; 59 private String tag; 60 private int powerManagerWakeLockLevel; 61 private int hashCode; 62 WakeLockKey(int uid, String tag, int powerManagerWakeLockLevel)63 WakeLockKey(int uid, String tag, int powerManagerWakeLockLevel) { 64 this.uid = uid; 65 this.tag = new String(tag); 66 this.powerManagerWakeLockLevel = powerManagerWakeLockLevel; 67 68 this.hashCode = Objects.hash(uid, tag, powerManagerWakeLockLevel); 69 } 70 getUid()71 int getUid() { 72 return uid; 73 } 74 getTag()75 String getTag() { 76 return tag; 77 } 78 getPowerManagerWakeLockLevel()79 int getPowerManagerWakeLockLevel() { 80 return powerManagerWakeLockLevel; 81 } 82 setOverflow()83 void setOverflow() { 84 tag = OVERFLOW_TAG; 85 powerManagerWakeLockLevel = OVERFLOW_LEVEL; 86 this.hashCode = Objects.hash(uid, tag, powerManagerWakeLockLevel); 87 } 88 setHardCap()89 void setHardCap() { 90 uid = HARD_CAP_UID; 91 tag = HARD_CAP_TAG; 92 powerManagerWakeLockLevel = OVERFLOW_LEVEL; 93 this.hashCode = Objects.hash(uid, tag, powerManagerWakeLockLevel); 94 } 95 96 @Override equals(Object o)97 public boolean equals(Object o) { 98 if (this == o) return true; 99 if (o == null || !(o instanceof WakeLockKey)) return false; 100 101 WakeLockKey that = (WakeLockKey) o; 102 return uid == that.uid 103 && tag.equals(that.tag) 104 && powerManagerWakeLockLevel == that.powerManagerWakeLockLevel; 105 } 106 107 @Override hashCode()108 public int hashCode() { 109 return this.hashCode; 110 } 111 } 112 113 private static class WakeLockStats { 114 // accumulated uptime attributed to this WakeLock since boot, where overlap 115 // (including nesting) is ignored 116 public long uptimeMillis = 0; 117 118 // count of WakeLocks that have been acquired and then released 119 public long completedCount = 0; 120 } 121 122 private final Object mLock = new Object(); 123 124 @GuardedBy("mLock") 125 private final Map<WakeLockKey, WakeLockStats> mWakeLockStats = new HashMap<>(); 126 127 private static class WakeLockData { 128 // uptime millis when first acquired 129 public long acquireUptimeMillis = 0; 130 public int refCount = 0; 131 WakeLockData(long uptimeMillis)132 WakeLockData(long uptimeMillis) { 133 acquireUptimeMillis = uptimeMillis; 134 } 135 } 136 137 @GuardedBy("mLock") 138 private final Map<WakeLockKey, WakeLockData> mOpenWakeLocks = new HashMap<>(); 139 noteStartWakeLock( int uid, String tag, int powerManagerWakeLockLevel, long eventUptimeMillis)140 public void noteStartWakeLock( 141 int uid, String tag, int powerManagerWakeLockLevel, long eventUptimeMillis) { 142 final WakeLockKey key = new WakeLockKey(uid, tag, powerManagerWakeLockLevel); 143 144 synchronized (mLock) { 145 WakeLockData data = 146 mOpenWakeLocks.computeIfAbsent(key, k -> new WakeLockData(eventUptimeMillis)); 147 data.refCount++; 148 mOpenWakeLocks.put(key, data); 149 } 150 } 151 152 @VisibleForTesting 153 @GuardedBy("mLock") inOverflow()154 public boolean inOverflow() { 155 return mWakeLockStats.size() >= SUMMARY_THRESHOLD; 156 } 157 158 @VisibleForTesting 159 @GuardedBy("mLock") inHardCap()160 public boolean inHardCap() { 161 return mWakeLockStats.size() >= MAX_WAKELOCK_DIMENSIONS; 162 } 163 noteStopWakeLock( int uid, String tag, int powerManagerWakeLockLevel, long eventUptimeMillis)164 public void noteStopWakeLock( 165 int uid, String tag, int powerManagerWakeLockLevel, long eventUptimeMillis) { 166 WakeLockKey key = new WakeLockKey(uid, tag, powerManagerWakeLockLevel); 167 168 synchronized (mLock) { 169 WakeLockData data = mOpenWakeLocks.get(key); 170 if (data == null) { 171 Log.e(TAG, "WakeLock not found when stopping: " + uid + " " + tag); 172 return; 173 } 174 175 if (data.refCount == 1) { 176 mOpenWakeLocks.remove(key); 177 long wakeLockDur = eventUptimeMillis - data.acquireUptimeMillis; 178 179 // Rewrite key if in an overflow state. 180 if (inOverflow() && !mWakeLockStats.containsKey(key)) { 181 key.setOverflow(); 182 if (inHardCap() && !mWakeLockStats.containsKey(key)) { 183 key.setHardCap(); 184 } 185 } 186 187 WakeLockStats stats = mWakeLockStats.computeIfAbsent(key, k -> new WakeLockStats()); 188 stats.uptimeMillis += wakeLockDur; 189 stats.completedCount++; 190 mWakeLockStats.put(key, stats); 191 } else { 192 data.refCount--; 193 mOpenWakeLocks.put(key, data); 194 } 195 } 196 } 197 198 // Shim interface for testing. 199 @VisibleForTesting 200 public interface EventLogger { logResult( int uid, String tag, int wakeLockLevel, long uptimeMillis, long completedCount)201 void logResult( 202 int uid, String tag, int wakeLockLevel, long uptimeMillis, long completedCount); 203 } 204 pullFrameworkWakelockInfoAtoms()205 public List<StatsEvent> pullFrameworkWakelockInfoAtoms() { 206 List<StatsEvent> result = new ArrayList<>(); 207 EventLogger logger = 208 new EventLogger() { 209 public void logResult( 210 int uid, 211 String tag, 212 int wakeLockLevel, 213 long uptimeMillis, 214 long completedCount) { 215 StatsEvent event = 216 StatsEvent.newBuilder() 217 .setAtomId(FrameworkStatsLog.FRAMEWORK_WAKELOCK_INFO) 218 .writeInt(uid) 219 .writeString(tag) 220 .writeInt(wakeLockLevel) 221 .writeLong(uptimeMillis) 222 .writeLong(completedCount) 223 .build(); 224 result.add(event); 225 } 226 }; 227 pullFrameworkWakelockInfoAtoms(SystemClock.uptimeMillis(), logger); 228 return result; 229 } 230 231 @VisibleForTesting pullFrameworkWakelockInfoAtoms(long nowMillis, EventLogger logger)232 public void pullFrameworkWakelockInfoAtoms(long nowMillis, EventLogger logger) { 233 HashSet<WakeLockKey> keys = new HashSet<>(); 234 235 // Used to collect open WakeLocks when in an overflow state. 236 HashMap<WakeLockKey, WakeLockStats> openOverflowStats = new HashMap<>(); 237 238 synchronized (mLock) { 239 keys.addAll(mWakeLockStats.keySet()); 240 241 // If we are in an overflow state, an open wakelock may have a new key 242 // that needs to be summarized. 243 if (inOverflow()) { 244 for (WakeLockKey key : mOpenWakeLocks.keySet()) { 245 if (!mWakeLockStats.containsKey(key)) { 246 WakeLockData data = mOpenWakeLocks.get(key); 247 248 key.setOverflow(); 249 if (inHardCap() && !mWakeLockStats.containsKey(key)) { 250 key.setHardCap(); 251 } 252 keys.add(key); 253 254 WakeLockStats stats = 255 openOverflowStats.computeIfAbsent(key, k -> new WakeLockStats()); 256 stats.uptimeMillis += nowMillis - data.acquireUptimeMillis; 257 openOverflowStats.put(key, stats); 258 } 259 } 260 } else { 261 keys.addAll(mOpenWakeLocks.keySet()); 262 } 263 264 for (WakeLockKey key : keys) { 265 long openWakeLockUptime = 0; 266 WakeLockData data = mOpenWakeLocks.get(key); 267 if (data != null) { 268 openWakeLockUptime = nowMillis - data.acquireUptimeMillis; 269 } 270 271 WakeLockStats stats = mWakeLockStats.computeIfAbsent(key, k -> new WakeLockStats()); 272 WakeLockStats extraTime = 273 openOverflowStats.computeIfAbsent(key, k -> new WakeLockStats()); 274 275 long totalUpdate = openWakeLockUptime + stats.uptimeMillis + extraTime.uptimeMillis; 276 long totalCount = stats.completedCount + extraTime.completedCount; 277 logger.logResult(key.getUid(), key.getTag(), key.getPowerManagerWakeLockLevel(), 278 totalUpdate, totalCount); 279 } 280 } 281 } 282 283 private static final String TAG = "BatteryStatsPulledMetrics"; 284 285 private final StatsPullCallbackHandler mStatsPullCallbackHandler = 286 new StatsPullCallbackHandler(); 287 288 private boolean mIsInitialized = false; 289 initialize(Context context)290 public void initialize(Context context) { 291 if (mIsInitialized) { 292 return; 293 } 294 295 final StatsManager statsManager = context.getSystemService(StatsManager.class); 296 if (statsManager == null) { 297 Log.e( 298 TAG, 299 "Error retrieving StatsManager. Cannot initialize BatteryStatsPulledMetrics."); 300 } else { 301 Log.d(TAG, "Registering callback with StatsManager"); 302 303 // DIRECT_EXECUTOR means that callback will run on binder thread. 304 statsManager.setPullAtomCallback( 305 FrameworkStatsLog.FRAMEWORK_WAKELOCK_INFO, 306 null /* metadata */, 307 ConcurrentUtils.DIRECT_EXECUTOR, 308 mStatsPullCallbackHandler); 309 mIsInitialized = true; 310 } 311 } 312 313 private class StatsPullCallbackHandler implements StatsManager.StatsPullAtomCallback { 314 @Override onPullAtom(int atomTag, List<StatsEvent> data)315 public int onPullAtom(int atomTag, List<StatsEvent> data) { 316 // handle the tags appropriately. 317 List<StatsEvent> events = pullEvents(atomTag); 318 if (events == null) { 319 return StatsManager.PULL_SKIP; 320 } 321 322 data.addAll(events); 323 return StatsManager.PULL_SUCCESS; 324 } 325 pullEvents(int atomTag)326 private List<StatsEvent> pullEvents(int atomTag) { 327 switch (atomTag) { 328 case FrameworkStatsLog.FRAMEWORK_WAKELOCK_INFO: 329 return pullFrameworkWakelockInfoAtoms(); 330 default: 331 return null; 332 } 333 } 334 } 335 } 336