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
This commit is contained in:
Kweku Adams
2022-05-13 21:59:10 +00:00
parent 96ad5ccad9
commit 0964a6759b
7 changed files with 736 additions and 15 deletions

View File

@@ -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<ActionAffordabilityNote> actionAffordabilityNotes =
mActionAffordabilityNotes.get(userId, pkgName);

View File

@@ -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<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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<Analyst.Report> 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<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();
@@ -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:");

View File

@@ -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();

View File

@@ -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<PackageInfo> mInstalledPackages = new ArrayList<>();
private final List<Analyst.Report> 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<List<Analyst.Report>> reportCaptor =
ArgumentCaptor.forClass(List.class);
InOrder inOrder = inOrder(mAnalyst);
// Empty set
mReports.clear();
mScribeUnderTest.writeImmediatelyForTesting();
mScribeUnderTest.loadFromDiskLocked();
inOrder.verify(mAnalyst).loadReports(reportCaptor.capture());
List<Analyst.Report> 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<Analyst.Report> expected,
List<Analyst.Report> 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);

View File

@@ -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<Analyst.Report> 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<Analyst.Report> expected,
List<Analyst.Report> 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));
}
}
}