Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9c630e36 authored by Michael Wachenschwanz's avatar Michael Wachenschwanz
Browse files

Add config to control BatteryStats reset logic

Also, fix the significant charge reset logic and add BatteryStats reset
tests.

Fix: 269538224
Bug: 256919205
Test: atest BatteryStatsResetTest
Change-Id: Id4a6dd22708ddee38dd6c10ceb6c81f6a7bfb44f
parent c01e1ce4
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -6322,4 +6322,9 @@

    <!-- Whether to show weather on the lock screen by default. -->
    <bool name="config_lockscreenWeatherEnabledByDefault">false</bool>

    <!-- Whether to reset Battery Stats on unplug when the battery level is high. -->
    <bool name="config_batteryStatsResetOnUnplugHighBatteryLevel">true</bool>
    <!-- Whether to reset Battery Stats on unplug if the battery was significantly charged -->
    <bool name="config_batteryStatsResetOnUnplugAfterSignificantCharge">true</bool>
</resources>
+3 −0
Original line number Diff line number Diff line
@@ -4960,4 +4960,7 @@

  <!-- Whether to show weather on the lockscreen by default. -->
  <java-symbol type="bool" name="config_lockscreenWeatherEnabledByDefault" />

  <java-symbol type="bool" name="config_batteryStatsResetOnUnplugHighBatteryLevel" />
  <java-symbol type="bool" name="config_batteryStatsResetOnUnplugAfterSignificantCharge" />
