From a14e0f5e5095fde35dbaa7136f6c93a121c2cd69 Mon Sep 17 00:00:00 2001 From: Michael Wachenschwanz Date: Thu, 16 Feb 2023 01:59:48 -0800 Subject: [PATCH] Reset BatteryStats when device has been plugged in for a long time. BatteryStats continues to accumlate some stats while plugged in. The avoid overflows or excessive memory usage, occasionally reset while plugged in for extended periods of time. Bug: 269538224 Test: atest BatteryStatsResetTest Change-Id: If0db995ffaf4866bbadc828a0ade37a10da91553 --- .../server/am/BatteryStatsService.java | 45 ++++++- .../server/power/stats/BatteryStatsImpl.java | 114 +++++++++++++++++- .../power/stats/BatteryStatsResetTest.java | 64 ++++++++++ .../stats/BatteryUsageStatsProviderTest.java | 8 +- .../stats/BatteryUsageStatsStoreTest.java | 6 +- 5 files changed, 226 insertions(+), 11 deletions(-) diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 5e92a90aa97c2..1607566169077 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -29,6 +29,7 @@ import static android.os.BatteryStats.POWER_DATA_UNAVAILABLE; import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.RequiresNoPermission; +import android.app.AlarmManager; import android.app.StatsManager; import android.app.usage.NetworkStatsManager; import android.bluetooth.BluetoothActivityEnergyInfo; @@ -411,6 +412,18 @@ public final class BatteryStatsService extends IBatteryStats.Stub Slog.e(TAG, "Could not register INetworkManagement event observer " + e); } + final AlarmManager am = mContext.getSystemService(AlarmManager.class); + mHandler.post(() -> { + synchronized (mStats) { + mStats.setLongPlugInAlarmInterface(new AlarmInterface(am, () -> { + synchronized (mStats) { + if (mStats.isOnBattery()) return; + mStats.maybeResetWhilePluggedInLocked(); + } + })); + } + }); + synchronized (mPowerStatsLock) { mPowerStatsInternal = LocalServices.getService(PowerStatsInternal.class); if (mPowerStatsInternal != null) { @@ -2494,6 +2507,32 @@ public final class BatteryStatsService extends IBatteryStats.Stub } } + final class AlarmInterface implements BatteryStatsImpl.AlarmInterface, + AlarmManager.OnAlarmListener { + private AlarmManager mAm; + private Runnable mOnAlarm; + + AlarmInterface(AlarmManager am, Runnable onAlarm) { + mAm = am; + mOnAlarm = onAlarm; + } + + @Override + public void schedule(long rtcTimeMs, long windowLengthMs) { + mAm.setWindow(AlarmManager.RTC, rtcTimeMs, windowLengthMs, TAG, this, mHandler); + } + + @Override + public void cancel() { + mAm.cancel(this); + } + + @Override + public void onAlarm() { + mOnAlarm.run(); + } + } + private static native int nativeWaitWakeup(ByteBuffer outBuffer); private void dumpHelp(PrintWriter pw) { @@ -2694,7 +2733,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub } else if ("--reset-all".equals(arg)) { awaitCompletion(); synchronized (mStats) { - mStats.resetAllStatsCmdLocked(); + mStats.resetAllStatsAndHistoryLocked( + BatteryStatsImpl.RESET_REASON_ADB_COMMAND); mBatteryUsageStatsStore.removeAllSnapshots(); pw.println("Battery stats and history reset."); noOutput = true; @@ -2702,7 +2742,8 @@ public final class BatteryStatsService extends IBatteryStats.Stub } else if ("--reset".equals(arg)) { awaitCompletion(); synchronized (mStats) { - mStats.resetAllStatsCmdLocked(); + mStats.resetAllStatsAndHistoryLocked( + BatteryStatsImpl.RESET_REASON_ADB_COMMAND); pw.println("Battery stats reset."); noOutput = true; } diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 0ce126302bb61..3d3c5e26e4f7c 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -82,6 +82,7 @@ import android.telephony.ServiceState.RegState; import android.telephony.SignalStrength; import android.telephony.TelephonyManager; import android.text.TextUtils; +import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; @@ -212,6 +213,7 @@ public class BatteryStatsImpl extends BatteryStats { public static final int RESET_REASON_ADB_COMMAND = 2; public static final int RESET_REASON_FULL_CHARGE = 3; public static final int RESET_REASON_ENERGY_CONSUMER_BUCKETS_CHANGE = 4; + public static final int RESET_REASON_PLUGGED_IN_FOR_LONG_DURATION = 5; protected Clock mClock; @@ -498,6 +500,14 @@ public class BatteryStatsImpl extends BatteryStats { } + /** Handles calls to AlarmManager */ + public interface AlarmInterface { + /** Schedule an RTC alarm */ + void schedule(long rtcTimeMs, long windowLengthMs); + /** Cancel the previously scheduled alarm */ + void cancel(); + } + private final PlatformIdleStateCallback mPlatformIdleStateCallback; private final Runnable mDeferSetCharging = new Runnable() { @@ -848,6 +858,7 @@ public class BatteryStatsImpl extends BatteryStats { private boolean mHaveBatteryLevel = false; private boolean mBatteryPluggedIn; + private long mBatteryPluggedInRealTimeMs = 0; private int mBatteryStatus; private int mBatteryLevel; private int mBatteryPlugType; @@ -1558,6 +1569,9 @@ public class BatteryStatsImpl extends BatteryStats { @GuardedBy("this") protected BatteryStatsConfig mBatteryStatsConfig = new BatteryStatsConfig.Builder().build(); + @VisibleForTesting + protected AlarmInterface mLongPlugInAlarmInterface = null; + /* * Holds a SamplingTimer associated with each Resource Power Manager state and voter, * recording their times when on-battery (regardless of screen state). @@ -11042,6 +11056,18 @@ public class BatteryStatsImpl extends BatteryStats { } } + /** + * Injects an AlarmInterface for the long plug in alarm. + */ + public void setLongPlugInAlarmInterface(AlarmInterface longPlugInAlarmInterface) { + synchronized (this) { + mLongPlugInAlarmInterface = longPlugInAlarmInterface; + if (mBatteryPluggedIn) { + scheduleNextResetWhilePluggedInCheck(); + } + } + } + /** * Starts tracking CPU time-in-state for threads of the system server process, * keeping a separate account of threads receiving incoming binder calls. @@ -11475,12 +11501,12 @@ public class BatteryStatsImpl extends BatteryStats { } @GuardedBy("this") - public void resetAllStatsCmdLocked() { + public void resetAllStatsAndHistoryLocked(int reason) { final long mSecUptime = mClock.uptimeMillis(); long uptimeUs = mSecUptime * 1000; long mSecRealtime = mClock.elapsedRealtime(); long realtimeUs = mSecRealtime * 1000; - resetAllStatsLocked(mSecUptime, mSecRealtime, RESET_REASON_ADB_COMMAND); + resetAllStatsLocked(mSecUptime, mSecRealtime, reason); pullPendingStateUpdatesLocked(); mHistory.writeHistoryItem(mSecRealtime, mSecUptime); mDischargeCurrentLevel = mDischargeUnplugLevel = mDischargePlugLevel = mBatteryLevel; @@ -14140,6 +14166,73 @@ public class BatteryStatsImpl extends BatteryStats { mRecordAllHistory = true; } + /** + * Might reset battery stats if conditions are met. Assumed the device is currently plugged in. + */ + @GuardedBy("this") + public void maybeResetWhilePluggedInLocked() { + final long elapsedRealtimeMs = mClock.elapsedRealtime(); + if (shouldResetWhilePluggedInLocked(elapsedRealtimeMs)) { + Slog.i(TAG, + "Resetting due to long plug in duration. elapsed time = " + elapsedRealtimeMs + + " ms, last plug in time = " + mBatteryPluggedInRealTimeMs + + " ms, last reset time = " + mRealtimeStartUs / 1000); + resetAllStatsAndHistoryLocked(RESET_REASON_PLUGGED_IN_FOR_LONG_DURATION); + } + + scheduleNextResetWhilePluggedInCheck(); + } + + @GuardedBy("this") + private void scheduleNextResetWhilePluggedInCheck() { + if (mLongPlugInAlarmInterface != null) { + final long timeoutMs = mClock.currentTimeMillis() + + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS + * DateUtils.HOUR_IN_MILLIS; + Calendar nextAlarm = Calendar.getInstance(); + nextAlarm.setTimeInMillis(timeoutMs); + + // Find the 2 AM the same day as the end of the minimum duration. + // This logic does not handle a Daylight Savings transition, or a timezone change + // while the alarm has been set. The need to reset after a long period while plugged + // in is not strict enough to warrant a well architected out solution. + nextAlarm.set(Calendar.MILLISECOND, 0); + nextAlarm.set(Calendar.SECOND, 0); + nextAlarm.set(Calendar.MINUTE, 0); + nextAlarm.set(Calendar.HOUR_OF_DAY, 2); + long nextTimeMs = nextAlarm.getTimeInMillis(); + if (nextTimeMs < timeoutMs) { + // The 2AM on the day of the timeout, move on the next day. + nextTimeMs += DateUtils.DAY_IN_MILLIS; + } + mLongPlugInAlarmInterface.schedule(nextTimeMs, DateUtils.HOUR_IN_MILLIS); + } + } + + + @GuardedBy("this") + private boolean shouldResetWhilePluggedInLocked(long elapsedRealtimeMs) { + if (mNoAutoReset) return false; + if (!mSystemReady) return false; + if (!mHistory.isResetEnabled()) return false; + + final long pluggedInThresholdMs = mBatteryPluggedInRealTimeMs + + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS + * DateUtils.HOUR_IN_MILLIS; + if (elapsedRealtimeMs >= pluggedInThresholdMs) { + // The device has been plugged in for a long time. + final long resetThresholdMs = mRealtimeStartUs / 1000 + + mConstants.RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS + * DateUtils.HOUR_IN_MILLIS; + if (elapsedRealtimeMs >= resetThresholdMs) { + // And it has been a long time since the last reset. + return true; + } + } + + return false; + } + @GuardedBy("this") private boolean shouldResetOnUnplugLocked(int batteryStatus, int batteryLevel) { if (mNoAutoReset) return false; @@ -14242,6 +14335,9 @@ public class BatteryStatsImpl extends BatteryStats { initActiveHistoryEventsLocked(mSecRealtime, mSecUptime); } mBatteryPluggedIn = false; + if (mLongPlugInAlarmInterface != null) { + mLongPlugInAlarmInterface.cancel(); + } mHistory.recordBatteryState(mSecRealtime, mSecUptime, level, mBatteryPluggedIn); mDischargeCurrentLevel = mDischargeUnplugLevel = level; if (Display.isOnState(screenState)) { @@ -14265,6 +14361,7 @@ public class BatteryStatsImpl extends BatteryStats { mOnBattery = mOnBatteryInternal = false; pullPendingStateUpdatesLocked(); mBatteryPluggedIn = true; + mBatteryPluggedInRealTimeMs = mSecRealtime; mHistory.recordBatteryState(mSecRealtime, mSecUptime, level, mBatteryPluggedIn); mDischargeCurrentLevel = mDischargePlugLevel = level; if (level < mDischargeUnplugLevel) { @@ -14278,6 +14375,7 @@ public class BatteryStatsImpl extends BatteryStats { mMaxChargeStepLevel = level; mInitStepMode = mCurStepMode; mModStepMode = 0; + scheduleNextResetWhilePluggedInCheck(); } if (doWrite || (mLastWriteTimeMs + (60 * 1000)) < mSecRealtime) { if (mStatsFile != null && !mHistory.isReadOnly()) { @@ -15184,6 +15282,8 @@ public class BatteryStatsImpl extends BatteryStats { "per_uid_modem_power_model"; public static final String KEY_PHONE_ON_EXTERNAL_STATS_COLLECTION = "phone_on_external_stats_collection"; + public static final String KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS = + "reset_while_plugged_in_minimum_duration_hours"; public static final String PER_UID_MODEM_POWER_MODEL_MOBILE_RADIO_ACTIVE_TIME_NAME = "mobile_radio_active_time"; @@ -15233,6 +15333,8 @@ public class BatteryStatsImpl extends BatteryStats { private static final int DEFAULT_PER_UID_MODEM_MODEL = PER_UID_MODEM_POWER_MODEL_MODEM_ACTIVITY_INFO_RX_TX; private static final boolean DEFAULT_PHONE_ON_EXTERNAL_STATS_COLLECTION = true; + // Little less than 2 days + private static final int DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS = 47; public boolean TRACK_CPU_ACTIVE_CLUSTER_TIME = DEFAULT_TRACK_CPU_ACTIVE_CLUSTER_TIME; /* Do not set default value for KERNEL_UID_READERS_THROTTLE_TIME. Need to trigger an @@ -15252,6 +15354,8 @@ public class BatteryStatsImpl extends BatteryStats { public int PER_UID_MODEM_MODEL = DEFAULT_PER_UID_MODEM_MODEL; public boolean PHONE_ON_EXTERNAL_STATS_COLLECTION = DEFAULT_PHONE_ON_EXTERNAL_STATS_COLLECTION; + public int RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS = + DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS; private ContentResolver mResolver; private final KeyValueListParser mParser = new KeyValueListParser(','); @@ -15337,6 +15441,10 @@ public class BatteryStatsImpl extends BatteryStats { KEY_PHONE_ON_EXTERNAL_STATS_COLLECTION, DEFAULT_PHONE_ON_EXTERNAL_STATS_COLLECTION); + RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS = mParser.getInt( + KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS, + DEFAULT_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS); + updateBatteryChargedDelayMsLocked(); onChange(); @@ -15408,6 +15516,8 @@ public class BatteryStatsImpl extends BatteryStats { pw.println(getPerUidModemModelName(PER_UID_MODEM_MODEL)); pw.print(KEY_PHONE_ON_EXTERNAL_STATS_COLLECTION); pw.print("="); pw.println(PHONE_ON_EXTERNAL_STATS_COLLECTION); + pw.print(KEY_RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS); pw.print("="); + pw.println(RESET_WHILE_PLUGGED_IN_MINIMUM_DURATION_HOURS); } } diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsResetTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsResetTest.java index be7adea1bc265..a0fb631812f49 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsResetTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryStatsResetTest.java @@ -244,6 +244,70 @@ public class BatteryStatsResetTest { assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs); } + @Test + public void testResetWhilePluggedIn_longPlugIn() { + // disable high battery level reset on unplug. + mBatteryStatsImpl.setBatteryStatsConfig( + new BatteryStatsImpl.BatteryStatsConfig.Builder() + .setResetOnUnplugHighBatteryLevel(false) + .setResetOnUnplugAfterSignificantCharge(false) + .build()); + long expectedResetTimeUs = 0; + + plugBattery(BatteryManager.BATTERY_PLUGGED_USB); + mBatteryStatsImpl.maybeResetWhilePluggedInLocked(); + // Reset should not occur + assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs); + + // Increment time a day + incTimeMs(24L * 60L * 60L * 1000L); + mBatteryStatsImpl.maybeResetWhilePluggedInLocked(); + // Reset should still not occur + assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs); + + // Increment time a day + incTimeMs(24L * 60L * 60L * 1000L); + mBatteryStatsImpl.maybeResetWhilePluggedInLocked(); + // Reset 47 hour threshold crossed, reset should occur. + expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000; + assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs); + + // Increment time a day + incTimeMs(24L * 60L * 60L * 1000L); + mBatteryStatsImpl.maybeResetWhilePluggedInLocked(); + // Reset should not occur + assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs); + + // Increment time a day + incTimeMs(24L * 60L * 60L * 1000L); + mBatteryStatsImpl.maybeResetWhilePluggedInLocked(); + // Reset another 47 hour threshold crossed, reset should occur. + expectedResetTimeUs = mMockClock.elapsedRealtime() * 1000; + assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs); + + // Increment time a day + incTimeMs(24L * 60L * 60L * 1000L); + mBatteryStatsImpl.maybeResetWhilePluggedInLocked(); + // Reset should not occur + assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs); + + unplugBattery(); + plugBattery(BatteryManager.BATTERY_PLUGGED_USB); + + // Increment time a day + incTimeMs(24L * 60L * 60L * 1000L); + mBatteryStatsImpl.maybeResetWhilePluggedInLocked(); + // Reset should not occur, since unplug occurred recently. + assertThat(mBatteryStatsImpl.getStatsStartRealtime()).isEqualTo(expectedResetTimeUs); + + // Increment time a day + incTimeMs(24L * 60L * 60L * 1000L); + mBatteryStatsImpl.maybeResetWhilePluggedInLocked(); + // Reset another 47 hour threshold crossed, reset should occur. + 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--) { diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java index 2f64506f16a90..5df0acb65249e 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsProviderTest.java @@ -371,7 +371,7 @@ public class BatteryUsageStatsProviderTest { BatteryStatsImpl batteryStats = mStatsRule.getBatteryStats(); mStatsRule.setCurrentTime(5 * MINUTE_IN_MS); synchronized (batteryStats) { - batteryStats.resetAllStatsCmdLocked(); + batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND); } BatteryUsageStatsStore batteryUsageStatsStore = new BatteryUsageStatsStore(context, batteryStats, new File(context.getCacheDir(), "BatteryUsageStatsProviderTest"), @@ -391,7 +391,7 @@ public class BatteryUsageStatsProviderTest { } mStatsRule.setCurrentTime(25 * MINUTE_IN_MS); synchronized (batteryStats) { - batteryStats.resetAllStatsCmdLocked(); + batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND); } synchronized (batteryStats) { @@ -404,7 +404,7 @@ public class BatteryUsageStatsProviderTest { } mStatsRule.setCurrentTime(55 * MINUTE_IN_MS); synchronized (batteryStats) { - batteryStats.resetAllStatsCmdLocked(); + batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND); } // This section should be ignored because the timestamp is out or range @@ -418,7 +418,7 @@ public class BatteryUsageStatsProviderTest { } mStatsRule.setCurrentTime(75 * MINUTE_IN_MS); synchronized (batteryStats) { - batteryStats.resetAllStatsCmdLocked(); + batteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND); } // This section should be ignored because it represents the current stats session diff --git a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java index 970020f152bb4..b846e3a366568 100644 --- a/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java +++ b/services/tests/servicestests/src/com/android/server/power/stats/BatteryUsageStatsStoreTest.java @@ -86,7 +86,7 @@ public class BatteryUsageStatsStoreTest { mMockClock.realtime = 1_000_000; mMockClock.uptime = 1_000_000; - mBatteryStats.resetAllStatsCmdLocked(); + mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND); final long[] timestamps = mBatteryUsageStatsStore.listBatteryUsageStatsTimestamps(); assertThat(timestamps).hasLength(1); @@ -116,7 +116,7 @@ public class BatteryUsageStatsStoreTest { final int numberOfSnapshots = (int) (MAX_BATTERY_STATS_SNAPSHOT_STORAGE_BYTES / snapshotFileSize); for (int i = 0; i < numberOfSnapshots + 2; i++) { - mBatteryStats.resetAllStatsCmdLocked(); + mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND); mMockClock.realtime += 10_000_000; mMockClock.uptime += 10_000_000; @@ -143,7 +143,7 @@ public class BatteryUsageStatsStoreTest { mMockClock.currentTime += 10_000_000; prepareBatteryStats(); - mBatteryStats.resetAllStatsCmdLocked(); + mBatteryStats.resetAllStatsAndHistoryLocked(BatteryStatsImpl.RESET_REASON_ADB_COMMAND); } assertThat(getDirectorySize(mStoreDirectory)).isNotEqualTo(0);