• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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