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 com.android.server.tare.EconomicPolicy.TYPE_ACTION; 20 import static com.android.server.tare.EconomicPolicy.TYPE_REGULATION; 21 import static com.android.server.tare.EconomicPolicy.TYPE_REWARD; 22 import static com.android.server.tare.EconomicPolicy.getEventType; 23 import static com.android.server.tare.TareUtils.cakeToString; 24 25 import android.annotation.NonNull; 26 import android.util.IndentingPrintWriter; 27 import android.util.Log; 28 29 import java.util.ArrayList; 30 import java.util.List; 31 32 /** 33 * Responsible for maintaining statistics and analysis of TARE's performance. 34 */ 35 public class Analyst { 36 private static final String TAG = "TARE-" + Analyst.class.getSimpleName(); 37 private static final boolean DEBUG = InternalResourceService.DEBUG 38 || Log.isLoggable(TAG, Log.DEBUG); 39 40 private static final int NUM_PERIODS_TO_RETAIN = 8; 41 42 static final class Report { 43 /** How much the battery was discharged over the tracked period. */ 44 public int cumulativeBatteryDischarge = 0; 45 public int currentBatteryLevel = 0; 46 /** 47 * Profit from performing actions. This excludes special circumstances where we charge the 48 * app 49 * less than the action's CTP. 50 */ 51 public long cumulativeProfit = 0; 52 public int numProfitableActions = 0; 53 /** 54 * Losses from performing actions for special circumstances (eg. for a TOP app) where we 55 * charge 56 * the app less than the action's CTP. 57 */ 58 public long cumulativeLoss = 0; 59 public int numUnprofitableActions = 0; 60 /** 61 * The total number of rewards given to apps over this period. 62 */ 63 public long cumulativeRewards = 0; 64 public int numRewards = 0; 65 /** 66 * Regulations that increased an app's balance. 67 */ 68 public long cumulativePositiveRegulations = 0; 69 public int numPositiveRegulations = 0; 70 /** 71 * Regulations that decreased an app's balance. 72 */ 73 public long cumulativeNegativeRegulations = 0; 74 public int numNegativeRegulations = 0; 75 clear()76 private void clear() { 77 cumulativeBatteryDischarge = 0; 78 currentBatteryLevel = 0; 79 cumulativeProfit = 0; 80 numProfitableActions = 0; 81 cumulativeLoss = 0; 82 numUnprofitableActions = 0; 83 cumulativeRewards = 0; 84 numRewards = 0; 85 cumulativePositiveRegulations = 0; 86 numPositiveRegulations = 0; 87 cumulativeNegativeRegulations = 0; 88 numNegativeRegulations = 0; 89 } 90 } 91 92 private int mPeriodIndex = 0; 93 /** How much the battery was discharged over the tracked period. */ 94 private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN]; 95 96 /** Returns the list of most recent reports, with the oldest report first. */ 97 @NonNull getReports()98 List<Report> getReports() { 99 final List<Report> list = new ArrayList<>(NUM_PERIODS_TO_RETAIN); 100 for (int i = 1; i <= NUM_PERIODS_TO_RETAIN; ++i) { 101 final int idx = (mPeriodIndex + i) % NUM_PERIODS_TO_RETAIN; 102 final Report report = mReports[idx]; 103 if (report != null) { 104 list.add(report); 105 } 106 } 107 return list; 108 } 109 110 /** 111 * Tracks the given reports instead of whatever is currently saved. Reports should be ordered 112 * oldest to most recent. 113 */ loadReports(@onNull List<Report> reports)114 void loadReports(@NonNull List<Report> reports) { 115 final int numReports = reports.size(); 116 mPeriodIndex = Math.max(0, numReports - 1); 117 for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) { 118 if (i < numReports) { 119 mReports[i] = reports.get(i); 120 } else { 121 mReports[i] = null; 122 } 123 } 124 } 125 noteBatteryLevelChange(int newBatteryLevel)126 void noteBatteryLevelChange(int newBatteryLevel) { 127 if (newBatteryLevel == 100 && mReports[mPeriodIndex] != null 128 && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel) { 129 mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN; 130 if (mReports[mPeriodIndex] != null) { 131 final Report report = mReports[mPeriodIndex]; 132 report.clear(); 133 report.currentBatteryLevel = newBatteryLevel; 134 return; 135 } 136 } 137 138 if (mReports[mPeriodIndex] == null) { 139 Report report = new Report(); 140 mReports[mPeriodIndex] = report; 141 report.currentBatteryLevel = newBatteryLevel; 142 return; 143 } 144 145 final Report report = mReports[mPeriodIndex]; 146 if (newBatteryLevel < report.currentBatteryLevel) { 147 report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel); 148 } 149 report.currentBatteryLevel = newBatteryLevel; 150 } 151 noteTransaction(@onNull Ledger.Transaction transaction)152 void noteTransaction(@NonNull Ledger.Transaction transaction) { 153 if (mReports[mPeriodIndex] == null) { 154 mReports[mPeriodIndex] = new Report(); 155 } 156 final Report report = mReports[mPeriodIndex]; 157 switch (getEventType(transaction.eventId)) { 158 case TYPE_ACTION: 159 // For now, assume all instances where price < CTP is a special instance. 160 // TODO: add an explicit signal for special circumstances 161 if (-transaction.delta > transaction.ctp) { 162 report.cumulativeProfit += (-transaction.delta - transaction.ctp); 163 report.numProfitableActions++; 164 } else if (-transaction.delta < transaction.ctp) { 165 report.cumulativeLoss += (transaction.ctp + transaction.delta); 166 report.numUnprofitableActions++; 167 } 168 break; 169 case TYPE_REGULATION: 170 if (transaction.delta > 0) { 171 report.cumulativePositiveRegulations += transaction.delta; 172 report.numPositiveRegulations++; 173 } else if (transaction.delta < 0) { 174 report.cumulativeNegativeRegulations -= transaction.delta; 175 report.numNegativeRegulations++; 176 } 177 break; 178 case TYPE_REWARD: 179 if (transaction.delta != 0) { 180 report.cumulativeRewards += transaction.delta; 181 report.numRewards++; 182 } 183 break; 184 } 185 } 186 tearDown()187 void tearDown() { 188 for (int i = 0; i < mReports.length; ++i) { 189 mReports[i] = null; 190 } 191 mPeriodIndex = 0; 192 } 193 194 @NonNull padStringWithSpaces(@onNull String text, int targetLength)195 private String padStringWithSpaces(@NonNull String text, int targetLength) { 196 // Make sure to have at least one space on either side. 197 final int padding = Math.max(2, targetLength - text.length()) >>> 1; 198 return " ".repeat(padding) + text + " ".repeat(padding); 199 } 200 dump(IndentingPrintWriter pw)201 void dump(IndentingPrintWriter pw) { 202 pw.println("Reports:"); 203 pw.increaseIndent(); 204 pw.print(" Total Discharge"); 205 final int statColsLength = 47; 206 pw.print(padStringWithSpaces("Profit (avg/action : avg/discharge)", statColsLength)); 207 pw.print(padStringWithSpaces("Loss (avg/action : avg/discharge)", statColsLength)); 208 pw.print(padStringWithSpaces("Rewards (avg/reward : avg/discharge)", statColsLength)); 209 pw.print(padStringWithSpaces("+Regs (avg/reg : avg/discharge)", statColsLength)); 210 pw.print(padStringWithSpaces("-Regs (avg/reg : avg/discharge)", statColsLength)); 211 pw.println(); 212 for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) { 213 final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN; 214 final Report report = mReports[idx]; 215 if (report == null) { 216 continue; 217 } 218 pw.print("t-"); 219 pw.print(r); 220 pw.print(": "); 221 pw.print(padStringWithSpaces(Integer.toString(report.cumulativeBatteryDischarge), 15)); 222 if (report.numProfitableActions > 0) { 223 final String perDischarge = report.cumulativeBatteryDischarge > 0 224 ? cakeToString(report.cumulativeProfit / report.cumulativeBatteryDischarge) 225 : "N/A"; 226 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 227 cakeToString(report.cumulativeProfit), 228 cakeToString(report.cumulativeProfit / report.numProfitableActions), 229 perDischarge), 230 statColsLength)); 231 } else { 232 pw.print(padStringWithSpaces("N/A", statColsLength)); 233 } 234 if (report.numUnprofitableActions > 0) { 235 final String perDischarge = report.cumulativeBatteryDischarge > 0 236 ? cakeToString(report.cumulativeLoss / report.cumulativeBatteryDischarge) 237 : "N/A"; 238 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 239 cakeToString(report.cumulativeLoss), 240 cakeToString(report.cumulativeLoss / report.numUnprofitableActions), 241 perDischarge), 242 statColsLength)); 243 } else { 244 pw.print(padStringWithSpaces("N/A", statColsLength)); 245 } 246 if (report.numRewards > 0) { 247 final String perDischarge = report.cumulativeBatteryDischarge > 0 248 ? cakeToString(report.cumulativeRewards / report.cumulativeBatteryDischarge) 249 : "N/A"; 250 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 251 cakeToString(report.cumulativeRewards), 252 cakeToString(report.cumulativeRewards / report.numRewards), 253 perDischarge), 254 statColsLength)); 255 } else { 256 pw.print(padStringWithSpaces("N/A", statColsLength)); 257 } 258 if (report.numPositiveRegulations > 0) { 259 final String perDischarge = report.cumulativeBatteryDischarge > 0 260 ? cakeToString( 261 report.cumulativePositiveRegulations / report.cumulativeBatteryDischarge) 262 : "N/A"; 263 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 264 cakeToString(report.cumulativePositiveRegulations), 265 cakeToString(report.cumulativePositiveRegulations 266 / report.numPositiveRegulations), 267 perDischarge), 268 statColsLength)); 269 } else { 270 pw.print(padStringWithSpaces("N/A", statColsLength)); 271 } 272 if (report.numNegativeRegulations > 0) { 273 final String perDischarge = report.cumulativeBatteryDischarge > 0 274 ? cakeToString( 275 report.cumulativeNegativeRegulations / report.cumulativeBatteryDischarge) 276 : "N/A"; 277 pw.print(padStringWithSpaces(String.format("%s (%s : %s)", 278 cakeToString(report.cumulativeNegativeRegulations), 279 cakeToString(report.cumulativeNegativeRegulations 280 / report.numNegativeRegulations), 281 perDischarge), 282 statColsLength)); 283 } else { 284 pw.print(padStringWithSpaces("N/A", statColsLength)); 285 } 286 pw.println(); 287 } 288 pw.decreaseIndent(); 289 } 290 } 291