1 /* 2 * Copyright (C) 2022 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.tare; 18 19 import static android.text.format.DateUtils.HOUR_IN_MILLIS; 20 21 import static com.android.server.tare.EconomicPolicy.TYPE_ACTION; 22 import static com.android.server.tare.EconomicPolicy.TYPE_REGULATION; 23 import static com.android.server.tare.EconomicPolicy.TYPE_REWARD; 24 import static com.android.server.tare.EconomicPolicy.getEventType; 25 import static com.android.server.tare.TareUtils.cakeToString; 26 27 import android.annotation.NonNull; 28 import android.os.BatteryManagerInternal; 29 import android.os.RemoteException; 30 import android.util.IndentingPrintWriter; 31 import android.util.Log; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.internal.app.IBatteryStats; 35 import com.android.server.LocalServices; 36 import com.android.server.am.BatteryStatsService; 37 38 import java.util.ArrayList; 39 import java.util.List; 40 41 /** 42 * Responsible for maintaining statistics and analysis of TARE's performance. 43 */ 44 public class Analyst { 45 private static final String TAG = "TARE-" + Analyst.class.getSimpleName(); 46 private static final boolean DEBUG = InternalResourceService.DEBUG 47 || Log.isLoggable(TAG, Log.DEBUG); 48 49 private static final int NUM_PERIODS_TO_RETAIN = 8; 50 @VisibleForTesting 51 static final long MIN_REPORT_DURATION_FOR_RESET = 24 * HOUR_IN_MILLIS; 52 53 static final class Report { 54 /** How much the battery was discharged over the tracked period. */ 55 public int cumulativeBatteryDischarge = 0; 56 public int currentBatteryLevel = 0; 57 /** 58 * Profit from performing actions. This excludes special circumstances where we charge the 59 * app 60 * less than the action's CTP. 61 */ 62 public long cumulativeProfit = 0; 63 public int numProfitableActions = 0; 64 /** 65 * Losses from performing actions for special circumstances (eg. for a TOP app) where we 66 * charge 67 * the app less than the action's CTP. 68 */ 69 public long cumulativeLoss = 0; 70 public int numUnprofitableActions = 0; 71 /** 72 * The total number of rewards given to apps over this period. 73 */ 74 public long cumulativeRewards = 0; 75 public int numRewards = 0; 76 /** 77 * Regulations that increased an app's balance. 78 */ 79 public long cumulativePositiveRegulations = 0; 80 public int numPositiveRegulations = 0; 81 /** 82 * Regulations that decreased an app's balance. 83 */ 84 public long cumulativeNegativeRegulations = 0; 85 public int numNegativeRegulations = 0; 86 87 /** 88 * The approximate amount of time the screen has been off while on battery while this 89 * report has been active. 90 */ 91 public long screenOffDurationMs = 0; 92 /** 93 * The approximate amount of battery discharge while this report has been active. 94 */ 95 public long screenOffDischargeMah = 0; 96 /** The offset used to get the delta when polling the screen off time from BatteryStats. */ 97 private long bsScreenOffRealtimeBase = 0; 98 /** 99 * The offset used to get the delta when polling the screen off discharge from BatteryStats. 100 */ 101 private long bsScreenOffDischargeMahBase = 0; 102 clear()103 private void clear() { 104 cumulativeBatteryDischarge = 0; 105 currentBatteryLevel = 0; 106 cumulativeProfit = 0; 107 numProfitableActions = 0; 108 cumulativeLoss = 0; 109 numUnprofitableActions = 0; 110 cumulativeRewards = 0; 111 numRewards = 0; 112 cumulativePositiveRegulations = 0; 113 numPositiveRegulations = 0; 114 cumulativeNegativeRegulations = 0; 115 numNegativeRegulations = 0; 116 screenOffDurationMs = 0; 117 screenOffDischargeMah = 0; 118 bsScreenOffRealtimeBase = 0; 119 bsScreenOffDischargeMahBase = 0; 120 } 121 } 122 123 private final IBatteryStats mIBatteryStats; 124 125 private int mPeriodIndex = 0; 126 /** How much the battery was discharged over the tracked period. */ 127 private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN]; 128 Analyst()129 Analyst() { 130 this(BatteryStatsService.getService()); 131 } 132 Analyst(IBatteryStats iBatteryStats)133 @VisibleForTesting Analyst(IBatteryStats iBatteryStats) { 134 mIBatteryStats = iBatteryStats; 135 } 136 137 /** Returns the list of most recent reports, with the oldest report first. */ 138 @NonNull getReports()139 List<Report> getReports() { 140 final List<Report> list = new ArrayList<>(NUM_PERIODS_TO_RETAIN); 141 for (int i = 1; i <= NUM_PERIODS_TO_RETAIN; ++i) { 142 final int idx = (mPeriodIndex + i) % NUM_PERIODS_TO_RETAIN; 143 final Report report = mReports[idx]; 144 if (report != null) { 145 list.add(report); 146 } 147 } 148 return list; 149 } 150 getBatteryScreenOffDischargeMah()151 long getBatteryScreenOffDischargeMah() { 152 long discharge = 0; 153 for (Report report : mReports) { 154 if (report == null) { 155 continue; 156 } 157 discharge += report.screenOffDischargeMah; 158 } 159 return discharge; 160 } 161 getBatteryScreenOffDurationMs()162 long getBatteryScreenOffDurationMs() { 163 long duration = 0; 164 for (Report report : mReports) { 165 if (report == null) { 166 continue; 167 } 168 duration += report.screenOffDurationMs; 169 } 170 return duration; 171 } 172 173 /** 174 * Tracks the given reports instead of whatever is currently saved. Reports should be ordered 175 * oldest to most recent. 176 */ loadReports(@onNull List<Report> reports)177 void loadReports(@NonNull List<Report> reports) { 178 final int numReports = reports.size(); 179 mPeriodIndex = Math.max(0, Math.min(NUM_PERIODS_TO_RETAIN, numReports) - 1); 180 for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) { 181 if (i < numReports) { 182 mReports[i] = reports.get(i); 183 } else { 184 mReports[i] = null; 185 } 186 } 187 final Report latest = mReports[mPeriodIndex]; 188 if (latest != null) { 189 latest.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs(); 190 latest.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah(); 191 } 192 } 193 noteBatteryLevelChange(int newBatteryLevel)194 void noteBatteryLevelChange(int newBatteryLevel) { 195 final boolean deviceDischargedEnough = mReports[mPeriodIndex] != null 196 && newBatteryLevel >= 90 197 // Battery level is increasing, so device is charging. 198 && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel 199 && mReports[mPeriodIndex].cumulativeBatteryDischarge >= 25; 200 final boolean reportLongEnough = mReports[mPeriodIndex] != null 201 // Battery level is increasing, so device is charging. 202 && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel 203 && mReports[mPeriodIndex].screenOffDurationMs >= MIN_REPORT_DURATION_FOR_RESET; 204 final boolean shouldStartNewReport = deviceDischargedEnough || reportLongEnough; 205 if (shouldStartNewReport) { 206 mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN; 207 if (mReports[mPeriodIndex] != null) { 208 final Report report = mReports[mPeriodIndex]; 209 report.clear(); 210 report.currentBatteryLevel = newBatteryLevel; 211 report.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs(); 212 report.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah(); 213 return; 214 } 215 } 216 217 if (mReports[mPeriodIndex] == null) { 218 Report report = initializeReport(); 219 mReports[mPeriodIndex] = report; 220 report.currentBatteryLevel = newBatteryLevel; 221 return; 222 } 223 224 final Report report = mReports[mPeriodIndex]; 225 if (newBatteryLevel < report.currentBatteryLevel) { 226 report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel); 227 228 final long latestScreenOffRealtime = getLatestBatteryScreenOffRealtimeMs(); 229 final long latestScreenOffDischargeMah = getLatestScreenOffDischargeMah(); 230 if (report.bsScreenOffRealtimeBase > latestScreenOffRealtime) { 231 // BatteryStats reset 232 report.bsScreenOffRealtimeBase = 0; 233 report.bsScreenOffDischargeMahBase = 0; 234 } 235 report.screenOffDurationMs += 236 (latestScreenOffRealtime - report.bsScreenOffRealtimeBase); 237 report.screenOffDischargeMah += 238 (latestScreenOffDischargeMah - report.bsScreenOffDischargeMahBase); 239 report.bsScreenOffRealtimeBase = latestScreenOffRealtime; 240 report.bsScreenOffDischargeMahBase = latestScreenOffDischargeMah; 241 } 242 report.currentBatteryLevel = newBatteryLevel; 243 } 244 noteTransaction(@onNull Ledger.Transaction transaction)245 void noteTransaction(@NonNull Ledger.Transaction transaction) { 246 if (mReports[mPeriodIndex] == null) { 247 mReports[mPeriodIndex] = initializeReport(); 248 } 249 final Report report = mReports[mPeriodIndex]; 250 switch (getEventType(transaction.eventId)) { 251 case TYPE_ACTION: 252 // For now, assume all instances where price < CTP is a special instance. 253 // TODO: add an explicit signal for special circumstances 254 if (-transaction.delta > transaction.ctp) { 255 report.cumulativeProfit += (-transaction.delta - transaction.ctp); 256 report.numProfitableActions++; 257 } else if (-transaction.delta < transaction.ctp) { 258 report.cumulativeLoss += (transaction.ctp + transaction.delta); 259 report.numUnprofitableActions++; 260 } 261 break; 262 case TYPE_REGULATION: 263 if (transaction.delta > 0) { 264 report.cumulativePositiveRegulations += transaction.delta; 265 report.numPositiveRegulations++; 266 } else if (transaction.delta < 0) { 267 report.cumulativeNegativeRegulations -= transaction.delta; 268 report.numNegativeRegulations++; 269 } 270 break; 271 case TYPE_REWARD: 272 if (transaction.delta != 0) { 273 report.cumulativeRewards += transaction.delta; 274 report.numRewards++; 275 } 276 break; 277 } 278 } 279 tearDown()280 void tearDown() { 281 for (int i = 0; i < mReports.length; ++i) { 282 mReports[i] = null; 283 } 284 mPeriodIndex = 0; 285 } 286 getLatestBatteryScreenOffRealtimeMs()287 private long getLatestBatteryScreenOffRealtimeMs() { 288 try { 289 return mIBatteryStats.computeBatteryScreenOffRealtimeMs(); 290 } catch (RemoteException e) { 291 // Shouldn't happen 292 return 0; 293 } 294 } 295 getLatestScreenOffDischargeMah()296 private long getLatestScreenOffDischargeMah() { 297 try { 298 return mIBatteryStats.getScreenOffDischargeMah(); 299 } catch (RemoteException e) { 300 // Shouldn't happen 301 return 0; 302 } 303 } 304 305 @NonNull initializeReport()306 private Report initializeReport() { 307 final Report report = new Report(); 308 report.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs(); 309 report.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah(); 310 return report; 311 } 312 313 @NonNull padStringWithSpaces(@onNull String text, int targetLength)314 private String padStringWithSpaces(@NonNull String text, int targetLength) { 315 // Make sure to have at least one space on either side. 316 final int padding = Math.max(2, targetLength - text.length()) >>> 1; 317 return " ".repeat(padding) + text + " ".repeat(padding); 318 } 319 dump(IndentingPrintWriter pw)320 void dump(IndentingPrintWriter pw) { 321 final BatteryManagerInternal bmi = LocalServices.getService(BatteryManagerInternal.class); 322 final long batteryCapacityMah = bmi.getBatteryFullCharge() / 1000; 323 pw.println("Reports:"); 324 pw.increaseIndent(); 325 pw.print(" Total Discharge"); 326 final int statColsLength = 47; 327 pw.print(padStringWithSpaces("Profit (avg/action : avg/discharge)", statColsLength)); 328 pw.print(padStringWithSpaces("Loss (avg/action : avg/discharge)", statColsLength)); 329 pw.print(padStringWithSpaces("Rewards (avg/reward : avg/discharge)", statColsLength)); 330 pw.print(padStringWithSpaces("+Regs (avg/reg : avg/discharge)", statColsLength)); 331 pw.print(padStringWithSpaces("-Regs (avg/reg : avg/discharge)", statColsLength)); 332 pw.print(padStringWithSpaces("Bg drain estimate", statColsLength)); 333 pw.println(); 334 for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) { 335 final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN; 336 final Report report = mReports[idx]; 337 if (report == null) { 338 continue; 339 } 340 pw.print("t-"); 341 pw.print(r); 342 pw.print(": "); 343 pw.print(padStringWithSpaces(Integer.toString(report.cumulativeBatteryDischarge), 15)); 344 if (report.numProfitableActions > 0) { 345 final String perDischarge = report.cumulativeBatteryDischarge > 0 346 ? cakeToString(report.cumulativeProfit / report.cumulativeBatteryDischarge) 347 : "N/A"; 348 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 349 cakeToString(report.cumulativeProfit), 350 cakeToString(report.cumulativeProfit / report.numProfitableActions), 351 perDischarge), 352 statColsLength)); 353 } else { 354 pw.print(padStringWithSpaces("N/A", statColsLength)); 355 } 356 if (report.numUnprofitableActions > 0) { 357 final String perDischarge = report.cumulativeBatteryDischarge > 0 358 ? cakeToString(report.cumulativeLoss / report.cumulativeBatteryDischarge) 359 : "N/A"; 360 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 361 cakeToString(report.cumulativeLoss), 362 cakeToString(report.cumulativeLoss / report.numUnprofitableActions), 363 perDischarge), 364 statColsLength)); 365 } else { 366 pw.print(padStringWithSpaces("N/A", statColsLength)); 367 } 368 if (report.numRewards > 0) { 369 final String perDischarge = report.cumulativeBatteryDischarge > 0 370 ? cakeToString(report.cumulativeRewards / report.cumulativeBatteryDischarge) 371 : "N/A"; 372 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 373 cakeToString(report.cumulativeRewards), 374 cakeToString(report.cumulativeRewards / report.numRewards), 375 perDischarge), 376 statColsLength)); 377 } else { 378 pw.print(padStringWithSpaces("N/A", statColsLength)); 379 } 380 if (report.numPositiveRegulations > 0) { 381 final String perDischarge = report.cumulativeBatteryDischarge > 0 382 ? cakeToString( 383 report.cumulativePositiveRegulations / report.cumulativeBatteryDischarge) 384 : "N/A"; 385 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 386 cakeToString(report.cumulativePositiveRegulations), 387 cakeToString(report.cumulativePositiveRegulations 388 / report.numPositiveRegulations), 389 perDischarge), 390 statColsLength)); 391 } else { 392 pw.print(padStringWithSpaces("N/A", statColsLength)); 393 } 394 if (report.numNegativeRegulations > 0) { 395 final String perDischarge = report.cumulativeBatteryDischarge > 0 396 ? cakeToString( 397 report.cumulativeNegativeRegulations / report.cumulativeBatteryDischarge) 398 : "N/A"; 399 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 400 cakeToString(report.cumulativeNegativeRegulations), 401 cakeToString(report.cumulativeNegativeRegulations 402 / report.numNegativeRegulations), 403 perDischarge), 404 statColsLength)); 405 } else { 406 pw.print(padStringWithSpaces("N/A", statColsLength)); 407 } 408 if (report.screenOffDurationMs > 0) { 409 pw.print(padStringWithSpaces(String.format("%d mAh (%.2f%%/hr)", 410 report.screenOffDischargeMah, 411 100.0 * report.screenOffDischargeMah * HOUR_IN_MILLIS 412 / (batteryCapacityMah * report.screenOffDurationMs)), 413 statColsLength)); 414 } else { 415 pw.print(padStringWithSpaces("N/A", statColsLength)); 416 } 417 pw.println(); 418 } 419 pw.decreaseIndent(); 420 } 421 } 422