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