From 0964a6759b989d96306dd02a8b7f8bd0aaf0b4d4 Mon Sep 17 00:00:00 2001 From: Kweku Adams Date: Fri, 13 May 2022 21:59:10 +0000 Subject: [PATCH] Track basic TARE statistics. Track some basic statistics to see how TARE performs over time. They'll only be kept and viewable locally. We can continue to refine them to determine what's useful and necessary. Bug: 158300259 Test: atest frameworks/base/services/tests/servicestests/src/com/android/server/tare Test: atest frameworks/base/services/tests/mockingservicestests/src/com/android/server/tare Change-Id: Ia408b9fc2b688a850fe8974ac473613711711a15 --- .../java/com/android/server/tare/Agent.java | 7 +- .../java/com/android/server/tare/Analyst.java | 290 ++++++++++++++++++ .../server/tare/InternalResourceService.java | 11 +- .../java/com/android/server/tare/Scribe.java | 80 ++++- .../com/android/server/tare/AgentTest.java | 14 +- .../com/android/server/tare/ScribeTest.java | 109 ++++++- .../com/android/server/tare/AnalystTest.java | 240 +++++++++++++++ 7 files changed, 736 insertions(+), 15 deletions(-) create mode 100644 apex/jobscheduler/service/java/com/android/server/tare/Analyst.java create mode 100644 services/tests/servicestests/src/com/android/server/tare/AnalystTest.java 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)); + } + } +}