diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java index f4faec85002ad..8b8a57d248b95 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Agent.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Agent.java @@ -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; @@ -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( @@ -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; } @@ -471,6 +473,7 @@ class Agent { } ledger.recordTransaction(transaction); mScribe.adjustRemainingConsumableCakesLocked(-transaction.ctp); + mAnalyst.noteTransaction(transaction); if (transaction.delta != 0 && notifyOnAffordabilityChange) { final ArraySet actionAffordabilityNotes = mActionAffordabilityNotes.get(userId, pkgName); diff --git a/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java new file mode 100644 index 0000000000000..bc6fe7e5a5352 --- /dev/null +++ b/apex/jobscheduler/service/java/com/android/server/tare/Analyst.java @@ -0,0 +1,290 @@ +/* + * 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 getReports() { + final List 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 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(); + } +} 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 2118eeb45d70a..9c7d702ec6b4e 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/InternalResourceService.java @@ -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; @@ -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); @@ -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); @@ -741,6 +744,7 @@ public class InternalResourceService extends SystemService { } synchronized (mLock) { mAgent.tearDownLocked(); + mAnalyst.tearDown(); mCompleteEconomicPolicy.tearDown(); mExemptedApps.clear(); mExemptListLoaded = false; @@ -1145,6 +1149,9 @@ public class InternalResourceService extends SystemService { pw.println(); mAgent.dumpLocked(pw); + + pw.println(); + mAnalyst.dump(pw); } } } 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 7442877548f1c..941cc39f2d970 100644 --- a/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java +++ b/apex/jobscheduler/service/java/com/android/server/tare/Scribe.java @@ -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"; @@ -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; @@ -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; @@ -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 @@ -210,6 +225,7 @@ public class Scribe { } } + final List reports = new ArrayList<>(); try (FileInputStream fis = mStateFile.openRead()) { TypedXmlPullParser parser = Xml.resolvePullParser(fis); @@ -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); @@ -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; @@ -501,6 +552,11 @@ public class Scribe { writeUserLocked(out, userId)); } + List 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(); @@ -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:"); diff --git a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java index 534d0a1bc76fe..f9f6fe919c3b0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/AgentTest.java @@ -46,6 +46,8 @@ public class AgentTest { @Mock private CompleteEconomicPolicy mEconomicPolicy; @Mock + private Analyst mAnalyst; + @Mock private Context mContext; @Mock private InternalResourceService mIrs; @@ -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 @@ -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 @@ -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(); @@ -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(); @@ -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(); 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 14f95e93577e2..721777ca6b38d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/tare/ScribeTest.java @@ -16,7 +16,7 @@ package com.android.server.tare; - +import static com.android.dx.mockito.inline.extended.ExtendedMockito.inOrder; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static org.junit.Assert.assertEquals; @@ -41,6 +41,8 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; @@ -66,7 +68,10 @@ public class ScribeTest { private Scribe mScribeUnderTest; private File mTestFileDir; private final List mInstalledPackages = new ArrayList<>(); + private final List mReports = new ArrayList<>(); + @Mock + private Analyst mAnalyst; @Mock private InternalResourceService mIrs; @@ -84,11 +89,12 @@ public class ScribeTest { when(mIrs.getLock()).thenReturn(new Object()); when(mIrs.isEnabled()).thenReturn(true); when(mIrs.getInstalledPackages()).thenReturn(mInstalledPackages); + when(mAnalyst.getReports()).thenReturn(mReports); mTestFileDir = new File(getContext().getFilesDir(), "scribe_test"); //noinspection ResultOfMethodCallIgnored mTestFileDir.mkdirs(); Log.d(TAG, "Saving data to '" + mTestFileDir + "'"); - mScribeUnderTest = new Scribe(mIrs, mTestFileDir); + mScribeUnderTest = new Scribe(mIrs, mAnalyst, mTestFileDir); addInstalledPackage(TEST_USER_ID, TEST_PACKAGE); } @@ -104,6 +110,62 @@ public class ScribeTest { } } + @Test + public void testWritingAnalystReportsToDisk() { + ArgumentCaptor> reportCaptor = + ArgumentCaptor.forClass(List.class); + + InOrder inOrder = inOrder(mAnalyst); + + // Empty set + mReports.clear(); + mScribeUnderTest.writeImmediatelyForTesting(); + mScribeUnderTest.loadFromDiskLocked(); + inOrder.verify(mAnalyst).loadReports(reportCaptor.capture()); + List result = reportCaptor.getValue(); + assertReportListsEqual(mReports, result); + + Analyst.Report report1 = new Analyst.Report(); + report1.cumulativeBatteryDischarge = 1; + report1.currentBatteryLevel = 2; + report1.cumulativeProfit = 3; + report1.numProfitableActions = 4; + report1.cumulativeLoss = 5; + report1.numUnprofitableActions = 6; + report1.cumulativeRewards = 7; + report1.numRewards = 8; + report1.cumulativePositiveRegulations = 9; + report1.numPositiveRegulations = 10; + report1.cumulativeNegativeRegulations = 11; + report1.numNegativeRegulations = 12; + mReports.add(report1); + mScribeUnderTest.writeImmediatelyForTesting(); + mScribeUnderTest.loadFromDiskLocked(); + inOrder.verify(mAnalyst).loadReports(reportCaptor.capture()); + result = reportCaptor.getValue(); + assertReportListsEqual(mReports, result); + + Analyst.Report report2 = new Analyst.Report(); + report2.cumulativeBatteryDischarge = 10; + report2.currentBatteryLevel = 20; + report2.cumulativeProfit = 30; + report2.numProfitableActions = 40; + report2.cumulativeLoss = 50; + report2.numUnprofitableActions = 60; + report2.cumulativeRewards = 70; + report2.numRewards = 80; + report2.cumulativePositiveRegulations = 90; + report2.numPositiveRegulations = 100; + report2.cumulativeNegativeRegulations = 110; + report2.numNegativeRegulations = 120; + mReports.add(report2); + mScribeUnderTest.writeImmediatelyForTesting(); + mScribeUnderTest.loadFromDiskLocked(); + inOrder.verify(mAnalyst).loadReports(reportCaptor.capture()); + result = reportCaptor.getValue(); + assertReportListsEqual(mReports, result); + } + @Test public void testWriteHighLevelStateToDisk() { long lastReclamationTime = System.currentTimeMillis(); @@ -277,6 +339,49 @@ public class ScribeTest { } } + private void assertReportListsEqual(List expected, + List actual) { + if (expected == null) { + assertNull(actual); + return; + } + assertNotNull(actual); + assertEquals(expected.size(), actual.size()); + for (int i = 0; i < expected.size(); ++i) { + Analyst.Report eReport = expected.get(i); + Analyst.Report aReport = actual.get(i); + if (eReport == null) { + assertNull(aReport); + continue; + } + assertNotNull(aReport); + assertEquals("Reports #" + i + " cumulativeBatteryDischarge are not equal", + eReport.cumulativeBatteryDischarge, aReport.cumulativeBatteryDischarge); + assertEquals("Reports #" + i + " currentBatteryLevel are not equal", + eReport.currentBatteryLevel, aReport.currentBatteryLevel); + assertEquals("Reports #" + i + " cumulativeProfit are not equal", + eReport.cumulativeProfit, aReport.cumulativeProfit); + assertEquals("Reports #" + i + " numProfitableActions are not equal", + eReport.numProfitableActions, aReport.numProfitableActions); + assertEquals("Reports #" + i + " cumulativeLoss are not equal", + eReport.cumulativeLoss, aReport.cumulativeLoss); + assertEquals("Reports #" + i + " numUnprofitableActions are not equal", + eReport.numUnprofitableActions, aReport.numUnprofitableActions); + assertEquals("Reports #" + i + " cumulativeRewards are not equal", + eReport.cumulativeRewards, aReport.cumulativeRewards); + assertEquals("Reports #" + i + " numRewards are not equal", + eReport.numRewards, aReport.numRewards); + assertEquals("Reports #" + i + " cumulativePositiveRegulations are not equal", + eReport.cumulativePositiveRegulations, aReport.cumulativePositiveRegulations); + assertEquals("Reports #" + i + " numPositiveRegulations are not equal", + eReport.numPositiveRegulations, aReport.numPositiveRegulations); + assertEquals("Reports #" + i + " cumulativeNegativeRegulations are not equal", + eReport.cumulativeNegativeRegulations, aReport.cumulativeNegativeRegulations); + assertEquals("Reports #" + i + " numNegativeRegulations are not equal", + eReport.numNegativeRegulations, aReport.numNegativeRegulations); + } + } + private void assertTransactionsEqual(Ledger.Transaction expected, Ledger.Transaction actual) { if (expected == null) { assertNull(actual); diff --git a/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java b/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java new file mode 100644 index 0000000000000..2b527a261ae15 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/tare/AnalystTest.java @@ -0,0 +1,240 @@ +/* + * 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 org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +/** Test that the Analyst processes transactions correctly. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class AnalystTest { + + @Test + public void testInitialState() { + final Analyst analyst = new Analyst(); + assertEquals(0, analyst.getReports().size()); + } + + @Test + public void testBatteryLevelChange() { + final Analyst analyst = new Analyst(); + + Analyst.Report expected = new Analyst.Report(); + expected.currentBatteryLevel = 55; + analyst.noteBatteryLevelChange(55); + assertEquals(1, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(0)); + + // Discharging + analyst.noteBatteryLevelChange(54); + expected.currentBatteryLevel = 54; + expected.cumulativeBatteryDischarge = 1; + assertEquals(1, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(0)); + analyst.noteBatteryLevelChange(50); + expected.currentBatteryLevel = 50; + expected.cumulativeBatteryDischarge = 5; + assertEquals(1, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(0)); + + // Charging + analyst.noteBatteryLevelChange(51); + expected.currentBatteryLevel = 51; + assertEquals(1, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(0)); + analyst.noteBatteryLevelChange(55); + expected.currentBatteryLevel = 55; + assertEquals(1, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(0)); + + // Reset + analyst.noteBatteryLevelChange(100); + assertEquals(2, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(0)); + expected.currentBatteryLevel = 100; + expected.cumulativeBatteryDischarge = 0; + assertReportsEqual(expected, analyst.getReports().get(1)); + } + + @Test + public void testTransaction() { + runTestTransactions(new Analyst(), new Analyst.Report(), 1); + } + + @Test + public void testTransaction_PeriodChange() { + final Analyst analyst = new Analyst(); + + Analyst.Report expected = new Analyst.Report(); + expected.currentBatteryLevel = 55; + analyst.noteBatteryLevelChange(55); + + runTestTransactions(analyst, expected, 1); + + expected.currentBatteryLevel = 49; + expected.cumulativeBatteryDischarge = 6; + analyst.noteBatteryLevelChange(49); + + runTestTransactions(analyst, expected, 1); + + expected = new Analyst.Report(); + expected.currentBatteryLevel = 100; + analyst.noteBatteryLevelChange(100); + expected.cumulativeBatteryDischarge = 0; + + runTestTransactions(analyst, expected, 2); + } + + private void runTestTransactions(Analyst analyst, Analyst.Report lastExpectedReport, + int numExpectedReports) { + Analyst.Report expected = lastExpectedReport; + + // Profit + analyst.noteTransaction( + new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -51, 1)); + expected.cumulativeProfit += 50; + expected.numProfitableActions += 1; + assertEquals(numExpectedReports, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1)); + + // Loss + analyst.noteTransaction( + new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -51, 100)); + expected.cumulativeLoss += 49; + expected.numUnprofitableActions += 1; + assertEquals(numExpectedReports, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1)); + + // Reward + analyst.noteTransaction( + new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD, null, 51, 0)); + expected.cumulativeRewards += 51; + expected.numRewards += 1; + assertEquals(numExpectedReports, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1)); + + // Regulations + analyst.noteTransaction( + new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, 25, 0)); + expected.cumulativePositiveRegulations += 25; + expected.numPositiveRegulations += 1; + assertEquals(numExpectedReports, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1)); + analyst.noteTransaction( + new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, -25, 0)); + expected.cumulativeNegativeRegulations += 25; + expected.numNegativeRegulations += 1; + assertEquals(numExpectedReports, analyst.getReports().size()); + assertReportsEqual(expected, analyst.getReports().get(numExpectedReports - 1)); + + // No-ops + analyst.noteTransaction( + new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_ACTION, null, -100, 100)); + analyst.noteTransaction( + new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REGULATION, null, 0, 0)); + analyst.noteTransaction( + new Ledger.Transaction(0, 1000, EconomicPolicy.TYPE_REWARD, null, 0, 0)); + assertEquals(numExpectedReports, analyst.getReports().size()); + } + + @Test + public void testLoadReports() { + final Analyst analyst = new Analyst(); + + List expected = new ArrayList<>(); + analyst.loadReports(expected); + assertReportListsEqual(expected, analyst.getReports()); + + Analyst.Report report1 = new Analyst.Report(); + report1.cumulativeBatteryDischarge = 1; + report1.currentBatteryLevel = 2; + report1.cumulativeProfit = 3; + report1.numProfitableActions = 4; + report1.cumulativeLoss = 5; + report1.numUnprofitableActions = 6; + report1.cumulativeRewards = 7; + report1.numRewards = 8; + report1.cumulativePositiveRegulations = 9; + report1.numPositiveRegulations = 10; + report1.cumulativeNegativeRegulations = 11; + report1.numNegativeRegulations = 12; + expected.add(report1); + analyst.loadReports(expected); + assertReportListsEqual(expected, analyst.getReports()); + + Analyst.Report report2 = new Analyst.Report(); + report2.cumulativeBatteryDischarge = 10; + report2.currentBatteryLevel = 20; + report2.cumulativeProfit = 30; + report2.numProfitableActions = 40; + report2.cumulativeLoss = 50; + report2.numUnprofitableActions = 60; + report2.cumulativeRewards = 70; + report2.numRewards = 80; + report2.cumulativePositiveRegulations = 90; + report2.numPositiveRegulations = 100; + report2.cumulativeNegativeRegulations = 110; + report2.numNegativeRegulations = 120; + expected.add(report2); + analyst.loadReports(expected); + assertReportListsEqual(expected, analyst.getReports()); + } + + private void assertReportsEqual(Analyst.Report expected, Analyst.Report actual) { + if (expected == null) { + assertNull(actual); + return; + } + assertNotNull(actual); + assertEquals(expected.cumulativeBatteryDischarge, actual.cumulativeBatteryDischarge); + assertEquals(expected.currentBatteryLevel, actual.currentBatteryLevel); + assertEquals(expected.cumulativeProfit, actual.cumulativeProfit); + assertEquals(expected.numProfitableActions, actual.numProfitableActions); + assertEquals(expected.cumulativeLoss, actual.cumulativeLoss); + assertEquals(expected.numUnprofitableActions, actual.numUnprofitableActions); + assertEquals(expected.cumulativeRewards, actual.cumulativeRewards); + assertEquals(expected.numRewards, actual.numRewards); + assertEquals(expected.cumulativePositiveRegulations, actual.cumulativePositiveRegulations); + assertEquals(expected.numPositiveRegulations, actual.numPositiveRegulations); + assertEquals(expected.cumulativeNegativeRegulations, actual.cumulativeNegativeRegulations); + assertEquals(expected.numNegativeRegulations, actual.numNegativeRegulations); + } + + private void assertReportListsEqual(List expected, + List actual) { + if (expected == null) { + assertNull(actual); + return; + } + assertNotNull(actual); + assertEquals(expected.size(), actual.size()); + for (int i = 0; i < expected.size(); ++i) { + assertReportsEqual(expected.get(i), actual.get(i)); + } + } +}