Track ongoing events and record them to the Ledger.

Bug: 158300259
Test: Android builds
Change-Id: I9fd4c17d526618ac284ac2336da9bd2e28ccc820
This commit is contained in:
Kweku Adams
2021-06-23 11:13:11 -07:00
parent 97330667b8
commit 88d0054ea5
3 changed files with 370 additions and 17 deletions

View File

@@ -37,6 +37,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.IndentingPrintWriter;
import android.util.Log;
import android.util.Pair;
@@ -50,6 +51,7 @@ import com.android.server.pm.UserManagerInternal;
import java.util.List;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.function.Consumer;
/**
* Other half of the IRS. The agent handles the nitty gritty details, interacting directly with
@@ -69,6 +71,7 @@ class Agent {
private static final long MAX_TRANSACTION_AGE_MS = 24 * HOUR_IN_MILLIS;
private static final String ALARM_TAG_LEDGER_CLEANUP = "*tare.ledger_cleanup*";
private static final String ALARM_TAG_SOLVENCY_CHECK = "*tare.solvency_check*";
private final Object mLock;
private final CompleteEconomicPolicy mCompleteEconomicPolicy;
@@ -78,6 +81,10 @@ class Agent {
@GuardedBy("mLock")
private final SparseArrayMap<String, Ledger> mLedgers = new SparseArrayMap<>();
@GuardedBy("mLock")
private final SparseArrayMap<String, SparseArrayMap<String, OngoingEvent>>
mCurrentOngoingEvents = new SparseArrayMap<>();
@GuardedBy("mLock")
private long mCurrentNarcsInCirculation;
@@ -88,6 +95,14 @@ class Agent {
private final LedgerCleanupAlarmListener mLedgerCleanupAlarmListener =
new LedgerCleanupAlarmListener();
/**
* Listener to track and manage when apps will cross the solvency threshold (in both
* directions).
*/
@GuardedBy("mLock")
private final SolvencyAlarmListener mSolvencyAlarmListener = new SolvencyAlarmListener();
private static final int MSG_CHECK_BALANCE = 0;
private static final int MSG_CLEAN_LEDGER = 1;
private static final int MSG_SET_ALARMS = 2;
@@ -111,12 +126,42 @@ class Agent {
return ledger;
}
private class TotalDeltaCalculator implements Consumer<OngoingEvent> {
private Ledger mLedger;
private long mNowElapsed;
private long mNow;
private long mTotal;
void reset(@NonNull Ledger ledger, long nowElapsed, long now) {
mLedger = ledger;
mNowElapsed = nowElapsed;
mNow = now;
mTotal = 0;
}
@Override
public void accept(OngoingEvent ongoingEvent) {
mTotal += getActualDeltaLocked(ongoingEvent, mLedger, mNowElapsed, mNow);
}
}
@GuardedBy("mLock")
private final TotalDeltaCalculator mTotalDeltaCalculator = new TotalDeltaCalculator();
/** Get an app's current balance, factoring in any currently ongoing events. */
@GuardedBy("mLock")
long getBalanceLocked(final int userId, @NonNull final String pkgName) {
final Ledger ledger = getLedgerLocked(userId, pkgName);
long balance = ledger.getCurrentBalance();
// TODO: add ongoing events
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents != null) {
final long nowElapsed = SystemClock.elapsedRealtime();
final long now = System.currentTimeMillis();
mTotalDeltaCalculator.reset(ledger, nowElapsed, now);
ongoingEvents.forEach(mTotalDeltaCalculator);
balance += mTotalDeltaCalculator.mTotal;
}
return balance;
}
@@ -125,6 +170,7 @@ class Agent {
final int eventId, @Nullable String tag) {
final long now = System.currentTimeMillis();
final Ledger ledger = getLedgerLocked(userId, pkgName);
final boolean wasSolvent = getBalanceLocked(userId, pkgName) > 0;
final int eventType = getEventType(eventId);
switch (eventType) {
@@ -150,6 +196,156 @@ class Agent {
default:
Slog.w(TAG, "Unsupported event type: " + eventType);
}
scheduleBalanceCheckLocked(userId, pkgName);
final boolean isSolvent = getBalanceLocked(userId, pkgName) > 0;
if (wasSolvent && !isSolvent) {
mIrs.postSolvencyChanged(userId, pkgName, false);
} else if (!wasSolvent && isSolvent) {
mIrs.postSolvencyChanged(userId, pkgName, true);
}
}
@GuardedBy("mLock")
void noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId,
@Nullable String tag, final long startElapsed) {
noteOngoingEventLocked(userId, pkgName, eventId, tag, startElapsed, true);
}
@GuardedBy("mLock")
void noteOngoingEventLocked(final int userId, @NonNull final String pkgName, final int eventId,
@Nullable String tag, final long startElapsed, final boolean updateBalanceCheck) {
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents == null) {
ongoingEvents = new SparseArrayMap<>();
mCurrentOngoingEvents.add(userId, pkgName, ongoingEvents);
}
OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag);
final int eventType = getEventType(eventId);
switch (eventType) {
case TYPE_ACTION:
final long actionCost =
mCompleteEconomicPolicy.getCostOfAction(eventId, userId, pkgName);
if (ongoingEvent == null) {
ongoingEvents.add(eventId, tag,
new OngoingEvent(eventId, tag, null, startElapsed, -actionCost));
} else {
ongoingEvent.refCount++;
}
break;
case TYPE_REWARD:
final EconomicPolicy.Reward reward = mCompleteEconomicPolicy.getReward(eventId);
if (reward != null) {
if (ongoingEvent == null) {
ongoingEvents.add(eventId, tag, new OngoingEvent(
eventId, tag, reward, startElapsed, reward.ongoingRewardPerSecond));
} else {
ongoingEvent.refCount++;
}
}
break;
default:
Slog.w(TAG, "Unsupported event type: " + eventType);
}
if (updateBalanceCheck) {
scheduleBalanceCheckLocked(userId, pkgName);
}
}
@GuardedBy("mLock")
void updateOngoingEventsLocked() {
final long now = System.currentTimeMillis();
final long nowElapsed = SystemClock.elapsedRealtime();
mCurrentOngoingEvents.forEach((userId, pkgName, ongoingEvents) -> {
ongoingEvents.forEach((ongoingEvent) -> {
stopOngoingActionLocked(userId, pkgName, ongoingEvent.eventId,
ongoingEvent.tag, nowElapsed, now, false);
noteOngoingEventLocked(userId, pkgName, ongoingEvent.eventId, ongoingEvent.tag,
nowElapsed, false);
});
scheduleBalanceCheckLocked(userId, pkgName);
});
}
@GuardedBy("mLock")
void updateOngoingEventsLocked(final int userId, @NonNull ArraySet<String> pkgNames) {
final long now = System.currentTimeMillis();
final long nowElapsed = SystemClock.elapsedRealtime();
for (int i = 0; i < pkgNames.size(); ++i) {
final String pkgName = pkgNames.valueAt(i);
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents != null) {
ongoingEvents.forEach((ongoingEvent) -> {
stopOngoingActionLocked(userId, pkgName, ongoingEvent.eventId,
ongoingEvent.tag, nowElapsed, now, false);
noteOngoingEventLocked(userId, pkgName, ongoingEvent.eventId, ongoingEvent.tag,
nowElapsed, false);
});
scheduleBalanceCheckLocked(userId, pkgName);
}
}
}
@GuardedBy("mLock")
void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId,
@Nullable String tag, final long nowElapsed, final long now) {
stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now, true);
}
@GuardedBy("mLock")
void stopOngoingActionLocked(final int userId, @NonNull final String pkgName, final int eventId,
@Nullable String tag, final long nowElapsed, final long now,
final boolean updateBalanceCheck) {
final Ledger ledger = getLedgerLocked(userId, pkgName);
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents == null) {
Slog.wtf(TAG, "No ongoing transactions :/");
return;
}
final OngoingEvent ongoingEvent = ongoingEvents.get(eventId, tag);
if (ongoingEvent == null) {
Slog.wtf(TAG, "Nonexistent ongoing transaction "
+ eventToString(eventId) + (tag == null ? "" : ":" + tag)
+ " for <" + userId + ">" + pkgName + " ended");
return;
}
ongoingEvent.refCount--;
if (ongoingEvent.refCount <= 0) {
final long startElapsed = ongoingEvent.startTimeElapsed;
final long startTime = now - (nowElapsed - startElapsed);
final long actualDelta = getActualDeltaLocked(ongoingEvent, ledger, nowElapsed, now);
recordTransactionLocked(userId, pkgName, ledger,
new Ledger.Transaction(startTime, now, eventId, tag, actualDelta));
ongoingEvents.delete(eventId, tag);
}
if (updateBalanceCheck) {
scheduleBalanceCheckLocked(userId, pkgName);
}
}
@GuardedBy("mLock")
private long getActualDeltaLocked(@NonNull OngoingEvent ongoingEvent, @NonNull Ledger ledger,
long nowElapsed, long now) {
final long startElapsed = ongoingEvent.startTimeElapsed;
final long durationSecs = (nowElapsed - startElapsed) / 1000;
final long computedDelta = durationSecs * ongoingEvent.deltaPerSec;
if (ongoingEvent.reward == null) {
return computedDelta;
}
final long rewardSum = ledger.get24HourSum(ongoingEvent.eventId, now);
return Math.max(0,
Math.min(ongoingEvent.reward.maxDailyReward - rewardSum, computedDelta));
}
@GuardedBy("mLock")
@@ -277,6 +473,7 @@ class Agent {
void onPackageRemovedLocked(final int userId, @NonNull final String pkgName) {
reclaimAssetsLocked(userId, pkgName);
mLedgerCleanupAlarmListener.removeAlarmLocked(userId, pkgName);
mSolvencyAlarmListener.removeAlarmLocked(userId, pkgName);
}
/**
@@ -291,12 +488,14 @@ class Agent {
}
// TODO: delete ledger entry from disk
mLedgers.delete(userId, pkgName);
mCurrentOngoingEvents.delete(userId, pkgName);
}
@GuardedBy("mLock")
void onUserRemovedLocked(final int userId, @NonNull final List<String> pkgNames) {
reclaimAssetsLocked(userId, pkgNames);
mLedgerCleanupAlarmListener.removeAlarmsLocked(userId);
mSolvencyAlarmListener.removeAlarmsLocked(userId);
}
@GuardedBy("mLock")
@@ -306,6 +505,80 @@ class Agent {
}
}
private static class TrendCalculator implements Consumer<OngoingEvent> {
private boolean mSolvent;
/**
* The maximum change in credits per second towards 0 (solvency/insolvency threshold).
* A value of 0 means the current ongoing events will never result in the app crossing the
* solvency threshold.
*/
private long mMaxDeltaPerSecToThreshold;
void reset(boolean solvent) {
mSolvent = solvent;
mMaxDeltaPerSecToThreshold = 0;
}
@Override
public void accept(OngoingEvent ongoingEvent) {
if ((mSolvent && ongoingEvent.deltaPerSec < 0)
|| (!mSolvent && ongoingEvent.deltaPerSec > 0)) {
mMaxDeltaPerSecToThreshold += ongoingEvent.deltaPerSec;
}
}
}
@GuardedBy("mLock")
private final TrendCalculator mTrendCalculator = new TrendCalculator();
@GuardedBy("mLock")
private void scheduleBalanceCheckLocked(final int userId, @NonNull final String pkgName) {
SparseArrayMap<String, OngoingEvent> ongoingEvents =
mCurrentOngoingEvents.get(userId, pkgName);
if (ongoingEvents == null) {
// No ongoing transactions. No reason to schedule
mSolvencyAlarmListener.removeAlarmLocked(userId, pkgName);
return;
}
final long balance = getBalanceLocked(userId, pkgName);
mTrendCalculator.reset(balance > 0);
ongoingEvents.forEach(mTrendCalculator);
if (mTrendCalculator.mMaxDeltaPerSecToThreshold == 0) {
// Will never cross solvency threshold based on current events.
mSolvencyAlarmListener.removeAlarmLocked(userId, pkgName);
return;
}
// The minimum amount of time before this app will cross the solvency threshold.
// Including "-" in the calculation ensures that minSeconds is always non-negative:
// * If balance is negative (or 0), solvent=false, so the maxDeltaPerSecToThreshold is
// positive
// * If balance is positive, solvent=true, so the maxDeltaPerSecToThreshold is negative
final long minSeconds = -balance / mTrendCalculator.mMaxDeltaPerSecToThreshold;
mSolvencyAlarmListener.addAlarmLocked(userId, pkgName,
SystemClock.elapsedRealtime() + minSeconds * 1000);
}
private static class OngoingEvent {
public final long startTimeElapsed;
public final int eventId;
@Nullable
public final String tag;
@Nullable
public final EconomicPolicy.Reward reward;
public final long deltaPerSec;
public int refCount;
OngoingEvent(int eventId, @Nullable String tag,
@Nullable EconomicPolicy.Reward reward, long startTimeElapsed, long deltaPerSec) {
this.startTimeElapsed = startTimeElapsed;
this.eventId = eventId;
this.tag = tag;
this.reward = reward;
this.deltaPerSec = deltaPerSec;
refCount = 1;
}
}
/**
* An {@link AlarmManager.OnAlarmListener} that will queue up all pending alarms and only
* schedule one alarm for the earliest alarm.
@@ -559,6 +832,19 @@ class Agent {
}
}
/** Track when apps will cross the solvency threshold (in both directions). */
private class SolvencyAlarmListener extends AlarmQueueListener {
private SolvencyAlarmListener() {
super(ALARM_TAG_SOLVENCY_CHECK, true, 15_000L);
}
@Override
@GuardedBy("mLock")
protected void processExpiredAlarmLocked(int userId, @NonNull String packageName) {
mHandler.obtainMessage(MSG_CHECK_BALANCE, userId, 0, packageName).sendToTarget();
}
}
private final class AgentHandler extends Handler {
AgentHandler(Looper looper) {
super(looper);
@@ -567,6 +853,24 @@ class Agent {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_CHECK_BALANCE: {
final int userId = msg.arg1;
final String pkgName = (String) msg.obj;
synchronized (mLock) {
final Ledger ledger = getLedgerLocked(userId, pkgName);
final long loggedBalance = ledger.getCurrentBalance();
final long newBalance = getBalanceLocked(userId, pkgName);
if (loggedBalance <= 0 && newBalance > 0) {
mIrs.postSolvencyChanged(userId, pkgName, true);
} else if (loggedBalance > 0 && newBalance <= 0) {
mIrs.postSolvencyChanged(userId, pkgName, false);
} else {
scheduleBalanceCheckLocked(userId, pkgName);
}
}
}
break;
case MSG_CLEAN_LEDGER: {
final int userId = msg.arg1;
final String pkgName = (String) msg.obj;
@@ -580,6 +884,7 @@ class Agent {
case MSG_SET_ALARMS: {
synchronized (mLock) {
mLedgerCleanupAlarmListener.setNextAlarmLocked();
mSolvencyAlarmListener.setNextAlarmLocked();
}
}
break;
@@ -592,6 +897,9 @@ class Agent {
pw.print("Current GDP: ");
pw.println(narcToString(mCurrentNarcsInCirculation));
pw.println();
mSolvencyAlarmListener.dumpLocked(pw);
pw.println();
mLedgerCleanupAlarmListener.dumpLocked(pw);
}

View File

@@ -29,6 +29,7 @@ import android.os.BatteryManagerInternal;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Log;
@@ -213,22 +214,6 @@ public class InternalResourceService extends SystemService {
/ 100;
}
@Nullable
@GuardedBy("mLock")
ArraySet<String> getPackagesForUidLocked(final int uid) {
ArraySet<String> packages = mUidToPackageCache.get(uid);
if (packages == null) {
final String[] pkgs = mPackageManager.getPackagesForUid(uid);
if (pkgs != null) {
for (String pkg : pkgs) {
mUidToPackageCache.add(uid, pkg);
}
packages = mUidToPackageCache.get(uid);
}
}
return packages;
}
void onBatteryLevelChanged() {
synchronized (mLock) {
final int newBatteryLevel = getCurrentBatteryLevel();
@@ -240,6 +225,9 @@ public class InternalResourceService extends SystemService {
}
void onDeviceStateChanged() {
synchronized (mLock) {
mAgent.updateOngoingEventsLocked();
}
}
void onPackageAdded(final int uid, @NonNull final String pkgName) {
@@ -282,6 +270,14 @@ public class InternalResourceService extends SystemService {
}
void onUidStateChanged(final int uid) {
synchronized (mLock) {
final ArraySet<String> pkgNames = getPackagesForUidLocked(uid);
if (pkgNames == null) {
Slog.e(TAG, "Don't have packages for uid " + uid);
} else {
mAgent.updateOngoingEventsLocked(UserHandle.getUserId(uid), pkgNames);
}
}
}
void onUserAdded(final int userId) {
@@ -318,6 +314,22 @@ public class InternalResourceService extends SystemService {
return mBatteryManagerInternal.getBatteryLevel();
}
@Nullable
@GuardedBy("mLock")
private ArraySet<String> getPackagesForUidLocked(final int uid) {
ArraySet<String> packages = mUidToPackageCache.get(uid);
if (packages == null) {
final String[] pkgs = mPackageManager.getPackagesForUid(uid);
if (pkgs != null) {
for (String pkg : pkgs) {
mUidToPackageCache.add(uid, pkg);
}
packages = mUidToPackageCache.get(uid);
}
}
return packages;
}
@GuardedBy("mLock")
private void loadInstalledPackageListLocked() {
mPkgCache = mPackageManager.getInstalledPackages(0);
@@ -384,11 +396,20 @@ public class InternalResourceService extends SystemService {
@Override
public void noteOngoingEventStarted(int userId, @NonNull String pkgName, int eventId,
@Nullable String tag) {
synchronized (mLock) {
final long nowElapsed = SystemClock.elapsedRealtime();
mAgent.noteOngoingEventLocked(userId, pkgName, eventId, tag, nowElapsed);
}
}
@Override
public void noteOngoingEventStopped(int userId, @NonNull String pkgName, int eventId,
@Nullable String tag) {
final long nowElapsed = SystemClock.elapsedRealtime();
final long now = System.currentTimeMillis();
synchronized (mLock) {
mAgent.stopOngoingActionLocked(userId, pkgName, eventId, tag, nowElapsed, now);
}
}
}
}

View File

@@ -157,4 +157,28 @@ public class SparseArrayMap<K, V> {
}
}
}
/**
* @param <K> Any class
* @param <V> Any class
* @hide
*/
public interface TriConsumer<K, V> {
/** Consume the int-K-V tuple. */
void accept(int key, K mapKey, V value);
}
/**
* Iterate through all int-K pairs and operate on all of the values.
* @hide
*/
public void forEach(@NonNull TriConsumer<K, V> consumer) {
for (int iIdx = numMaps() - 1; iIdx >= 0; --iIdx) {
final int i = mData.keyAt(iIdx);
final ArrayMap<K, V> data = mData.valueAt(i);
for (int kIdx = data.size() - 1; kIdx >= 0; --kIdx) {
consumer.accept(i, data.keyAt(kIdx), data.valueAt(kIdx));
}
}
}
}