Loading apex/jobscheduler/service/java/com/android/server/tare/Agent.java +5 −2 Original line number Diff line number Diff line Loading @@ -73,6 +73,7 @@ class Agent { private final Object mLock; private final Handler mHandler; private final Analyst mAnalyst; private final InternalResourceService mIrs; private final Scribe mScribe; Loading Loading @@ -110,10 +111,11 @@ class Agent { */ private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1; Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe) { Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst) { mLock = irs.getLock(); mIrs = irs; mScribe = scribe; mAnalyst = analyst; mHandler = new AgentHandler(TareHandlerThread.get().getLooper()); mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue( Loading Loading @@ -443,7 +445,7 @@ class Agent { void recordTransactionLocked(final int userId, @NonNull final String pkgName, @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction, final boolean notifyOnAffordabilityChange) { if (transaction.delta == 0) { if (!DEBUG && transaction.delta == 0) { // Skip recording transactions with a delta of 0 to save on space. return; } Loading Loading @@ -471,6 +473,7 @@ class Agent { } ledger.recordTransaction(transaction); mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp); mAnalyst.noteTransaction(transaction); if (transaction.delta != 0 && notifyOnAffordabilityChange) { final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = mActionAffordabilityNotes.get(userId, pkgName); Loading apex/jobscheduler/service/java/com/android/server/tare/Analyst.java 0 → 100644 +290 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 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.tare; 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; import static com.android.server.tare.EconomicPolicy.getEventType; import static com.android.server.tare.TareUtils.cakeToString; import android.annotation.NonNull; import android.util.IndentingPrintWriter; import android.util.Log; import java.util.ArrayList; import java.util.List; /** * Responsible for maintaining statistics and analysis of TARE's performance. */ public class Analyst { private static final String TAG = "TARE-" + Analyst.class.getSimpleName(); private static final boolean DEBUG = InternalResourceService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); private static final int NUM_PERIODS_TO_RETAIN = 8; static final class Report { /** How much the battery was discharged over the tracked period. */ public int cumulativeBatteryDischarge = 0; public int currentBatteryLevel = 0; /** * Profit from performing actions. This excludes special circumstances where we charge the * app * less than the action's CTP. */ public long cumulativeProfit = 0; public int numProfitableActions = 0; /** * Losses from performing actions for special circumstances (eg. for a TOP app) where we * charge * the app less than the action's CTP. */ public long cumulativeLoss = 0; public int numUnprofitableActions = 0; /** * The total number of rewards given to apps over this period. */ public long cumulativeRewards = 0; public int numRewards = 0; /** * Regulations that increased an app's balance. */ public long cumulativePositiveRegulations = 0; public int numPositiveRegulations = 0; /** * Regulations that decreased an app's balance. */ public long cumulativeNegativeRegulations = 0; public int numNegativeRegulations = 0; private void clear() { cumulativeBatteryDischarge = 0; currentBatteryLevel = 0; cumulativeProfit = 0; numProfitableActions = 0; cumulativeLoss = 0; numUnprofitableActions = 0; cumulativeRewards = 0; numRewards = 0; cumulativePositiveRegulations = 0; numPositiveRegulations = 0; cumulativeNegativeRegulations = 0; numNegativeRegulations = 0; } } private int mPeriodIndex = 0; /** How much the battery was discharged over the tracked period. */ private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN]; /** Returns the list of most recent reports, with the oldest report first. */ @NonNull List<Report> getReports() { final List<Report> list = new ArrayList<>(NUM_PERIODS_TO_RETAIN); for (int i = 1; i <= NUM_PERIODS_TO_RETAIN; ++i) { final int idx = (mPeriodIndex + i) % NUM_PERIODS_TO_RETAIN; final Report report = mReports[idx]; if (report != null) { list.add(report); } } return list; } /** * 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); for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) { if (i < numReports) { mReports[i] = reports.get(i); } else { mReports[i] = null; } } } void noteBatteryLevelChange(int newBatteryLevel) { if (newBatteryLevel == 100 && mReports[mPeriodIndex] != null && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel) { mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN; if (mReports[mPeriodIndex] != null) { final Report report = mReports[mPeriodIndex]; report.clear(); report.currentBatteryLevel = newBatteryLevel; return; } } if (mReports[mPeriodIndex] == null) { Report report = new Report(); mReports[mPeriodIndex] = report; report.currentBatteryLevel = newBatteryLevel; return; } final Report report = mReports[mPeriodIndex]; if (newBatteryLevel < report.currentBatteryLevel) { report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel); } report.currentBatteryLevel = newBatteryLevel; } void noteTransaction(@NonNull Ledger.Transaction transaction) { if (mReports[mPeriodIndex] == null) { mReports[mPeriodIndex] = new Report(); } final Report report = mReports[mPeriodIndex]; switch (getEventType(transaction.eventId)) { case TYPE_ACTION: // For now, assume all instances where price < CTP is a special instance. // TODO: add an explicit signal for special circumstances if (-transaction.delta > transaction.ctp) { report.cumulativeProfit += (-transaction.delta - transaction.ctp); report.numProfitableActions++; } else if (-transaction.delta < transaction.ctp) { report.cumulativeLoss += (transaction.ctp + transaction.delta); report.numUnprofitableActions++; } break; case TYPE_REGULATION: if (transaction.delta > 0) { report.cumulativePositiveRegulations += transaction.delta; report.numPositiveRegulations++; } else if (transaction.delta < 0) { report.cumulativeNegativeRegulations -= transaction.delta; report.numNegativeRegulations++; } break; case TYPE_REWARD: if (transaction.delta != 0) { report.cumulativeRewards += transaction.delta; report.numRewards++; } break; } } void tearDown() { for (int i = 0; i < mReports.length; ++i) { mReports[i] = null; } mPeriodIndex = 0; } @NonNull private String padStringWithSpaces(@NonNull String text, int targetLength) { // Make sure to have at least one space on either side. final int padding = Math.max(2, targetLength - text.length()) >>> 1; return " ".repeat(padding) + text + " ".repeat(padding); } void dump(IndentingPrintWriter pw) { pw.println("Reports:"); pw.increaseIndent(); pw.print(" Total Discharge"); final int statColsLength = 47; pw.print(padStringWithSpaces("Profit (avg/action : avg/discharge)", statColsLength)); pw.print(padStringWithSpaces("Loss (avg/action : avg/discharge)", statColsLength)); 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.println(); for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) { final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN; final Report report = mReports[idx]; if (report == null) { continue; } pw.print("t-"); pw.print(r); pw.print(": "); pw.print(padStringWithSpaces(Integer.toString(report.cumulativeBatteryDischarge), 15)); if (report.numProfitableActions > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString(report.cumulativeProfit / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativeProfit), cakeToString(report.cumulativeProfit / report.numProfitableActions), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } if (report.numUnprofitableActions > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString(report.cumulativeLoss / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativeLoss), cakeToString(report.cumulativeLoss / report.numUnprofitableActions), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } if (report.numRewards > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString(report.cumulativeRewards / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativeRewards), cakeToString(report.cumulativeRewards / report.numRewards), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } if (report.numPositiveRegulations > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString( report.cumulativePositiveRegulations / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativePositiveRegulations), cakeToString(report.cumulativePositiveRegulations / report.numPositiveRegulations), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } if (report.numNegativeRegulations > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString( report.cumulativeNegativeRegulations / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativeNegativeRegulations), cakeToString(report.cumulativeNegativeRegulations / report.numNegativeRegulations), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } pw.println(); } pw.decreaseIndent(); } } apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +9 −2 Original line number Diff line number Diff line Loading @@ -121,6 +121,7 @@ public class InternalResourceService extends SystemService { private IDeviceIdleController mDeviceIdleController; private final Agent mAgent; private final Analyst mAnalyst; private final ConfigObserver mConfigObserver; private final EconomyManagerStub mEconomyManagerStub; private final Scribe mScribe; Loading Loading @@ -254,9 +255,10 @@ public class InternalResourceService extends SystemService { mPackageManager = context.getPackageManager(); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mEconomyManagerStub = new EconomyManagerStub(); mScribe = new Scribe(this); mAnalyst = new Analyst(); mScribe = new Scribe(this, mAnalyst); mCompleteEconomicPolicy = new CompleteEconomicPolicy(this); mAgent = new Agent(this, mScribe); mAgent = new Agent(this, mScribe, mAnalyst); mConfigObserver = new ConfigObserver(mHandler, context); Loading Loading @@ -375,6 +377,7 @@ public class InternalResourceService extends SystemService { void onBatteryLevelChanged() { synchronized (mLock) { final int newBatteryLevel = getCurrentBatteryLevel(); mAnalyst.noteBatteryLevelChange(newBatteryLevel); final boolean increased = newBatteryLevel > mCurrentBatteryLevel; if (increased) { mAgent.distributeBasicIncomeLocked(newBatteryLevel); Loading Loading @@ -741,6 +744,7 @@ public class InternalResourceService extends SystemService { } synchronized (mLock) { mAgent.tearDownLocked(); mAnalyst.tearDown(); mCompleteEconomicPolicy.tearDown(); mExemptedApps.clear(); mExemptListLoaded = false; Loading Loading @@ -1145,6 +1149,9 @@ public class InternalResourceService extends SystemService { pw.println(); mAgent.dumpLocked(pw); pw.println(); mAnalyst.dump(pw); } } } apex/jobscheduler/service/java/com/android/server/tare/Scribe.java +77 −3 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ public class Scribe { private static final String XML_TAG_TARE = "tare"; private static final String XML_TAG_TRANSACTION = "transaction"; private static final String XML_TAG_USER = "user"; private static final String XML_TAG_PERIOD_REPORT = "report"; private static final String XML_ATTR_CTP = "ctp"; private static final String XML_ATTR_DELTA = "delta"; Loading @@ -86,6 +87,18 @@ public class Scribe { private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime"; 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"; private static final String XML_ATTR_PR_BATTERY_LEVEL = "batteryLevel"; private static final String XML_ATTR_PR_PROFIT = "profit"; private static final String XML_ATTR_PR_NUM_PROFIT = "numProfits"; private static final String XML_ATTR_PR_LOSS = "loss"; private static final String XML_ATTR_PR_NUM_LOSS = "numLoss"; private static final String XML_ATTR_PR_REWARDS = "rewards"; private static final String XML_ATTR_PR_NUM_REWARDS = "numRewards"; private static final String XML_ATTR_PR_POS_REGULATIONS = "posRegulations"; 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"; /** Version of the file schema. */ private static final int STATE_FILE_VERSION = 0; Loading @@ -94,6 +107,7 @@ public class Scribe { private final AtomicFile mStateFile; private final InternalResourceService mIrs; private final Analyst mAnalyst; @GuardedBy("mIrs.getLock()") private long mLastReclamationTime; Loading @@ -107,13 +121,14 @@ public class Scribe { private final Runnable mCleanRunnable = this::cleanupLedgers; private final Runnable mWriteRunnable = this::writeState; Scribe(InternalResourceService irs) { this(irs, Environment.getDataSystemDirectory()); Scribe(InternalResourceService irs, Analyst analyst) { this(irs, analyst, Environment.getDataSystemDirectory()); } @VisibleForTesting Scribe(InternalResourceService irs, File dataDir) { Scribe(InternalResourceService irs, Analyst analyst, File dataDir) { mIrs = irs; mAnalyst = analyst; final File tareDir = new File(dataDir, "tare"); //noinspection ResultOfMethodCallIgnored Loading Loading @@ -210,6 +225,7 @@ public class Scribe { } } final List<Analyst.Report> reports = new ArrayList<>(); try (FileInputStream fis = mStateFile.openRead()) { TypedXmlPullParser parser = Xml.resolvePullParser(fis); Loading Loading @@ -263,11 +279,15 @@ public class Scribe { readUserFromXmlLocked( parser, installedPackagesPerUser, endTimeCutoff)); break; case XML_TAG_PERIOD_REPORT: reports.add(readReportFromXml(parser)); break; default: Slog.e(TAG, "Unexpected tag: " + tagName); break; } } mAnalyst.loadReports(reports); scheduleCleanup(earliestEndTime); } catch (IOException | XmlPullParserException e) { Slog.wtf(TAG, "Error reading state from disk", e); Loading Loading @@ -457,6 +477,37 @@ 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. * @return Newly instantiated Report holding all the information we just read out of the xml tag */ @NonNull private static Analyst.Report readReportFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { final Analyst.Report report = new Analyst.Report(); report.cumulativeBatteryDischarge = parser.getAttributeInt(null, XML_ATTR_PR_DISCHARGE); report.currentBatteryLevel = parser.getAttributeInt(null, XML_ATTR_PR_BATTERY_LEVEL); report.cumulativeProfit = parser.getAttributeLong(null, XML_ATTR_PR_PROFIT); report.numProfitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_PROFIT); report.cumulativeLoss = parser.getAttributeLong(null, XML_ATTR_PR_LOSS); report.numUnprofitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_LOSS); report.cumulativeRewards = parser.getAttributeLong(null, XML_ATTR_PR_REWARDS); report.numRewards = parser.getAttributeInt(null, XML_ATTR_PR_NUM_REWARDS); report.cumulativePositiveRegulations = parser.getAttributeLong(null, XML_ATTR_PR_POS_REGULATIONS); report.numPositiveRegulations = parser.getAttributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS); report.cumulativeNegativeRegulations = parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS); report.numNegativeRegulations = parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS); return report; } private void scheduleCleanup(long earliestEndTime) { if (earliestEndTime == Long.MAX_VALUE) { return; Loading Loading @@ -501,6 +552,11 @@ public class Scribe { writeUserLocked(out, userId)); } List<Analyst.Report> reports = mAnalyst.getReports(); for (int i = 0, size = reports.size(); i < size; ++i) { writeReport(out, reports.get(i)); } out.endTag(null, XML_TAG_TARE); out.endDocument(); Loading Loading @@ -560,6 +616,24 @@ public class Scribe { out.endTag(null, XML_TAG_TRANSACTION); } private static void writeReport(@NonNull TypedXmlSerializer out, @NonNull Analyst.Report report) throws IOException { out.startTag(null, XML_TAG_PERIOD_REPORT); out.attributeInt(null, XML_ATTR_PR_DISCHARGE, report.cumulativeBatteryDischarge); out.attributeInt(null, XML_ATTR_PR_BATTERY_LEVEL, report.currentBatteryLevel); out.attributeLong(null, XML_ATTR_PR_PROFIT, report.cumulativeProfit); out.attributeInt(null, XML_ATTR_PR_NUM_PROFIT, report.numProfitableActions); out.attributeLong(null, XML_ATTR_PR_LOSS, report.cumulativeLoss); out.attributeInt(null, XML_ATTR_PR_NUM_LOSS, report.numUnprofitableActions); out.attributeLong(null, XML_ATTR_PR_REWARDS, report.cumulativeRewards); out.attributeInt(null, XML_ATTR_PR_NUM_REWARDS, report.numRewards); out.attributeLong(null, XML_ATTR_PR_POS_REGULATIONS, report.cumulativePositiveRegulations); 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.endTag(null, XML_TAG_PERIOD_REPORT); } @GuardedBy("mIrs.getLock()") void dumpLocked(IndentingPrintWriter pw, boolean dumpAll) { pw.println("Ledgers:"); Loading services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java +8 −6 Original line number Diff line number Diff line Loading @@ -46,6 +46,8 @@ public class AgentTest { @Mock private CompleteEconomicPolicy mEconomicPolicy; @Mock private Analyst mAnalyst; @Mock private Context mContext; @Mock private InternalResourceService mIrs; Loading @@ -53,8 +55,8 @@ public class AgentTest { private Scribe mScribe; private static class MockScribe extends Scribe { MockScribe(InternalResourceService irs) { super(irs); MockScribe(InternalResourceService irs, Analyst analyst) { super(irs, analyst); } @Override Loading @@ -74,7 +76,7 @@ public class AgentTest { doReturn(mEconomicPolicy).when(mIrs).getCompleteEconomicPolicyLocked(); doReturn(mIrs).when(mIrs).getLock(); doReturn(mock(AlarmManager.class)).when(mContext).getSystemService(Context.ALARM_SERVICE); mScribe = new MockScribe(mIrs); mScribe = new MockScribe(mIrs, mAnalyst); } @After Loading @@ -86,7 +88,7 @@ public class AgentTest { @Test public void testRecordTransaction_UnderMax() { Agent agent = new Agent(mIrs, mScribe); Agent agent = new Agent(mIrs, mScribe, mAnalyst); Ledger ledger = new Ledger(); doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked(); Loading Loading @@ -115,7 +117,7 @@ public class AgentTest { @Test public void testRecordTransaction_MaxConsumptionLimit() { Agent agent = new Agent(mIrs, mScribe); Agent agent = new Agent(mIrs, mScribe, mAnalyst); Ledger ledger = new Ledger(); doReturn(1000L).when(mIrs).getConsumptionLimitLocked(); Loading Loading @@ -162,7 +164,7 @@ public class AgentTest { @Test public void testRecordTransaction_MaxSatiatedBalance() { Agent agent = new Agent(mIrs, mScribe); Agent agent = new Agent(mIrs, mScribe, mAnalyst); Ledger ledger = new Ledger(); doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked(); Loading Loading
apex/jobscheduler/service/java/com/android/server/tare/Agent.java +5 −2 Original line number Diff line number Diff line Loading @@ -73,6 +73,7 @@ class Agent { private final Object mLock; private final Handler mHandler; private final Analyst mAnalyst; private final InternalResourceService mIrs; private final Scribe mScribe; Loading Loading @@ -110,10 +111,11 @@ class Agent { */ private static final int MSG_CHECK_INDIVIDUAL_AFFORDABILITY = 1; Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe) { Agent(@NonNull InternalResourceService irs, @NonNull Scribe scribe, @NonNull Analyst analyst) { mLock = irs.getLock(); mIrs = irs; mScribe = scribe; mAnalyst = analyst; mHandler = new AgentHandler(TareHandlerThread.get().getLooper()); mAppStandbyInternal = LocalServices.getService(AppStandbyInternal.class); mBalanceThresholdAlarmQueue = new BalanceThresholdAlarmQueue( Loading Loading @@ -443,7 +445,7 @@ class Agent { void recordTransactionLocked(final int userId, @NonNull final String pkgName, @NonNull Ledger ledger, @NonNull Ledger.Transaction transaction, final boolean notifyOnAffordabilityChange) { if (transaction.delta == 0) { if (!DEBUG && transaction.delta == 0) { // Skip recording transactions with a delta of 0 to save on space. return; } Loading Loading @@ -471,6 +473,7 @@ class Agent { } ledger.recordTransaction(transaction); mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp); mAnalyst.noteTransaction(transaction); if (transaction.delta != 0 && notifyOnAffordabilityChange) { final ArraySet<ActionAffordabilityNote> actionAffordabilityNotes = mActionAffordabilityNotes.get(userId, pkgName); Loading
apex/jobscheduler/service/java/com/android/server/tare/Analyst.java 0 → 100644 +290 −0 Original line number Diff line number Diff line /* * Copyright (C) 2022 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.tare; 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; import static com.android.server.tare.EconomicPolicy.getEventType; import static com.android.server.tare.TareUtils.cakeToString; import android.annotation.NonNull; import android.util.IndentingPrintWriter; import android.util.Log; import java.util.ArrayList; import java.util.List; /** * Responsible for maintaining statistics and analysis of TARE's performance. */ public class Analyst { private static final String TAG = "TARE-" + Analyst.class.getSimpleName(); private static final boolean DEBUG = InternalResourceService.DEBUG || Log.isLoggable(TAG, Log.DEBUG); private static final int NUM_PERIODS_TO_RETAIN = 8; static final class Report { /** How much the battery was discharged over the tracked period. */ public int cumulativeBatteryDischarge = 0; public int currentBatteryLevel = 0; /** * Profit from performing actions. This excludes special circumstances where we charge the * app * less than the action's CTP. */ public long cumulativeProfit = 0; public int numProfitableActions = 0; /** * Losses from performing actions for special circumstances (eg. for a TOP app) where we * charge * the app less than the action's CTP. */ public long cumulativeLoss = 0; public int numUnprofitableActions = 0; /** * The total number of rewards given to apps over this period. */ public long cumulativeRewards = 0; public int numRewards = 0; /** * Regulations that increased an app's balance. */ public long cumulativePositiveRegulations = 0; public int numPositiveRegulations = 0; /** * Regulations that decreased an app's balance. */ public long cumulativeNegativeRegulations = 0; public int numNegativeRegulations = 0; private void clear() { cumulativeBatteryDischarge = 0; currentBatteryLevel = 0; cumulativeProfit = 0; numProfitableActions = 0; cumulativeLoss = 0; numUnprofitableActions = 0; cumulativeRewards = 0; numRewards = 0; cumulativePositiveRegulations = 0; numPositiveRegulations = 0; cumulativeNegativeRegulations = 0; numNegativeRegulations = 0; } } private int mPeriodIndex = 0; /** How much the battery was discharged over the tracked period. */ private final Report[] mReports = new Report[NUM_PERIODS_TO_RETAIN]; /** Returns the list of most recent reports, with the oldest report first. */ @NonNull List<Report> getReports() { final List<Report> list = new ArrayList<>(NUM_PERIODS_TO_RETAIN); for (int i = 1; i <= NUM_PERIODS_TO_RETAIN; ++i) { final int idx = (mPeriodIndex + i) % NUM_PERIODS_TO_RETAIN; final Report report = mReports[idx]; if (report != null) { list.add(report); } } return list; } /** * 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); for (int i = 0; i < NUM_PERIODS_TO_RETAIN; ++i) { if (i < numReports) { mReports[i] = reports.get(i); } else { mReports[i] = null; } } } void noteBatteryLevelChange(int newBatteryLevel) { if (newBatteryLevel == 100 && mReports[mPeriodIndex] != null && mReports[mPeriodIndex].currentBatteryLevel < newBatteryLevel) { mPeriodIndex = (mPeriodIndex + 1) % NUM_PERIODS_TO_RETAIN; if (mReports[mPeriodIndex] != null) { final Report report = mReports[mPeriodIndex]; report.clear(); report.currentBatteryLevel = newBatteryLevel; return; } } if (mReports[mPeriodIndex] == null) { Report report = new Report(); mReports[mPeriodIndex] = report; report.currentBatteryLevel = newBatteryLevel; return; } final Report report = mReports[mPeriodIndex]; if (newBatteryLevel < report.currentBatteryLevel) { report.cumulativeBatteryDischarge += (report.currentBatteryLevel - newBatteryLevel); } report.currentBatteryLevel = newBatteryLevel; } void noteTransaction(@NonNull Ledger.Transaction transaction) { if (mReports[mPeriodIndex] == null) { mReports[mPeriodIndex] = new Report(); } final Report report = mReports[mPeriodIndex]; switch (getEventType(transaction.eventId)) { case TYPE_ACTION: // For now, assume all instances where price < CTP is a special instance. // TODO: add an explicit signal for special circumstances if (-transaction.delta > transaction.ctp) { report.cumulativeProfit += (-transaction.delta - transaction.ctp); report.numProfitableActions++; } else if (-transaction.delta < transaction.ctp) { report.cumulativeLoss += (transaction.ctp + transaction.delta); report.numUnprofitableActions++; } break; case TYPE_REGULATION: if (transaction.delta > 0) { report.cumulativePositiveRegulations += transaction.delta; report.numPositiveRegulations++; } else if (transaction.delta < 0) { report.cumulativeNegativeRegulations -= transaction.delta; report.numNegativeRegulations++; } break; case TYPE_REWARD: if (transaction.delta != 0) { report.cumulativeRewards += transaction.delta; report.numRewards++; } break; } } void tearDown() { for (int i = 0; i < mReports.length; ++i) { mReports[i] = null; } mPeriodIndex = 0; } @NonNull private String padStringWithSpaces(@NonNull String text, int targetLength) { // Make sure to have at least one space on either side. final int padding = Math.max(2, targetLength - text.length()) >>> 1; return " ".repeat(padding) + text + " ".repeat(padding); } void dump(IndentingPrintWriter pw) { pw.println("Reports:"); pw.increaseIndent(); pw.print(" Total Discharge"); final int statColsLength = 47; pw.print(padStringWithSpaces("Profit (avg/action : avg/discharge)", statColsLength)); pw.print(padStringWithSpaces("Loss (avg/action : avg/discharge)", statColsLength)); 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.println(); for (int r = 0; r < NUM_PERIODS_TO_RETAIN; ++r) { final int idx = (mPeriodIndex - r + NUM_PERIODS_TO_RETAIN) % NUM_PERIODS_TO_RETAIN; final Report report = mReports[idx]; if (report == null) { continue; } pw.print("t-"); pw.print(r); pw.print(": "); pw.print(padStringWithSpaces(Integer.toString(report.cumulativeBatteryDischarge), 15)); if (report.numProfitableActions > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString(report.cumulativeProfit / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativeProfit), cakeToString(report.cumulativeProfit / report.numProfitableActions), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } if (report.numUnprofitableActions > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString(report.cumulativeLoss / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativeLoss), cakeToString(report.cumulativeLoss / report.numUnprofitableActions), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } if (report.numRewards > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString(report.cumulativeRewards / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativeRewards), cakeToString(report.cumulativeRewards / report.numRewards), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } if (report.numPositiveRegulations > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString( report.cumulativePositiveRegulations / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativePositiveRegulations), cakeToString(report.cumulativePositiveRegulations / report.numPositiveRegulations), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } if (report.numNegativeRegulations > 0) { final String perDischarge = report.cumulativeBatteryDischarge > 0 ? cakeToString( report.cumulativeNegativeRegulations / report.cumulativeBatteryDischarge) : "N/A"; pw.print(padStringWithSpaces(String.format("%s (%s : %s)", cakeToString(report.cumulativeNegativeRegulations), cakeToString(report.cumulativeNegativeRegulations / report.numNegativeRegulations), perDischarge), statColsLength)); } else { pw.print(padStringWithSpaces("N/A", statColsLength)); } pw.println(); } pw.decreaseIndent(); } }
apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +9 −2 Original line number Diff line number Diff line Loading @@ -121,6 +121,7 @@ public class InternalResourceService extends SystemService { private IDeviceIdleController mDeviceIdleController; private final Agent mAgent; private final Analyst mAnalyst; private final ConfigObserver mConfigObserver; private final EconomyManagerStub mEconomyManagerStub; private final Scribe mScribe; Loading Loading @@ -254,9 +255,10 @@ public class InternalResourceService extends SystemService { mPackageManager = context.getPackageManager(); mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); mEconomyManagerStub = new EconomyManagerStub(); mScribe = new Scribe(this); mAnalyst = new Analyst(); mScribe = new Scribe(this, mAnalyst); mCompleteEconomicPolicy = new CompleteEconomicPolicy(this); mAgent = new Agent(this, mScribe); mAgent = new Agent(this, mScribe, mAnalyst); mConfigObserver = new ConfigObserver(mHandler, context); Loading Loading @@ -375,6 +377,7 @@ public class InternalResourceService extends SystemService { void onBatteryLevelChanged() { synchronized (mLock) { final int newBatteryLevel = getCurrentBatteryLevel(); mAnalyst.noteBatteryLevelChange(newBatteryLevel); final boolean increased = newBatteryLevel > mCurrentBatteryLevel; if (increased) { mAgent.distributeBasicIncomeLocked(newBatteryLevel); Loading Loading @@ -741,6 +744,7 @@ public class InternalResourceService extends SystemService { } synchronized (mLock) { mAgent.tearDownLocked(); mAnalyst.tearDown(); mCompleteEconomicPolicy.tearDown(); mExemptedApps.clear(); mExemptListLoaded = false; Loading Loading @@ -1145,6 +1149,9 @@ public class InternalResourceService extends SystemService { pw.println(); mAgent.dumpLocked(pw); pw.println(); mAnalyst.dump(pw); } } }
apex/jobscheduler/service/java/com/android/server/tare/Scribe.java +77 −3 Original line number Diff line number Diff line Loading @@ -72,6 +72,7 @@ public class Scribe { private static final String XML_TAG_TARE = "tare"; private static final String XML_TAG_TRANSACTION = "transaction"; private static final String XML_TAG_USER = "user"; private static final String XML_TAG_PERIOD_REPORT = "report"; private static final String XML_ATTR_CTP = "ctp"; private static final String XML_ATTR_DELTA = "delta"; Loading @@ -86,6 +87,18 @@ public class Scribe { private static final String XML_ATTR_LAST_RECLAMATION_TIME = "lastReclamationTime"; 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"; private static final String XML_ATTR_PR_BATTERY_LEVEL = "batteryLevel"; private static final String XML_ATTR_PR_PROFIT = "profit"; private static final String XML_ATTR_PR_NUM_PROFIT = "numProfits"; private static final String XML_ATTR_PR_LOSS = "loss"; private static final String XML_ATTR_PR_NUM_LOSS = "numLoss"; private static final String XML_ATTR_PR_REWARDS = "rewards"; private static final String XML_ATTR_PR_NUM_REWARDS = "numRewards"; private static final String XML_ATTR_PR_POS_REGULATIONS = "posRegulations"; 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"; /** Version of the file schema. */ private static final int STATE_FILE_VERSION = 0; Loading @@ -94,6 +107,7 @@ public class Scribe { private final AtomicFile mStateFile; private final InternalResourceService mIrs; private final Analyst mAnalyst; @GuardedBy("mIrs.getLock()") private long mLastReclamationTime; Loading @@ -107,13 +121,14 @@ public class Scribe { private final Runnable mCleanRunnable = this::cleanupLedgers; private final Runnable mWriteRunnable = this::writeState; Scribe(InternalResourceService irs) { this(irs, Environment.getDataSystemDirectory()); Scribe(InternalResourceService irs, Analyst analyst) { this(irs, analyst, Environment.getDataSystemDirectory()); } @VisibleForTesting Scribe(InternalResourceService irs, File dataDir) { Scribe(InternalResourceService irs, Analyst analyst, File dataDir) { mIrs = irs; mAnalyst = analyst; final File tareDir = new File(dataDir, "tare"); //noinspection ResultOfMethodCallIgnored Loading Loading @@ -210,6 +225,7 @@ public class Scribe { } } final List<Analyst.Report> reports = new ArrayList<>(); try (FileInputStream fis = mStateFile.openRead()) { TypedXmlPullParser parser = Xml.resolvePullParser(fis); Loading Loading @@ -263,11 +279,15 @@ public class Scribe { readUserFromXmlLocked( parser, installedPackagesPerUser, endTimeCutoff)); break; case XML_TAG_PERIOD_REPORT: reports.add(readReportFromXml(parser)); break; default: Slog.e(TAG, "Unexpected tag: " + tagName); break; } } mAnalyst.loadReports(reports); scheduleCleanup(earliestEndTime); } catch (IOException | XmlPullParserException e) { Slog.wtf(TAG, "Error reading state from disk", e); Loading Loading @@ -457,6 +477,37 @@ 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. * @return Newly instantiated Report holding all the information we just read out of the xml tag */ @NonNull private static Analyst.Report readReportFromXml(TypedXmlPullParser parser) throws XmlPullParserException, IOException { final Analyst.Report report = new Analyst.Report(); report.cumulativeBatteryDischarge = parser.getAttributeInt(null, XML_ATTR_PR_DISCHARGE); report.currentBatteryLevel = parser.getAttributeInt(null, XML_ATTR_PR_BATTERY_LEVEL); report.cumulativeProfit = parser.getAttributeLong(null, XML_ATTR_PR_PROFIT); report.numProfitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_PROFIT); report.cumulativeLoss = parser.getAttributeLong(null, XML_ATTR_PR_LOSS); report.numUnprofitableActions = parser.getAttributeInt(null, XML_ATTR_PR_NUM_LOSS); report.cumulativeRewards = parser.getAttributeLong(null, XML_ATTR_PR_REWARDS); report.numRewards = parser.getAttributeInt(null, XML_ATTR_PR_NUM_REWARDS); report.cumulativePositiveRegulations = parser.getAttributeLong(null, XML_ATTR_PR_POS_REGULATIONS); report.numPositiveRegulations = parser.getAttributeInt(null, XML_ATTR_PR_NUM_POS_REGULATIONS); report.cumulativeNegativeRegulations = parser.getAttributeLong(null, XML_ATTR_PR_NEG_REGULATIONS); report.numNegativeRegulations = parser.getAttributeInt(null, XML_ATTR_PR_NUM_NEG_REGULATIONS); return report; } private void scheduleCleanup(long earliestEndTime) { if (earliestEndTime == Long.MAX_VALUE) { return; Loading Loading @@ -501,6 +552,11 @@ public class Scribe { writeUserLocked(out, userId)); } List<Analyst.Report> reports = mAnalyst.getReports(); for (int i = 0, size = reports.size(); i < size; ++i) { writeReport(out, reports.get(i)); } out.endTag(null, XML_TAG_TARE); out.endDocument(); Loading Loading @@ -560,6 +616,24 @@ public class Scribe { out.endTag(null, XML_TAG_TRANSACTION); } private static void writeReport(@NonNull TypedXmlSerializer out, @NonNull Analyst.Report report) throws IOException { out.startTag(null, XML_TAG_PERIOD_REPORT); out.attributeInt(null, XML_ATTR_PR_DISCHARGE, report.cumulativeBatteryDischarge); out.attributeInt(null, XML_ATTR_PR_BATTERY_LEVEL, report.currentBatteryLevel); out.attributeLong(null, XML_ATTR_PR_PROFIT, report.cumulativeProfit); out.attributeInt(null, XML_ATTR_PR_NUM_PROFIT, report.numProfitableActions); out.attributeLong(null, XML_ATTR_PR_LOSS, report.cumulativeLoss); out.attributeInt(null, XML_ATTR_PR_NUM_LOSS, report.numUnprofitableActions); out.attributeLong(null, XML_ATTR_PR_REWARDS, report.cumulativeRewards); out.attributeInt(null, XML_ATTR_PR_NUM_REWARDS, report.numRewards); out.attributeLong(null, XML_ATTR_PR_POS_REGULATIONS, report.cumulativePositiveRegulations); 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.endTag(null, XML_TAG_PERIOD_REPORT); } @GuardedBy("mIrs.getLock()") void dumpLocked(IndentingPrintWriter pw, boolean dumpAll) { pw.println("Ledgers:"); Loading
services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java +8 −6 Original line number Diff line number Diff line Loading @@ -46,6 +46,8 @@ public class AgentTest { @Mock private CompleteEconomicPolicy mEconomicPolicy; @Mock private Analyst mAnalyst; @Mock private Context mContext; @Mock private InternalResourceService mIrs; Loading @@ -53,8 +55,8 @@ public class AgentTest { private Scribe mScribe; private static class MockScribe extends Scribe { MockScribe(InternalResourceService irs) { super(irs); MockScribe(InternalResourceService irs, Analyst analyst) { super(irs, analyst); } @Override Loading @@ -74,7 +76,7 @@ public class AgentTest { doReturn(mEconomicPolicy).when(mIrs).getCompleteEconomicPolicyLocked(); doReturn(mIrs).when(mIrs).getLock(); doReturn(mock(AlarmManager.class)).when(mContext).getSystemService(Context.ALARM_SERVICE); mScribe = new MockScribe(mIrs); mScribe = new MockScribe(mIrs, mAnalyst); } @After Loading @@ -86,7 +88,7 @@ public class AgentTest { @Test public void testRecordTransaction_UnderMax() { Agent agent = new Agent(mIrs, mScribe); Agent agent = new Agent(mIrs, mScribe, mAnalyst); Ledger ledger = new Ledger(); doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked(); Loading Loading @@ -115,7 +117,7 @@ public class AgentTest { @Test public void testRecordTransaction_MaxConsumptionLimit() { Agent agent = new Agent(mIrs, mScribe); Agent agent = new Agent(mIrs, mScribe, mAnalyst); Ledger ledger = new Ledger(); doReturn(1000L).when(mIrs).getConsumptionLimitLocked(); Loading Loading @@ -162,7 +164,7 @@ public class AgentTest { @Test public void testRecordTransaction_MaxSatiatedBalance() { Agent agent = new Agent(mIrs, mScribe); Agent agent = new Agent(mIrs, mScribe, mAnalyst); Ledger ledger = new Ledger(); doReturn(1_000_000L).when(mIrs).getConsumptionLimitLocked(); Loading