Merge "Track basic TARE statistics." into tm-dev

This commit is contained in:
Kweku Adams
2022-05-17 17:47:28 +00:00
committed by Android (Google) Code Review
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));
}
}
}