1/*
2 * Copyright (C) 2018 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 */
16package com.android.server.power.batterysaver;
17
18import android.metrics.LogMaker;
19import android.os.BatteryManagerInternal;
20import android.os.SystemClock;
21import android.util.ArrayMap;
22import android.util.Slog;
23import android.util.TimeUtils;
24
25import com.android.internal.annotations.GuardedBy;
26import com.android.internal.annotations.VisibleForTesting;
27import com.android.internal.logging.MetricsLogger;
28import com.android.internal.logging.nano.MetricsProto;
29import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
30import com.android.server.EventLogTags;
31import com.android.server.LocalServices;
32import com.android.server.power.BatterySaverPolicy;
33
34import java.io.PrintWriter;
35import java.text.SimpleDateFormat;
36import java.util.Date;
37
38/**
39 * This class keeps track of battery drain rate.
40 *
41 * IMPORTANT: This class shares the power manager lock, which is very low in the lock hierarchy.
42 * Do not call out with the lock held. (Settings provider is okay.)
43 *
44 * TODO: The use of the terms "percent" and "level" in this class is not standard. Fix it.
45 *
46 * Test:
47 atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/power/batterysaver/BatterySavingStatsTest.java
48 */
49public class BatterySavingStats {
50
51    private static final String TAG = "BatterySavingStats";
52
53    private static final boolean DEBUG = BatterySaverPolicy.DEBUG;
54
55    private final Object mLock;
56
57    /** Whether battery saver is on or off. */
58    interface BatterySaverState {
59        int OFF = 0;
60        int ON = 1;
61
62        int SHIFT = 0;
63        int BITS = 1;
64        int MASK = (1 << BITS) - 1;
65
66        static int fromIndex(int index) {
67            return (index >> SHIFT) & MASK;
68        }
69    }
70
71    /** Whether the device is interactive (i.e. screen on) or not. */
72    interface InteractiveState {
73        int NON_INTERACTIVE = 0;
74        int INTERACTIVE = 1;
75
76        int SHIFT = BatterySaverState.SHIFT + BatterySaverState.BITS;
77        int BITS = 1;
78        int MASK = (1 << BITS) - 1;
79
80        static int fromIndex(int index) {
81            return (index >> SHIFT) & MASK;
82        }
83    }
84
85    /** Doze mode. */
86    interface DozeState {
87        int NOT_DOZING = 0;
88        int LIGHT = 1;
89        int DEEP = 2;
90
91        int SHIFT = InteractiveState.SHIFT + InteractiveState.BITS;
92        int BITS = 2;
93        int MASK = (1 << BITS) - 1;
94
95        static int fromIndex(int index) {
96            return (index >> SHIFT) & MASK;
97        }
98    }
99
100    /**
101     * Various stats in each state.
102     */
103    static class Stat {
104        public long startTime;
105        public long endTime;
106
107        public int startBatteryLevel;
108        public int endBatteryLevel;
109
110        public int startBatteryPercent;
111        public int endBatteryPercent;
112
113        public long totalTimeMillis;
114        public int totalBatteryDrain;
115        public int totalBatteryDrainPercent;
116
117        public long totalMinutes() {
118            return totalTimeMillis / 60_000;
119        }
120
121        public double drainPerHour() {
122            if (totalTimeMillis == 0) {
123                return 0;
124            }
125            return (double) totalBatteryDrain / (totalTimeMillis / (60.0 * 60 * 1000));
126        }
127
128        public double drainPercentPerHour() {
129            if (totalTimeMillis == 0) {
130                return 0;
131            }
132            return (double) totalBatteryDrainPercent / (totalTimeMillis / (60.0 * 60 * 1000));
133        }
134
135        @VisibleForTesting
136        String toStringForTest() {
137            return "{" + totalMinutes() + "m," + totalBatteryDrain + ","
138                    + String.format("%.2f", drainPerHour()) + "uA/H,"
139                    + String.format("%.2f", drainPercentPerHour()) + "%"
140                    + "}";
141        }
142    }
143
144    private BatteryManagerInternal mBatteryManagerInternal;
145    private final MetricsLogger mMetricsLogger;
146
147    private static final int STATE_NOT_INITIALIZED = -1;
148    private static final int STATE_CHARGING = -2;
149
150    /**
151     * Current state, one of STATE_* or values returned by {@link #statesToIndex}.
152     */
153    @GuardedBy("mLock")
154    private int mCurrentState = STATE_NOT_INITIALIZED;
155
156    /**
157     * Stats in each state.
158     */
159    @VisibleForTesting
160    @GuardedBy("mLock")
161    final ArrayMap<Integer, Stat> mStats = new ArrayMap<>();
162
163    @GuardedBy("mLock")
164    private int mBatterySaverEnabledCount = 0;
165
166    @GuardedBy("mLock")
167    private boolean mIsBatterySaverEnabled;
168
169    @GuardedBy("mLock")
170    private long mLastBatterySaverEnabledTime = 0;
171
172    @GuardedBy("mLock")
173    private long mLastBatterySaverDisabledTime = 0;
174
175    private final MetricsLoggerHelper mMetricsLoggerHelper = new MetricsLoggerHelper();
176
177    @VisibleForTesting
178    @GuardedBy("mLock")
179    private boolean mSendTronLog;
180
181    /** Visible for unit tests */
182    @VisibleForTesting
183    public BatterySavingStats(Object lock, MetricsLogger metricsLogger) {
184        mLock = lock;
185        mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
186        mMetricsLogger = metricsLogger;
187    }
188
189    public BatterySavingStats(Object lock) {
190        this(lock, new MetricsLogger());
191    }
192
193    public void setSendTronLog(boolean send) {
194        synchronized (mLock) {
195            mSendTronLog = send;
196        }
197    }
198
199    private BatteryManagerInternal getBatteryManagerInternal() {
200        if (mBatteryManagerInternal == null) {
201            mBatteryManagerInternal = LocalServices.getService(BatteryManagerInternal.class);
202            if (mBatteryManagerInternal == null) {
203                Slog.wtf(TAG, "BatteryManagerInternal not initialized");
204            }
205        }
206        return mBatteryManagerInternal;
207    }
208
209    /**
210     * Takes a state triplet and generates a state index.
211     */
212    @VisibleForTesting
213    static int statesToIndex(
214            int batterySaverState, int interactiveState, int dozeState) {
215        int ret = batterySaverState & BatterySaverState.MASK;
216        ret |= (interactiveState & InteractiveState.MASK) << InteractiveState.SHIFT;
217        ret |= (dozeState & DozeState.MASK) << DozeState.SHIFT;
218        return ret;
219    }
220
221    /**
222     * Takes a state index and returns a string for logging.
223     */
224    @VisibleForTesting
225    static String stateToString(int state) {
226        switch (state) {
227            case STATE_NOT_INITIALIZED:
228                return "NotInitialized";
229            case STATE_CHARGING:
230                return "Charging";
231        }
232        return "BS=" + BatterySaverState.fromIndex(state)
233                + ",I=" + InteractiveState.fromIndex(state)
234                + ",D=" + DozeState.fromIndex(state);
235    }
236
237    /**
238     * @return {@link Stat} fo a given state.
239     */
240    @VisibleForTesting
241    Stat getStat(int stateIndex) {
242        synchronized (mLock) {
243            Stat stat = mStats.get(stateIndex);
244            if (stat == null) {
245                stat = new Stat();
246                mStats.put(stateIndex, stat);
247            }
248            return stat;
249        }
250    }
251
252    /**
253     * @return {@link Stat} fo a given state triplet.
254     */
255    private Stat getStat(int batterySaverState, int interactiveState, int dozeState) {
256        return getStat(statesToIndex(batterySaverState, interactiveState, dozeState));
257    }
258
259    @VisibleForTesting
260    long injectCurrentTime() {
261        return SystemClock.elapsedRealtime();
262    }
263
264    @VisibleForTesting
265    int injectBatteryLevel() {
266        final BatteryManagerInternal bmi = getBatteryManagerInternal();
267        if (bmi == null) {
268            return 0;
269        }
270        return bmi.getBatteryChargeCounter();
271    }
272
273    @VisibleForTesting
274    int injectBatteryPercent() {
275        final BatteryManagerInternal bmi = getBatteryManagerInternal();
276        if (bmi == null) {
277            return 0;
278        }
279        return bmi.getBatteryLevel();
280    }
281
282    /**
283     * Called from the outside whenever any of the states changes, when the device is not plugged
284     * in.
285     */
286    public void transitionState(int batterySaverState, int interactiveState, int dozeState) {
287        synchronized (mLock) {
288            final int newState = statesToIndex(
289                    batterySaverState, interactiveState, dozeState);
290            transitionStateLocked(newState);
291        }
292    }
293
294    /**
295     * Called from the outside when the device is plugged in.
296     */
297    public void startCharging() {
298        synchronized (mLock) {
299            transitionStateLocked(STATE_CHARGING);
300        }
301    }
302
303    @GuardedBy("mLock")
304    private void transitionStateLocked(int newState) {
305        if (mCurrentState == newState) {
306            return;
307        }
308        final long now = injectCurrentTime();
309        final int batteryLevel = injectBatteryLevel();
310        final int batteryPercent = injectBatteryPercent();
311
312        final boolean oldBatterySaverEnabled =
313                BatterySaverState.fromIndex(mCurrentState) != BatterySaverState.OFF;
314        final boolean newBatterySaverEnabled =
315                BatterySaverState.fromIndex(newState) != BatterySaverState.OFF;
316        if (oldBatterySaverEnabled != newBatterySaverEnabled) {
317            mIsBatterySaverEnabled = newBatterySaverEnabled;
318            if (newBatterySaverEnabled) {
319                mBatterySaverEnabledCount++;
320                mLastBatterySaverEnabledTime = injectCurrentTime();
321            } else {
322                mLastBatterySaverDisabledTime = injectCurrentTime();
323            }
324        }
325
326        endLastStateLocked(now, batteryLevel, batteryPercent);
327        startNewStateLocked(newState, now, batteryLevel, batteryPercent);
328        mMetricsLoggerHelper.transitionStateLocked(newState, now, batteryLevel, batteryPercent);
329    }
330
331    @GuardedBy("mLock")
332    private void endLastStateLocked(long now, int batteryLevel, int batteryPercent) {
333        if (mCurrentState < 0) {
334            return;
335        }
336        final Stat stat = getStat(mCurrentState);
337
338        stat.endBatteryLevel = batteryLevel;
339        stat.endBatteryPercent = batteryPercent;
340        stat.endTime = now;
341
342        final long deltaTime = stat.endTime - stat.startTime;
343        final int deltaDrain = stat.startBatteryLevel - stat.endBatteryLevel;
344        final int deltaPercent = stat.startBatteryPercent - stat.endBatteryPercent;
345
346        stat.totalTimeMillis += deltaTime;
347        stat.totalBatteryDrain += deltaDrain;
348        stat.totalBatteryDrainPercent += deltaPercent;
349
350        if (DEBUG) {
351            Slog.d(TAG, "State summary: " + stateToString(mCurrentState)
352                    + ": " + (deltaTime / 1_000) + "s "
353                    + "Start level: " + stat.startBatteryLevel + "uA "
354                    + "End level: " + stat.endBatteryLevel + "uA "
355                    + "Start percent: " + stat.startBatteryPercent + "% "
356                    + "End percent: " + stat.endBatteryPercent + "% "
357                    + "Drain " + deltaDrain + "uA");
358        }
359        EventLogTags.writeBatterySavingStats(
360                BatterySaverState.fromIndex(mCurrentState),
361                InteractiveState.fromIndex(mCurrentState),
362                DozeState.fromIndex(mCurrentState),
363                deltaTime,
364                deltaDrain,
365                deltaPercent,
366                stat.totalTimeMillis,
367                stat.totalBatteryDrain,
368                stat.totalBatteryDrainPercent);
369
370    }
371
372    @GuardedBy("mLock")
373    private void startNewStateLocked(int newState, long now, int batteryLevel, int batteryPercent) {
374        if (DEBUG) {
375            Slog.d(TAG, "New state: " + stateToString(newState));
376        }
377        mCurrentState = newState;
378
379        if (mCurrentState < 0) {
380            return;
381        }
382
383        final Stat stat = getStat(mCurrentState);
384        stat.startBatteryLevel = batteryLevel;
385        stat.startBatteryPercent = batteryPercent;
386        stat.startTime = now;
387        stat.endTime = 0;
388    }
389
390    public void dump(PrintWriter pw, String indent) {
391        synchronized (mLock) {
392            pw.print(indent);
393            pw.println("Battery saving stats:");
394
395            indent = indent + "  ";
396
397            final long now = System.currentTimeMillis();
398            final long nowElapsed = injectCurrentTime();
399            final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
400
401            pw.print(indent);
402            pw.print("Battery Saver is currently: ");
403            pw.println(mIsBatterySaverEnabled ? "ON" : "OFF");
404            if (mLastBatterySaverEnabledTime > 0) {
405                pw.print(indent);
406                pw.print("  ");
407                pw.print("Last ON time: ");
408                pw.print(sdf.format(new Date(now - nowElapsed + mLastBatterySaverEnabledTime)));
409                pw.print(" ");
410                TimeUtils.formatDuration(mLastBatterySaverEnabledTime, nowElapsed, pw);
411                pw.println();
412            }
413
414            if (mLastBatterySaverDisabledTime > 0) {
415                pw.print(indent);
416                pw.print("  ");
417                pw.print("Last OFF time: ");
418                pw.print(sdf.format(new Date(now - nowElapsed + mLastBatterySaverDisabledTime)));
419                pw.print(" ");
420                TimeUtils.formatDuration(mLastBatterySaverDisabledTime, nowElapsed, pw);
421                pw.println();
422            }
423
424            pw.print(indent);
425            pw.print("  ");
426            pw.print("Times enabled: ");
427            pw.println(mBatterySaverEnabledCount);
428
429            pw.println();
430
431            pw.print(indent);
432            pw.println("Drain stats:");
433
434            pw.print(indent);
435            pw.println("                   Battery saver OFF                          ON");
436            dumpLineLocked(pw, indent, InteractiveState.NON_INTERACTIVE, "NonIntr",
437                    DozeState.NOT_DOZING, "NonDoze");
438            dumpLineLocked(pw, indent, InteractiveState.INTERACTIVE, "   Intr",
439                    DozeState.NOT_DOZING, "       ");
440
441            dumpLineLocked(pw, indent, InteractiveState.NON_INTERACTIVE, "NonIntr",
442                    DozeState.DEEP, "Deep   ");
443            dumpLineLocked(pw, indent, InteractiveState.INTERACTIVE, "   Intr",
444                    DozeState.DEEP, "       ");
445
446            dumpLineLocked(pw, indent, InteractiveState.NON_INTERACTIVE, "NonIntr",
447                    DozeState.LIGHT, "Light  ");
448            dumpLineLocked(pw, indent, InteractiveState.INTERACTIVE, "   Intr",
449                    DozeState.LIGHT, "       ");
450        }
451    }
452
453    private void dumpLineLocked(PrintWriter pw, String indent,
454            int interactiveState, String interactiveLabel,
455            int dozeState, String dozeLabel) {
456        pw.print(indent);
457        pw.print(dozeLabel);
458        pw.print(" ");
459        pw.print(interactiveLabel);
460        pw.print(": ");
461
462        final Stat offStat = getStat(BatterySaverState.OFF, interactiveState, dozeState);
463        final Stat onStat = getStat(BatterySaverState.ON, interactiveState, dozeState);
464
465        pw.println(String.format("%6dm %6dmAh(%3d%%) %8.1fmAh/h     %6dm %6dmAh(%3d%%) %8.1fmAh/h",
466                offStat.totalMinutes(),
467                offStat.totalBatteryDrain / 1000,
468                offStat.totalBatteryDrainPercent,
469                offStat.drainPerHour() / 1000.0,
470                onStat.totalMinutes(),
471                onStat.totalBatteryDrain / 1000,
472                onStat.totalBatteryDrainPercent,
473                onStat.drainPerHour() / 1000.0));
474    }
475
476    @VisibleForTesting
477    class MetricsLoggerHelper {
478        private int mLastState = STATE_NOT_INITIALIZED;
479        private long mStartTime;
480        private int mStartBatteryLevel;
481        private int mStartPercent;
482
483        private static final int STATE_CHANGE_DETECT_MASK =
484                (BatterySaverState.MASK << BatterySaverState.SHIFT) |
485                (InteractiveState.MASK << InteractiveState.SHIFT);
486
487        public void transitionStateLocked(
488                int newState, long now, int batteryLevel, int batteryPercent) {
489            final boolean stateChanging =
490                    ((mLastState >= 0) ^ (newState >= 0)) ||
491                    (((mLastState ^ newState) & STATE_CHANGE_DETECT_MASK) != 0);
492            if (stateChanging) {
493                if (mLastState >= 0) {
494                    final long deltaTime = now - mStartTime;
495
496                    reportLocked(mLastState, deltaTime, mStartBatteryLevel, mStartPercent,
497                            batteryLevel, batteryPercent);
498                }
499                mStartTime = now;
500                mStartBatteryLevel = batteryLevel;
501                mStartPercent = batteryPercent;
502            }
503            mLastState = newState;
504        }
505
506        void reportLocked(int state, long deltaTimeMs,
507                int startBatteryLevelUa, int startBatteryLevelPercent,
508                int endBatteryLevelUa, int endBatteryLevelPercent) {
509            if (!mSendTronLog) {
510                return;
511            }
512            final boolean batterySaverOn =
513                    BatterySaverState.fromIndex(state) != BatterySaverState.OFF;
514            final boolean interactive =
515                    InteractiveState.fromIndex(state) != InteractiveState.NON_INTERACTIVE;
516
517            final LogMaker logMaker = new LogMaker(MetricsProto.MetricsEvent.BATTERY_SAVER)
518                    .setSubtype(batterySaverOn ? 1 : 0)
519                    .addTaggedData(MetricsEvent.FIELD_INTERACTIVE, interactive ? 1 : 0)
520                    .addTaggedData(MetricsEvent.FIELD_DURATION_MILLIS, deltaTimeMs)
521                    .addTaggedData(MetricsEvent.FIELD_START_BATTERY_UA, startBatteryLevelUa)
522                    .addTaggedData(MetricsEvent.FIELD_START_BATTERY_PERCENT,
523                            startBatteryLevelPercent)
524                    .addTaggedData(MetricsEvent.FIELD_END_BATTERY_UA, endBatteryLevelUa)
525                    .addTaggedData(MetricsEvent.FIELD_END_BATTERY_PERCENT, endBatteryLevelPercent);
526
527            mMetricsLogger.write(logMaker);
528        }
529    }
530}
531