</resources>
+10 −0
Original line number Diff line number Diff line
@@ -370,6 +370,16 @@ public final class BatteryStatsService extends IBatteryStats.Stub
        mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger(
                com.android.internal.R.integer.config_radioScanningTimeout) * 1000L);
        mStats.setPowerProfileLocked(mPowerProfile);

        final boolean resetOnUnplugHighBatteryLevel = context.getResources().getBoolean(
                com.android.internal.R.bool.config_batteryStatsResetOnUnplugHighBatteryLevel);
        final boolean resetOnUnplugAfterSignificantCharge = context.getResources().getBoolean(
                com.android.internal.R.bool.config_batteryStatsResetOnUnplugAfterSignificantCharge);
        mStats.setBatteryStatsConfig(
                new BatteryStatsImpl.BatteryStatsConfig.Builder()
                        .setResetOnUnplugHighBatteryLevel(resetOnUnplugHighBatteryLevel)
                        .setResetOnUnplugAfterSignificantCharge(resetOnUnplugAfterSignificantCharge)
                        .build());
        mStats.startTrackingSystemServerCpuTime();

        if (BATTERY_USAGE_STORE_ENABLED) {
+119 −17
Original line number Diff line number Diff line
@@ -423,6 +423,81 @@ public class BatteryStatsImpl extends BatteryStats {
        }
    }
    /** Provide BatteryStatsImpl configuration choices */
    public static class BatteryStatsConfig {
        static final int RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG = 1 << 0;
        static final int RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG = 1 << 1;
        private final int mFlags;
        private BatteryStatsConfig(Builder builder) {
            int flags = 0;
            if (builder.mResetOnUnplugHighBatteryLevel) {
                flags |= RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG;
            }
            if (builder.mResetOnUnplugAfterSignificantCharge) {
                flags |= RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
            }
            mFlags = flags;
        }
        /**
         * Returns whether a BatteryStats reset should occur on unplug when the battery level is
         * high.
         */
        boolean shouldResetOnUnplugHighBatteryLevel() {
            return (mFlags & RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG)
                    == RESET_ON_UNPLUG_HIGH_BATTERY_LEVEL_FLAG;
        }
        /**
         * Returns whether a BatteryStats reset should occur on unplug if the battery charge a
         * significant amount since it has been plugged in.
         */
        boolean shouldResetOnUnplugAfterSignificantCharge() {
            return (mFlags & RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG)
                    == RESET_ON_UNPLUG_AFTER_SIGNIFICANT_CHARGE_FLAG;
        }
        /**
         * Builder for BatteryStatsConfig
         */
        public static class Builder {
            private boolean mResetOnUnplugHighBatteryLevel;
            private boolean mResetOnUnplugAfterSignificantCharge;
            public Builder() {
                mResetOnUnplugHighBatteryLevel = true;
                mResetOnUnplugAfterSignificantCharge = true;
            }
            /**
             * Build the BatteryStatsConfig.
             */
            public BatteryStatsConfig build() {
                return new BatteryStatsConfig(this);
            }
            /**
             * Set whether a BatteryStats reset should occur on unplug when the battery level is
             * high.
             */
            public Builder setResetOnUnplugHighBatteryLevel(boolean reset) {
                mResetOnUnplugHighBatteryLevel = reset;
                return this;
            }
            /**
             * Set whether a BatteryStats reset should occur on unplug if the battery charge a
             * significant amount since it has been plugged in.
             */
            public Builder setResetOnUnplugAfterSignificantCharge(boolean reset) {
                mResetOnUnplugAfterSignificantCharge = reset;
                return this;
            }
        }
    }
    private final PlatformIdleStateCallback mPlatformIdleStateCallback;
    private final Runnable mDeferSetCharging = new Runnable() {
@@ -1479,6 +1554,10 @@ public class BatteryStatsImpl extends BatteryStats {
    @GuardedBy("this")
    protected final Constants mConstants;
    @VisibleForTesting
    @GuardedBy("this")
    protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build();
    /*
     * Holds a SamplingTimer associated with each Resource Power Manager state and voter,
     * recording their times when on-battery (regardless of screen state).
@@ -1646,12 +1725,13 @@ public class BatteryStatsImpl extends BatteryStats {
        mHandler = null;
        mConstants = new Constants(mHandler);
        mStartClockTimeMs = clock.currentTimeMillis();
        mCheckinFile = null;
        mDailyFile = null;
        if (historyDirectory == null) {
            mCheckinFile = null;
            mStatsFile = null;
            mHistory = new BatteryStatsHistory(mStepDetailsCalculator, mClock);
        } else {
            mCheckinFile = new AtomicFile(new File(historyDirectory, "batterystats-checkin.bin"));
            mStatsFile = new AtomicFile(new File(historyDirectory, "batterystats.bin"));
            mHistory = new BatteryStatsHistory(historyDirectory, mConstants.MAX_HISTORY_FILES,
                    mConstants.MAX_HISTORY_BUFFER, mStepDetailsCalculator, mClock);
@@ -10953,6 +11033,15 @@ public class BatteryStatsImpl extends BatteryStats {
        return mPowerProfile;
    }
    /**
     * Injects BatteryStatsConfig
     */
    public void setBatteryStatsConfig(BatteryStatsConfig config) {
        synchronized (this) {
            mBatteryStatsConfig = config;
        }
    }
    /**
     * Starts tracking CPU time-in-state for threads of the system server process,
     * keeping a separate account of threads receiving incoming binder calls.
@@ -14051,6 +14140,33 @@ public class BatteryStatsImpl extends BatteryStats {
        mRecordAllHistory = true;
    }
    @GuardedBy("this")
    private boolean shouldResetOnUnplugLocked(int batteryStatus, int batteryLevel) {
        if (mNoAutoReset) return false;
        if (!mSystemReady) return false;
        if (!mHistory.isResetEnabled()) return false;
        if (mBatteryStatsConfig.shouldResetOnUnplugHighBatteryLevel()) {
            // Allow resetting due to currently being at high battery level
            if (batteryStatus == BatteryManager.BATTERY_STATUS_FULL) return true;
            if (batteryLevel >= 90) return true;
        }
        if (mBatteryStatsConfig.shouldResetOnUnplugAfterSignificantCharge()) {
            // Allow resetting after a significant charge (from a very low level to a now very
            // high level).
            if (mDischargePlugLevel < 20 && batteryLevel >= 80) return true;
        }
        if (getHighDischargeAmountSinceCharge() >= 200) {
            // Reset the stats if battery got partially charged and discharged repeatedly without
            // ever reaching the full charge.
            // This reset is done in order to prevent stats sessions from going on forever.
            // Exceedingly long battery sessions would lead to an overflow of
            // data structures such as mWakeupReasonStats.
            return true;
        }
        return false;
    }
    @GuardedBy("this")
    protected void setOnBatteryLocked(final long mSecRealtime, final long mSecUptime,
            final boolean onBattery, final int oldStatus, final int level, final int chargeUah) {
@@ -14063,24 +14179,10 @@ public class BatteryStatsImpl extends BatteryStats {
        final long realtimeUs = mSecRealtime * 1000;
        final int screenState = mScreenState;
        if (onBattery) {
            // We will reset our status if we are unplugging after the
            // battery was last full, or the level is at 100, or
            // we have gone through a significant charge (from a very low
            // level to a now very high level).
            // Also, we will reset the stats if battery got partially charged
            // and discharged repeatedly without ever reaching the full charge.
            // This reset is done in order to prevent stats sessions from going on forever.
            // Exceedingly long battery sessions would lead to an overflow of
            // data structures such as mWakeupReasonStats.
            boolean reset = false;
            if (!mNoAutoReset && mSystemReady
                    && (oldStatus == BatteryManager.BATTERY_STATUS_FULL
                    || level >= 90
                    || (mDischargeCurrentLevel < 20 && level >= 80)
                    || getHighDischargeAmountSinceCharge() >= 200)
                    && mHistory.isResetEnabled()) {
            if (shouldResetOnUnplugLocked(oldStatus, level)) {
                Slog.i(TAG, "Resetting battery stats: level=" + level + " status=" + oldStatus
                        + " dischargeLevel=" + mDischargeCurrentLevel
                        + " dischargeLevel=" + mDischargePlugLevel
                        + " lowAmount=" + getLowDischargeAmountSinceCharge()
                        + " highAmount=" + getHighDischargeAmountSinceCharge());
                // Before we write, collect a snapshot of the final aggregated
+296 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.power.stats;

import static com.google.common.truth.Truth.assertThat;

import android.content.Context;
import android.os.BatteryManager;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

@SmallTest
@RunWith(AndroidJUnit4.class)
public class BatteryStatsResetTest {

    private static final int BATTERY_NOMINAL_VOLTAGE_MV = 3700;
    private static final int BATTERY_CAPACITY_UAH = 4_000_000;
    private static final int BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL = 100;

    private MockClock mMockClock;
    private MockBatteryStatsImpl mBatteryStatsImpl;


    /**
     * Battery status. Must be one of the following:
     * {@link BatteryManager#BATTERY_STATUS_UNKNOWN}
     * {@link BatteryManager#BATTERY_STATUS_CHARGING}
     * {@link BatteryManager#BATTERY_STATUS_DISCHARGING}
     * {@link BatteryManager#BATTERY_STATUS_NOT_CHARGING}
     * {@link BatteryManager#BATTERY_STATUS_FULL}
     */
    private int mBatteryStatus;
    /**
     * Battery health. Must be one of the following:
     * {@link BatteryManager#BATTERY_HEALTH_UNKNOWN}
     * {@link BatteryManager#BATTERY_HEALTH_GOOD}
     * {@link BatteryManager#BATTERY_HEALTH_OVERHEAT}
     * {@link BatteryManager#BATTERY_HEALTH_DEAD}
     * {@link BatteryManager#BATTERY_HEALTH_OVER_VOLTAGE}
     * {@link BatteryManager#BATTERY_HEALTH_UNSPECIFIED_FAILURE}
     * {@link BatteryManager#BATTERY_HEALTH_COLD}
     */
    private int mBatteryHealth;
    /**
     * Battery plug type. Can be the union of any number of the following flags:
     * {@link BatteryManager#BATTERY_PLUGGED_AC}
     * {@link BatteryManager#BATTERY_PLUGGED_USB}
     * {@link BatteryManager#BATTERY_PLUGGED_WIRELESS}
     * {@link BatteryManager#BATTERY_PLUGGED_DOCK}
     *
     * Zero means the device is unplugged.
     */
    private int mBatteryPlugType;
    private int mBatteryLevel;
    private int mBatteryTemp;
    private int mBatteryVoltageMv;
    private int mBatteryChargeUah;
    private int mBatteryChargeFullUah;
    private long mBatteryChargeTimeToFullSeconds;

    @Before
    public void setUp() {
        final Context context = InstrumentationRegistry.getContext();

        mMockClock = new MockClock();
        mBatteryStatsImpl = new MockBatteryStatsImpl(mMockClock, context.getFilesDir());
        mBatteryStatsImpl.onSystemReady();


        // Set up the battery state. Start off with a fully charged plugged in battery.
        mBatteryStatus = BatteryManager.BATTERY_STATUS_FULL;
        mBatteryHealth = BatteryManager.BATTERY_HEALTH_GOOD;
        mBatteryPlugType = BatteryManager.BATTERY_PLUGGED_USB;
        mBatteryLevel = 100;
        mBatteryTemp = 70; // Arbitrary reasonable temperature.
        mBatteryVoltageMv = BATTERY_NOMINAL_VOLTAGE_MV;
        mBatteryChargeUah = BATTERY_CAPACITY_UAH;
        mBatteryChargeFullUah = BATTERY_CAPACITY_UAH;
        mBatteryChargeTimeToFullSeconds = 0;
    }

    @Test
    public void testResetOnUnplug_highBatteryLevel() {
        mBatteryStatsImpl.setBatteryStatsConfig(
                new BatteryStatsImpl.BatteryStatsConfig.Builder()
                        .setResetOnUnplugHighBatteryLevel(true)
                        .build());

        long expectedResetTimeUs = 0;

        unplugBattery();
        dischargeToLevel(60);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(80);
        unplugBattery();
        // Reset should not occur until battery level above 90.
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(95);
        // Reset should not occur until unplug.
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        unplugBattery();
        // Reset should occur on unplug now that battery level is high (>=90)
        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        // disable high battery level reset on unplug.
        mBatteryStatsImpl.setBatteryStatsConfig(
                new BatteryStatsImpl.BatteryStatsConfig.Builder()
                        .setResetOnUnplugHighBatteryLevel(false)
                        .build());

        dischargeToLevel(60);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(95);
        unplugBattery();
        // Reset should not occur since the high battery level logic has been disabled.
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
    }

    @Test
    public void testResetOnUnplug_significantCharge() {
        mBatteryStatsImpl.setBatteryStatsConfig(
                new BatteryStatsImpl.BatteryStatsConfig.Builder()
                        .setResetOnUnplugAfterSignificantCharge(true)
                        .build());
        long expectedResetTimeUs = 0;

        unplugBattery();
        // Battery level dropped below 20%.
        dischargeToLevel(15);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(50);
        unplugBattery();
        // Reset should not occur until battery level above 80
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(85);
        unplugBattery();
        // Reset should not occur because the charge session did not go from 20% to 80%
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        // Battery level dropped below 20%.
        dischargeToLevel(15);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(85);
        unplugBattery();
        // Reset should occur after significant charge amount.
        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        // disable reset on unplug after significant charge.
        mBatteryStatsImpl.setBatteryStatsConfig(
                new BatteryStatsImpl.BatteryStatsConfig.Builder()
                        .setResetOnUnplugAfterSignificantCharge(false)
                        .build());

        // Battery level dropped below 20%.
        dischargeToLevel(15);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(85);
        unplugBattery();
        // Reset should not occur after significant charge amount.
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
    }

    @Test
    public void testResetOnUnplug_manyPartialCharges() {
        long expectedResetTimeUs = 0;

        unplugBattery();
        // Cumulative battery discharged: 60%.
        dischargeToLevel(40);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(80);
        unplugBattery();
        // Reset should not occur
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        // Cumulative battery discharged: 100%.
        dischargeToLevel(40);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(80);
        unplugBattery();
        // Reset should not occur
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        // Cumulative battery discharged: 140%.
        dischargeToLevel(40);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(80);
        unplugBattery();
        // Reset should not occur
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        // Cumulative battery discharged: 180%.
        dischargeToLevel(40);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(80);
        unplugBattery();
        // Reset should not occur
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);

        // Cumulative battery discharged: 220%.
        dischargeToLevel(40);

        plugBattery(BatteryManager.BATTERY_PLUGGED_USB);
        chargeToLevel(80);
        unplugBattery();
        // Should reset after >200% of cumulative battery discharge
        expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000;
        assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs);
    }

    private void dischargeToLevel(int targetLevel) {
        mBatteryStatus = BatteryManager.BATTERY_STATUS_DISCHARGING;
        for (int level = mBatteryLevel - 1; level >= targetLevel; level--) {
            prepareBatteryLevel(level);
            incTimeMs(5000); // Arbitrary discharge rate.
            updateBatteryState();
        }
    }

    private void chargeToLevel(int targetLevel) {
        mBatteryStatus = BatteryManager.BATTERY_STATUS_CHARGING;
        for (int level = mBatteryLevel + 1; level <= targetLevel; level++) {
            if (level >= 100) mBatteryStatus = BatteryManager.BATTERY_STATUS_FULL;
            prepareBatteryLevel(level);
            incTimeMs(BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL * 1000);
            updateBatteryState();
        }
    }

    private void unplugBattery() {
        mBatteryPlugType = 0;
        updateBatteryState();
    }

    private void plugBattery(int type) {
        mBatteryPlugType |= type;
        updateBatteryState();
    }

    private void prepareBatteryLevel(int level) {
        mBatteryLevel = level;
        mBatteryChargeUah = mBatteryLevel * mBatteryChargeFullUah / 100;
        mBatteryChargeTimeToFullSeconds =
                (100 - mBatteryLevel) * BATTERY_CHARGE_RATE_SECONDS_PER_LEVEL;
    }

    private void incTimeMs(long milliseconds) {
        mMockClock.realtime += milliseconds;
        mMockClock.uptime += milliseconds / 2; // Arbitrary slower uptime accumulation
        mMockClock.currentTime += milliseconds;
    }

    private void updateBatteryState() {
        mBatteryStatsImpl.setBatteryStateLocked(mBatteryStatus, mBatteryHealth, mBatteryPlugType,
                mBatteryLevel, mBatteryTemp, mBatteryVoltageMv, mBatteryChargeUah,
                mBatteryChargeFullUah, mBatteryChargeTimeToFullSeconds,
                mMockClock.elapsedRealtime(), mMockClock.uptimeMillis(),
                mMockClock.currentTimeMillis());
    }
}