diff options
8 files changed, 356 insertions, 20 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java index bc6fe7e5a535..f27da4a1a4b2 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java @@ -16,6 +16,8 @@ package com.android.server.tare; +import static android.text.format.DateUtils.HOUR_IN_MILLIS; + import static com.android.server.tare.EconomicPolicy.TYPE_ACTION; import static com.android.server.tare.EconomicPolicy.TYPE_REGULATION; import static com.android.server.tare.EconomicPolicy.TYPE_REWARD; @@ -23,9 +25,16 @@ import static com.android.server.tare.EconomicPolicy.getEventType; import static com.android.server.tare.TareUtils.cakeToString; import android.annotation.NonNull; +import android.os.BatteryManagerInternal; +import android.os.RemoteException; import android.util.IndentingPrintWriter; import android.util.Log; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.IBatteryStats; +import com.android.server.LocalServices; +import com.android.server.am.BatteryStatsService; + import java.util.ArrayList; import java.util.List; @@ -38,6 +47,8 @@ public class Analyst { || Log.isLoggable(TAG, Log.DEBUG); private static final int NUM_PERIODS_TO_RETAIN = 8; + @VisibleForTesting + static final long MIN_REPORT_DURATION_FOR_RESET = 24 * HOUR_IN_MILLIS; static final class Report { /** How much the battery was discharged over the tracked period. */ @@ -73,6 +84,22 @@ public class Analyst { public long cumulativeNegativeRegulations = 0; public int numNegativeRegulations = 0; + /** + * The approximate amount of time the screen has been off while on battery while this + * report has been active. + */ + public long screenOffDurationMs = 0; + /** + * The approximate amount of battery discharge while this report has been active. + */ + public long screenOffDischargeMah = 0; + /** The offset used to get the delta when polling the screen off time from BatteryStats. */ + private long bsScreenOffRealtimeBase = 0; + /** + * The offset used to get the delta when polling the screen off discharge from BatteryStats. + */ + private long bsScreenOffDischargeMahBase = 0; + private void clear() { cumulativeBatteryDischarge = 0; currentBatteryLevel = 0; @@ -86,13 +113,27 @@ public class Analyst { numPositiveRegulations = 0; cumulativeNegativeRegulations = 0; numNegativeRegulations = 0; + screenOffDurationMs = 0; + screenOffDischargeMah = 0; + bsScreenOffRealtimeBase = 0; + bsScreenOffDischargeMahBase = 0; } } + private final IBatteryStats mIBatteryStats; + private int mPeriodIndex = 0; /** How much the battery was discharged over the tracked period. */ private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN]; + Analyst() { + this(BatteryStatsService.getService()); + } + + @VisibleForTesting Analyst(IBatteryStats iBatteryStats) { + mIBatteryStats = iBatteryStats; + } + /** Returns the list of most recent reports, with the oldest report first. */ @NonNull List<Report> getReports() { @@ -107,13 +148,35 @@ public class Analyst { return list; } + long getBatteryScreenOffDischargeMah() { + long discharge = 0; + for (Report report : mReports) { + if (report == null) { + continue; + } + discharge += report.screenOffDischargeMah; + } + return discharge; + } + + long getBatteryScreenOffDurationMs() { + long duration = 0; + for (Report report : mReports) { + if (report == null) { + continue; + } + duration += report.screenOffDurationMs; + } + return duration; + } + /** * Tracks the given reports instead of whatever is currently saved. Reports should be ordered * oldest to most recent. */ void loadReports(@NonNull List<Report> reports) { final int numReports = reports.size(); - mPeriodIndex = Math.max(0, numReports - 1); + mPeriodIndex = Math.max(0, Math.min(NUM_PERIODS_TO_RETAIN, numReports) - 1); for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) { if (i < numReports) { mReports[i] = reports.get(i); @@ -121,22 +184,38 @@ public class Analyst { mReports[i] = null; } } + final Report latest = mReports[mPeriodIndex]; + if (latest != null) { + latest.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs(); + latest.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah(); + } } void noteBatteryLevelChange(int newBatteryLevel) { - if (newBatteryLevel == 100 && mReports[mPeriodIndex] != null - && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel) { + final boolean deviceDischargedEnough = mReports[mPeriodIndex] != null + && newBatteryLevel >= 90 + // Battery level is increasing, so device is charging. + && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel + && mReports[mPeriodIndex].cumulativeBatteryDischarge >= 25; + final boolean reportLongEnough = mReports[mPeriodIndex] != null + // Battery level is increasing, so device is charging. + && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel + && mReports[mPeriodIndex].screenOffDurationMs >= MIN_REPORT_DURATION_FOR_RESET; + final boolean shouldStartNewReport = deviceDischargedEnough || reportLongEnough; + if (shouldStartNewReport) { mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN; if (mReports[mPeriodIndex] != null) { final Report report = mReports[mPeriodIndex]; report.clear(); report.currentBatteryLevel = newBatteryLevel; + report.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs(); + report.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah(); return; } } if (mReports[mPeriodIndex] == null) { - Report report = new Report(); + Report report = initializeReport(); mReports[mPeriodIndex] = report; report.currentBatteryLevel = newBatteryLevel; return; @@ -145,13 +224,27 @@ public class Analyst { final Report report = mReports[mPeriodIndex]; if (newBatteryLevel < report.currentBatteryLevel) { report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel); + + final long latestScreenOffRealtime = getLatestBatteryScreenOffRealtimeMs(); + final long latestScreenOffDischargeMah = getLatestScreenOffDischargeMah(); + if (report.bsScreenOffRealtimeBase > latestScreenOffRealtime) { + // BatteryStats reset + report.bsScreenOffRealtimeBase = 0; + report.bsScreenOffDischargeMahBase = 0; + } + report.screenOffDurationMs += + (latestScreenOffRealtime - report.bsScreenOffRealtimeBase); + report.screenOffDischargeMah += + (latestScreenOffDischargeMah - report.bsScreenOffDischargeMahBase); + report.bsScreenOffRealtimeBase = latestScreenOffRealtime; + report.bsScreenOffDischargeMahBase = latestScreenOffDischargeMah; } report.currentBatteryLevel = newBatteryLevel; } void noteTransaction(@NonNull Ledger.Transaction transaction) { if (mReports[mPeriodIndex] == null) { - mReports[mPeriodIndex] = new Report(); + mReports[mPeriodIndex] = initializeReport(); } final Report report = mReports[mPeriodIndex]; switch (getEventType(transaction.eventId)) { @@ -191,6 +284,32 @@ public class Analyst { mPeriodIndex = 0; } + private long getLatestBatteryScreenOffRealtimeMs() { + try { + return mIBatteryStats.computeBatteryScreenOffRealtimeMs(); + } catch (RemoteException e) { + // Shouldn't happen + return 0; + } + } + + private long getLatestScreenOffDischargeMah() { + try { + return mIBatteryStats.getScreenOffDischargeMah(); + } catch (RemoteException e) { + // Shouldn't happen + return 0; + } + } + + @NonNull + private Report initializeReport() { + final Report report = new Report(); + report.bsScreenOffRealtimeBase = getLatestBatteryScreenOffRealtimeMs(); + report.bsScreenOffDischargeMahBase = getLatestScreenOffDischargeMah(); + return report; + } + @NonNull private String padStringWithSpaces(@NonNull String text, int targetLength) { // Make sure to have at least one space on either side. @@ -199,6 +318,8 @@ public class Analyst { } void dump(IndentingPrintWriter pw) { + final BatteryManagerInternal bmi = LocalServices.getService(BatteryManagerInternal.class); + final long batteryCapacityMah = bmi.getBatteryFullCharge() / 1000; pw.println("Reports:"); pw.increaseIndent(); pw.print(" Total Discharge"); @@ -208,6 +329,7 @@ public class Analyst { pw.print(padStringWithSpaces("Rewards (avg/reward : avg/discharge)", statColsLength)); pw.print(padStringWithSpaces("+Regs (avg/reg : avg/discharge)", statColsLength)); pw.print(padStringWithSpaces("-Regs (avg/reg : avg/discharge)", statColsLength)); + pw.print(padStringWithSpaces("Bg drain estimate", statColsLength)); pw.println(); for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) { final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN; @@ -283,6 +405,15 @@ public class Analyst { } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } + if (report.screenOffDurationMs > 0) { + pw.print(padStringWithSpaces(String.format("%d mAh (%.2f%%/hr)", + report.screenOffDischargeMah, + 1.0 * report.screenOffDischargeMah * HOUR_IN_MILLIS + / (batteryCapacityMah * report.screenOffDurationMs)), + statColsLength)); + } else { + pw.print(padStringWithSpaces("N/A", statColsLength)); + } pw.println(); } pw.decreaseIndent(); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java index 4a26d213a48a..ca0b65c2e6c9 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java @@ -112,6 +112,19 @@ public class InternalResourceService extends SystemService { * limit). */ private static final int QUANTITATIVE_EASING_BATTERY_THRESHOLD = 50; + /** + * The battery level above which we may consider adjusting the desired stock level. + */ + private static final int STOCK_RECALCULATION_BATTERY_THRESHOLD = 80; + /** + * The amount of time to wait before considering recalculating the desired stock level. + */ + private static final long STOCK_RECALCULATION_DELAY_MS = 16 * HOUR_IN_MILLIS; + /** + * The minimum amount of time we must have background drain for before considering + * recalculating the desired stock level. + */ + private static final long STOCK_RECALCULATION_MIN_DATA_DURATION_MS = 8 * HOUR_IN_MILLIS; private static final int PACKAGE_QUERY_FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.MATCH_APEX; @@ -177,6 +190,9 @@ public class InternalResourceService extends SystemService { @GuardedBy("mLock") private int mCurrentBatteryLevel; + // TODO(250007395): make configurable per device + private final int mTargetBackgroundBatteryLifeHours; + private final IAppOpsCallback mApbListener = new IAppOpsCallback.Stub() { @Override public void opChanged(int op, int uid, String packageName) { @@ -316,6 +332,11 @@ public class InternalResourceService extends SystemService { mConfigObserver = new ConfigObserver(mHandler, context); + mTargetBackgroundBatteryLifeHours = + mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH) + ? 200 // ~ 0.5%/hr + : 100; // ~ 1%/hr + publishLocalService(EconomyManagerInternal.class, new LocalService()); } @@ -461,6 +482,9 @@ public class InternalResourceService extends SystemService { mAnalyst.noteBatteryLevelChange(newBatteryLevel); final boolean increased = newBatteryLevel > mCurrentBatteryLevel; if (increased) { + if (newBatteryLevel >= STOCK_RECALCULATION_BATTERY_THRESHOLD) { + maybeAdjustDesiredStockLevelLocked(); + } mAgent.distributeBasicIncomeLocked(newBatteryLevel); } else if (newBatteryLevel == mCurrentBatteryLevel) { // The broadcast is also sent when the plug type changes... @@ -623,6 +647,10 @@ public class InternalResourceService extends SystemService { */ @GuardedBy("mLock") void maybePerformQuantitativeEasingLocked() { + if (mConfigObserver.ENABLE_TIP3) { + maybeAdjustDesiredStockLevelLocked(); + return; + } // We don't need to increase the limit if the device runs out of consumable credits // when the battery is low. final long remainingConsumableCakes = mScribe.getRemainingConsumableCakesLocked(); @@ -643,6 +671,68 @@ public class InternalResourceService extends SystemService { } } + /** + * Adjust the consumption limit based on historical data and the target battery drain. + */ + @GuardedBy("mLock") + void maybeAdjustDesiredStockLevelLocked() { + if (!mConfigObserver.ENABLE_TIP3) { + return; + } + // Don't adjust the limit too often or while the battery is low. + final long now = getCurrentTimeMillis(); + if ((now - mScribe.getLastStockRecalculationTimeLocked()) < STOCK_RECALCULATION_DELAY_MS + || mCurrentBatteryLevel <= STOCK_RECALCULATION_BATTERY_THRESHOLD) { + return; + } + + // For now, use screen off battery drain as a proxy for background battery drain. + // TODO: get more accurate background battery drain numbers + final long totalScreenOffDurationMs = mAnalyst.getBatteryScreenOffDurationMs(); + if (totalScreenOffDurationMs < STOCK_RECALCULATION_MIN_DATA_DURATION_MS) { + return; + } + final long totalDischargeMah = mAnalyst.getBatteryScreenOffDischargeMah(); + final long batteryCapacityMah = mBatteryManagerInternal.getBatteryFullCharge() / 1000; + final long estimatedLifeHours = batteryCapacityMah * totalScreenOffDurationMs + / totalDischargeMah / HOUR_IN_MILLIS; + final long percentageOfTarget = + 100 * estimatedLifeHours / mTargetBackgroundBatteryLifeHours; + if (DEBUG) { + Slog.d(TAG, "maybeAdjustDesiredStockLevelLocked:" + + " screenOffMs=" + totalScreenOffDurationMs + + " dischargeMah=" + totalDischargeMah + + " capacityMah=" + batteryCapacityMah + + " estimatedLifeHours=" + estimatedLifeHours + + " %ofTarget=" + percentageOfTarget); + } + final long currentConsumptionLimit = mScribe.getSatiatedConsumptionLimitLocked(); + final long newConsumptionLimit; + if (percentageOfTarget > 105) { + // The stock is too low. We're doing pretty well. We can increase the stock slightly + // to let apps do more work in the background. + newConsumptionLimit = Math.min((long) (currentConsumptionLimit * 1.01), + mCompleteEconomicPolicy.getHardSatiatedConsumptionLimit()); + } else if (percentageOfTarget < 100) { + // The stock is too high IMO. We're below the target. Decrease the stock to reduce + // background work. + newConsumptionLimit = Math.max((long) (currentConsumptionLimit * .98), + mCompleteEconomicPolicy.getInitialSatiatedConsumptionLimit()); + } else { + // The stock is just right. + return; + } + // TODO(250007191): calculate and log implied service level + if (newConsumptionLimit != currentConsumptionLimit) { + Slog.i(TAG, "Adjusting consumption limit from " + cakeToString(currentConsumptionLimit) + + " to " + cakeToString(newConsumptionLimit) + + " because drain was " + percentageOfTarget + "% of target"); + mScribe.setConsumptionLimitLocked(newConsumptionLimit); + adjustCreditSupplyLocked(/* allowIncrease */ true); + mScribe.setLastStockRecalculationTimeLocked(now); + } + } + void postAffordabilityChanged(final int userId, @NonNull final String pkgName, @NonNull Agent.ActionAffordabilityNote affordabilityNote) { if (DEBUG) { @@ -1214,6 +1304,12 @@ public class InternalResourceService extends SystemService { private class ConfigObserver extends ContentObserver implements DeviceConfig.OnPropertiesChangedListener { private static final String KEY_DC_ENABLE_TARE = "enable_tare"; + private static final String KEY_ENABLE_TIP3 = "enable_tip3"; + + private static final boolean DEFAULT_ENABLE_TIP3 = true; + + /** Use a target background battery drain rate to determine consumption limits. */ + public boolean ENABLE_TIP3 = DEFAULT_ENABLE_TIP3; private final ContentResolver mContentResolver; @@ -1264,6 +1360,9 @@ public class InternalResourceService extends SystemService { case KEY_DC_ENABLE_TARE: updateEnabledStatus(); break; + case KEY_ENABLE_TIP3: + ENABLE_TIP3 = properties.getBoolean(name, DEFAULT_ENABLE_TIP3); + break; default: if (!economicPolicyUpdated && (name.startsWith("am") || name.startsWith("js"))) { diff --git a/apex/jobscheduler/service/java/com/android/server/tare/README.md b/apex/jobscheduler/service/java/com/android/server/tare/README.md index e338ed1c6987..8d25ecce8431 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/README.md +++ b/apex/jobscheduler/service/java/com/android/server/tare/README.md @@ -80,9 +80,9 @@ consumption limit, then the available resources are decreased to match the scale Regulations are unique events invoked by the ~~government~~ system in order to get the whole economy moving smoothly. -# Previous Implementations +# Significant Changes -## V0 +## Tare Improvement Proposal #1 (TIP1) The initial implementation/proposal combined the supply of resources with the allocation in a single mechanism. It defined the maximum number of resources (ARCs) available at a time, and then divided @@ -98,10 +98,25 @@ allocated as part of the rewards. There were several problems with that mechanis These problems effectively meant that misallocation was a big problem, demand wasn't well reflected, and some apps may not have been able to perform work even though they otherwise should have been. -Tare Improvement Proposal #1 (TIP1) separated allocation (to apps) from supply (by the system) and +TIP1 separated allocation (to apps) from supply (by the system) and allowed apps to accrue credits as appropriate while still limiting the total number of credits consumed. +## Tare Improvement Proposal #3 (TIP3) + +TIP1 introduced Consumption Limits, which control the total number of ARCs that can be used to +perform actions, based on the production costs of each action. The Consumption Limits were initially +determined manually, but could increase in the system if apps used the full consumption limit before +the device had drained to 50% battery. As with any system that relies on manually deciding +parameters, the only mechanism to identify an optimal value is through experimentation, which can +take many iterations and requires extended periods of time to observe results. The limits are also +chosen and adjusted without consideration of the resulting battery drain of each possible value. In +addition, having the system potentially increase the limit without considering a decrease introduced +potential for battery life to get worse as time goes on and the user installed more background-work +demanding apps. + +TIP3 uses a target background battery drain rate to dynamically adjust the Consumption Limit. + # Potential Future Changes These are some ideas for further changes. There's no guarantee that they'll be implemented. diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java index bd4fd72b78ce..27d00b76f452 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java @@ -84,6 +84,8 @@ public class Scribe { private static final String XML_ATTR_USER_ID = "userId"; private static final String XML_ATTR_VERSION = "version"; private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime"; + private static final String XML_ATTR_LAST_STOCK_RECALCULATION_TIME = + "lastStockRecalculationTime"; private static final String XML_ATTR_REMAINING_CONSUMABLE_CAKES = "remainingConsumableCakes"; private static final String XML_ATTR_CONSUMPTION_LIMIT = "consumptionLimit"; private static final String XML_ATTR_PR_DISCHARGE = "discharge"; @@ -98,6 +100,8 @@ public class Scribe { private static final String XML_ATTR_PR_NUM_POS_REGULATIONS = "numPosRegulations"; private static final String XML_ATTR_PR_NEG_REGULATIONS = "negRegulations"; private static final String XML_ATTR_PR_NUM_NEG_REGULATIONS = "numNegRegulations"; + private static final String XML_ATTR_PR_SCREEN_OFF_DURATION_MS = "screenOffDurationMs"; + private static final String XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH = "screenOffDischargeMah"; /** Version of the file schema. */ private static final int STATE_FILE_VERSION = 0; @@ -111,6 +115,8 @@ public class Scribe { @GuardedBy("mIrs.getLock()") private long mLastReclamationTime; @GuardedBy("mIrs.getLock()") + private long mLastStockRecalculationTime; + @GuardedBy("mIrs.getLock()") private long mSatiatedConsumptionLimit; @GuardedBy("mIrs.getLock()") private long mRemainingConsumableCakes; @@ -173,6 +179,11 @@ public class Scribe { } @GuardedBy("mIrs.getLock()") + long getLastStockRecalculationTimeLocked() { + return mLastStockRecalculationTime; + } + + @GuardedBy("mIrs.getLock()") @NonNull Ledger getLedgerLocked(final int userId, @NonNull final String pkgName) { Ledger ledger = mLedgers.get(userId, pkgName); @@ -281,6 +292,8 @@ public class Scribe { case XML_TAG_HIGH_LEVEL_STATE: mLastReclamationTime = parser.getAttributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME); + mLastStockRecalculationTime = parser.getAttributeLong(null, + XML_ATTR_LAST_STOCK_RECALCULATION_TIME, 0); mSatiatedConsumptionLimit = parser.getAttributeLong(null, XML_ATTR_CONSUMPTION_LIMIT, mIrs.getInitialSatiatedConsumptionLimitLocked()); @@ -337,6 +350,12 @@ public class Scribe { } @GuardedBy("mIrs.getLock()") + void setLastStockRecalculationTimeLocked(long time) { + mLastStockRecalculationTime = time; + postWrite(); + } + + @GuardedBy("mIrs.getLock()") void tearDownLocked() { TareHandlerThread.getHandler().removeCallbacks(mCleanRunnable); TareHandlerThread.getHandler().removeCallbacks(mWriteRunnable); @@ -504,7 +523,6 @@ public class Scribe { return earliestEndTime; } - /** * @param parser Xml parser at the beginning of a {@link #XML_TAG_PERIOD_REPORT} tag. The next * "parser.next()" call will take the parser into the body of the report tag. @@ -531,6 +549,10 @@ public class Scribe { parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS); report.numNegativeRegulations = parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS); + report.screenOffDurationMs = + parser.getAttributeLong(null, XML_ATTR_PR_SCREEN_OFF_DURATION_MS, 0); + report.screenOffDischargeMah = + parser.getAttributeLong(null, XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH, 0); return report; } @@ -606,6 +628,8 @@ public class Scribe { out.startTag(null, XML_TAG_HIGH_LEVEL_STATE); out.attributeLong(null, XML_ATTR_LAST_RECLAMATION_TIME, mLastReclamationTime); + out.attributeLong(null, + XML_ATTR_LAST_STOCK_RECALCULATION_TIME, mLastStockRecalculationTime); out.attributeLong(null, XML_ATTR_CONSUMPTION_LIMIT, mSatiatedConsumptionLimit); out.attributeLong(null, XML_ATTR_REMAINING_CONSUMABLE_CAKES, mRemainingConsumableCakes); @@ -718,6 +742,8 @@ public class Scribe { out.attributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS, report.numPositiveRegulations); out.attributeLong(null, XML_ATTR_PR_NEG_REGULATIONS, report.cumulativeNegativeRegulations); out.attributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS, report.numNegativeRegulations); + out.attributeLong(null, XML_ATTR_PR_SCREEN_OFF_DURATION_MS, report.screenOffDurationMs); + out.attributeLong(null, XML_ATTR_PR_SCREEN_OFF_DISCHARGE_MAH, report.screenOffDischargeMah); out.endTag(null, XML_TAG_PERIOD_REPORT); } diff --git a/core/java/com/android/internal/app/IBatteryStats.aidl b/core/java/com/android/internal/app/IBatteryStats.aidl index 3732ea5abaa5..787b594af6bb 100644 --- a/core/java/com/android/internal/app/IBatteryStats.aidl +++ b/core/java/com/android/internal/app/IBatteryStats.aidl @@ -84,6 +84,11 @@ interface IBatteryStats { @RequiresNoPermission long computeChargeTimeRemaining(); + @EnforcePermission("BATTERY_STATS") + long computeBatteryScreenOffRealtimeMs(); + @EnforcePermission("BATTERY_STATS") + long getScreenOffDischargeMah(); + @EnforcePermission("UPDATE_DEVICE_STATS") void noteEvent(int code, String name, int uid); diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index a41a311bd643..d9d29d650f03 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -831,6 +831,26 @@ public final class BatteryStatsService extends IBatteryStats.Stub } @Override + @EnforcePermission(BATTERY_STATS) + public long computeBatteryScreenOffRealtimeMs() { + synchronized (mStats) { + final long curTimeUs = SystemClock.elapsedRealtimeNanos() / 1000; + long timeUs = mStats.computeBatteryScreenOffRealtime(curTimeUs, + BatteryStats.STATS_SINCE_CHARGED); + return timeUs / 1000; + } + } + + @Override + @EnforcePermission(BATTERY_STATS) + public long getScreenOffDischargeMah() { + synchronized (mStats) { + long dischargeUah = mStats.getUahDischargeScreenOff(BatteryStats.STATS_SINCE_CHARGED); + return dischargeUah / 1000; + } + } + + @Override @EnforcePermission(UPDATE_DEVICE_STATS) public void noteEvent(final int code, final String name, final int uid) { if (name == null) { diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java index 4ce268f0dc39..ddfa05cf5a2e 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java @@ -140,6 +140,8 @@ public class ScribeTest { report1.numPositiveRegulations = 10; report1.cumulativeNegativeRegulations = 11; report1.numNegativeRegulations = 12; + report1.screenOffDurationMs = 13; + report1.screenOffDischargeMah = 14; mReports.add(report1); mScribeUnderTest.writeImmediatelyForTesting(); mScribeUnderTest.loadFromDiskLocked(); @@ -160,6 +162,8 @@ public class ScribeTest { report2.numPositiveRegulations = 100; report2.cumulativeNegativeRegulations = 110; report2.numNegativeRegulations = 120; + report2.screenOffDurationMs = 130; + report2.screenOffDischargeMah = 140; mReports.add(report2); mScribeUnderTest.writeImmediatelyForTesting(); mScribeUnderTest.loadFromDiskLocked(); @@ -385,6 +389,10 @@ public class ScribeTest { eReport.cumulativeNegativeRegulations, aReport.cumulativeNegativeRegulations); assertEquals("Reports #" + i + " numNegativeRegulations are not equal", eReport.numNegativeRegulations, aReport.numNegativeRegulations); + assertEquals("Reports #" + i + " screenOffDurationMs are not equal", + eReport.screenOffDurationMs, aReport.screenOffDurationMs); + assertEquals("Reports #" + i + " screenOffDischargeMah are not equal", + eReport.screenOffDischargeMah, aReport.screenOffDischargeMah); } } diff --git a/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java b/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java index 2b527a261ae1..a603b93ab307 100644 --- a/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java +++ b/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java @@ -19,10 +19,14 @@ package com.android.server.tare; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.app.IBatteryStats; + import org.junit.Test; import org.junit.runner.RunWith; @@ -45,20 +49,20 @@ public class AnalystTest { final Analyst analyst = new Analyst(); Analyst.Report expected = new Analyst.Report(); - expected.currentBatteryLevel = 55; - analyst.noteBatteryLevelChange(55); + expected.currentBatteryLevel = 75; + analyst.noteBatteryLevelChange(75); assertEquals(1, analyst.getReports().size()); assertReportsEqual(expected, analyst.getReports().get(0)); // Discharging analyst.noteBatteryLevelChange(54); expected.currentBatteryLevel = 54; - expected.cumulativeBatteryDischarge = 1; + expected.cumulativeBatteryDischarge = 21; assertEquals(1, analyst.getReports().size()); assertReportsEqual(expected, analyst.getReports().get(0)); analyst.noteBatteryLevelChange(50); expected.currentBatteryLevel = 50; - expected.cumulativeBatteryDischarge = 5; + expected.cumulativeBatteryDischarge = 25; assertEquals(1, analyst.getReports().size()); assertReportsEqual(expected, analyst.getReports().get(0)); @@ -87,27 +91,53 @@ public class AnalystTest { } @Test - public void testTransaction_PeriodChange() { - final Analyst analyst = new Analyst(); + public void testTransaction_PeriodChange() throws Exception { + IBatteryStats iBatteryStats = mock(IBatteryStats.class); + final Analyst analyst = new Analyst(iBatteryStats); + // Reset from enough discharge. Analyst.Report expected = new Analyst.Report(); - expected.currentBatteryLevel = 55; - analyst.noteBatteryLevelChange(55); + expected.currentBatteryLevel = 75; + analyst.noteBatteryLevelChange(75); runTestTransactions(analyst, expected, 1); expected.currentBatteryLevel = 49; - expected.cumulativeBatteryDischarge = 6; + expected.cumulativeBatteryDischarge = 26; analyst.noteBatteryLevelChange(49); runTestTransactions(analyst, expected, 1); expected = new Analyst.Report(); - expected.currentBatteryLevel = 100; - analyst.noteBatteryLevelChange(100); + expected.currentBatteryLevel = 90; + analyst.noteBatteryLevelChange(90); expected.cumulativeBatteryDischarge = 0; runTestTransactions(analyst, expected, 2); + + // Reset from report being long enough. + doReturn(Analyst.MIN_REPORT_DURATION_FOR_RESET) + .when(iBatteryStats).computeBatteryScreenOffRealtimeMs(); + expected.currentBatteryLevel = 85; + analyst.noteBatteryLevelChange(85); + expected.cumulativeBatteryDischarge = 5; + expected.screenOffDurationMs = Analyst.MIN_REPORT_DURATION_FOR_RESET; + + runTestTransactions(analyst, expected, 2); + + expected.currentBatteryLevel = 79; + analyst.noteBatteryLevelChange(79); + expected.cumulativeBatteryDischarge = 11; + + runTestTransactions(analyst, expected, 2); + + expected = new Analyst.Report(); + expected.currentBatteryLevel = 80; + analyst.noteBatteryLevelChange(80); + expected.cumulativeBatteryDischarge = 0; + expected.screenOffDurationMs = 0; + + runTestTransactions(analyst, expected, 3); } private void runTestTransactions(Analyst analyst, Analyst.Report lastExpectedReport, @@ -223,6 +253,8 @@ public class AnalystTest { assertEquals(expected.numPositiveRegulations, actual.numPositiveRegulations); assertEquals(expected.cumulativeNegativeRegulations, actual.cumulativeNegativeRegulations); assertEquals(expected.numNegativeRegulations, actual.numNegativeRegulations); + assertEquals(expected.screenOffDurationMs, actual.screenOffDurationMs); + assertEquals(expected.screenOffDischargeMah, actual.screenOffDischargeMah); } private void assertReportListsEqual(List<Analyst.Report> expected, |