summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java2
-rw-r--r--apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java262
-rw-r--r--core/proto/android/server/jobscheduler.proto22
-rw-r--r--services/core/java/com/android/server/utils/quota/QuotaTracker.java1
4 files changed, 184 insertions, 103 deletions
diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
index b86aba687a64..8c08e757e405 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java
@@ -3034,7 +3034,7 @@ public class JobSchedulerService extends com.android.server.SystemService
}
void resetExecutionQuota(@NonNull String pkgName, int userId) {
- mQuotaController.clearAppStats(pkgName, userId);
+ mQuotaController.clearAppStats(userId, pkgName);
}
void resetScheduleQuota() {
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index 4393a95af5ba..7256371102f5 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -54,12 +54,14 @@ import android.provider.Settings;
import android.util.ArraySet;
import android.util.KeyValueListParser;
import android.util.Log;
+import android.util.Pair;
import android.util.Slog;
import android.util.SparseArrayMap;
import android.util.SparseBooleanArray;
import android.util.SparseSetArray;
import android.util.proto.ProtoOutputStream;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.IndentingPrintWriter;
@@ -74,6 +76,7 @@ import com.android.server.usage.AppStandbyInternal.AppIdleStateChangeListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import java.util.PriorityQueue;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -301,10 +304,10 @@ public final class QuotaController extends StateController {
private final SparseArrayMap<List<TimingSession>> mTimingSessions = new SparseArrayMap<>();
/**
- * List of alarm listeners for each package that listen for when each package comes back within
- * quota.
+ * Listener to track and manage when each package comes back within quota.
*/
- private final SparseArrayMap<QcAlarmListener> mInQuotaAlarmListeners = new SparseArrayMap<>();
+ @GuardedBy("mLock")
+ private final InQuotaAlarmListener mInQuotaAlarmListener = new InQuotaAlarmListener();
/** Cached calculation results for each app, with the standby buckets as the array indices. */
private final SparseArrayMap<ExecutionStats[]> mExecutionStatsCache = new SparseArrayMap<>();
@@ -579,7 +582,7 @@ public final class QuotaController extends StateController {
Slog.wtf(TAG, "Told app removed but given null package name.");
return;
}
- clearAppStats(packageName, UserHandle.getUserId(uid));
+ clearAppStats(UserHandle.getUserId(uid), packageName);
mForegroundUids.delete(uid);
mUidToPackageCache.remove(uid);
}
@@ -589,13 +592,13 @@ public final class QuotaController extends StateController {
mTrackedJobs.delete(userId);
mPkgTimers.delete(userId);
mTimingSessions.delete(userId);
- mInQuotaAlarmListeners.delete(userId);
+ mInQuotaAlarmListener.removeAlarmsLocked(userId);
mExecutionStatsCache.delete(userId);
mUidToPackageCache.clear();
}
/** Drop all historical stats and stop tracking any active sessions for the specified app. */
- public void clearAppStats(@NonNull String packageName, int userId) {
+ public void clearAppStats(int userId, @NonNull String packageName) {
mTrackedJobs.delete(userId, packageName);
Timer timer = mPkgTimers.get(userId, packageName);
if (timer != null) {
@@ -606,11 +609,7 @@ public final class QuotaController extends StateController {
mPkgTimers.delete(userId, packageName);
}
mTimingSessions.delete(userId, packageName);
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
- if (alarmListener != null) {
- mAlarmManager.cancel(alarmListener);
- mInQuotaAlarmListeners.delete(userId, packageName);
- }
+ mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
mExecutionStatsCache.delete(userId, packageName);
}
@@ -1208,12 +1207,7 @@ public final class QuotaController extends StateController {
// exempted.
maybeScheduleStartAlarmLocked(userId, packageName, realStandbyBucket);
} else {
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
- if (alarmListener != null && alarmListener.isWaiting()) {
- mAlarmManager.cancel(alarmListener);
- // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
- alarmListener.setTriggerTime(0);
- }
+ mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
}
return changed;
}
@@ -1229,12 +1223,7 @@ public final class QuotaController extends StateController {
final String packageName = jobStatus.getSourcePackageName();
final int realStandbyBucket = jobStatus.getStandbyBucket();
if (isWithinQuotaLocked(userId, packageName, realStandbyBucket)) {
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
- if (alarmListener != null && alarmListener.isWaiting()) {
- mAlarmManager.cancel(alarmListener);
- // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
- alarmListener.setTriggerTime(0);
- }
+ mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
} else {
mToScheduleStartAlarms.add(userId, packageName, realStandbyBucket);
}
@@ -1285,7 +1274,6 @@ public final class QuotaController extends StateController {
final boolean isUnderTimingSessionCountQuota = isUnderSessionCountQuotaLocked(stats,
standbyBucket);
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, packageName);
if (stats.executionTimeInWindowMs < mAllowedTimePerPeriodMs
&& stats.executionTimeInMaxPeriodMs < mMaxExecutionTimeMs
&& isUnderJobCountQuota
@@ -1297,21 +1285,11 @@ public final class QuotaController extends StateController {
+ getRemainingExecutionTimeLocked(userId, packageName, standbyBucket)
+ "ms in its quota.");
}
- if (alarmListener != null) {
- // Cancel any pending alarm.
- mAlarmManager.cancel(alarmListener);
- // Set the trigger time to 0 so that the alarm doesn't think it's still waiting.
- alarmListener.setTriggerTime(0);
- }
+ mInQuotaAlarmListener.removeAlarmLocked(userId, packageName);
mHandler.obtainMessage(MSG_CHECK_PACKAGE, userId, 0, packageName).sendToTarget();
return;
}
- if (alarmListener == null) {
- alarmListener = new QcAlarmListener(userId, packageName);
- mInQuotaAlarmListeners.add(userId, packageName, alarmListener);
- }
-
// The time this app will have quota again.
long inQuotaTimeElapsed = stats.inQuotaTimeElapsed;
if (!isUnderJobCountQuota && stats.bgJobCountInWindow < stats.jobCountLimit) {
@@ -1325,27 +1303,7 @@ public final class QuotaController extends StateController {
inQuotaTimeElapsed = Math.max(inQuotaTimeElapsed,
stats.sessionRateLimitExpirationTimeElapsed);
}
- // Only schedule the alarm if:
- // 1. There isn't one currently scheduled
- // 2. The new alarm is significantly earlier than the previous alarm (which could be the
- // case if the package moves into a higher standby bucket). If it's earlier but not
- // significantly so, then we essentially delay the job a few extra minutes.
- // 3. The alarm is after the current alarm by more than the quota buffer.
- // TODO: this might be overengineering. Simplify if proven safe.
- if (!alarmListener.isWaiting()
- || inQuotaTimeElapsed < alarmListener.getTriggerTimeElapsed() - 3 * MINUTE_IN_MILLIS
- || alarmListener.getTriggerTimeElapsed() < inQuotaTimeElapsed) {
- if (DEBUG) {
- Slog.d(TAG, "Scheduling start alarm for " + pkgString);
- }
- // If the next time this app will have quota is at least 3 minutes before the
- // alarm is supposed to go off, reschedule the alarm.
- mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, inQuotaTimeElapsed,
- ALARM_TAG_QUOTA_CHECK, alarmListener, mHandler);
- alarmListener.setTriggerTime(inQuotaTimeElapsed);
- } else if (DEBUG) {
- Slog.d(TAG, "No need to schedule start alarm for " + pkgString);
- }
+ mInQuotaAlarmListener.addAlarmLocked(userId, packageName, inQuotaTimeElapsed);
}
private boolean setConstraintSatisfied(@NonNull JobStatus jobStatus, boolean isWithinQuota) {
@@ -1875,32 +1833,161 @@ public final class QuotaController extends StateController {
}
}
- private class QcAlarmListener implements AlarmManager.OnAlarmListener {
- private final int mUserId;
- private final String mPackageName;
- private volatile long mTriggerTimeElapsed;
+ static class AlarmQueue extends PriorityQueue<Pair<Package, Long>> {
+ AlarmQueue() {
+ super(1, (o1, o2) -> (int) (o1.second - o2.second));
+ }
+
+ /**
+ * Remove any instances of the Package from the queue.
+ *
+ * @return true if an instance was removed, false otherwise.
+ */
+ boolean remove(@NonNull Package pkg) {
+ boolean removed = false;
+ Pair[] alarms = toArray(new Pair[size()]);
+ for (int i = alarms.length - 1; i >= 0; --i) {
+ if (pkg.equals(alarms[i].first)) {
+ remove(alarms[i]);
+ removed = true;
+ }
+ }
+ return removed;
+ }
+ }
+
+ /** Track when UPTCs are expected to come back into quota. */
+ private class InQuotaAlarmListener implements AlarmManager.OnAlarmListener {
+ @GuardedBy("mLock")
+ private final AlarmQueue mAlarmQueue = new AlarmQueue();
+ /** The next time the alarm is set to go off, in the elapsed realtime timebase. */
+ @GuardedBy("mLock")
+ private long mTriggerTimeElapsed = 0;
+
+ @GuardedBy("mLock")
+ void addAlarmLocked(int userId, @NonNull String pkgName, long inQuotaTimeElapsed) {
+ final Package pkg = new Package(userId, pkgName);
+ mAlarmQueue.remove(pkg);
+ mAlarmQueue.offer(new Pair<>(pkg, inQuotaTimeElapsed));
+ setNextAlarmLocked();
+ }
- QcAlarmListener(int userId, String packageName) {
- mUserId = userId;
- mPackageName = packageName;
+ @GuardedBy("mLock")
+ void removeAlarmLocked(@NonNull Package pkg) {
+ if (mAlarmQueue.remove(pkg)) {
+ setNextAlarmLocked();
+ }
}
- boolean isWaiting() {
- return mTriggerTimeElapsed > 0;
+ @GuardedBy("mLock")
+ void removeAlarmLocked(int userId, @NonNull String packageName) {
+ removeAlarmLocked(new Package(userId, packageName));
}
- void setTriggerTime(long timeElapsed) {
- mTriggerTimeElapsed = timeElapsed;
+ @GuardedBy("mLock")
+ void removeAlarmsLocked(int userId) {
+ boolean removed = false;
+ Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+ for (int i = alarms.length - 1; i >= 0; --i) {
+ final Package pkg = (Package) alarms[i].first;
+ if (userId == pkg.userId) {
+ mAlarmQueue.remove(alarms[i]);
+ removed = true;
+ }
+ }
+ if (removed) {
+ setNextAlarmLocked();
+ }
}
- long getTriggerTimeElapsed() {
- return mTriggerTimeElapsed;
+ @GuardedBy("mLock")
+ private void setNextAlarmLocked() {
+ if (mAlarmQueue.size() > 0) {
+ final long nextTriggerTimeElapsed = mAlarmQueue.peek().second;
+ // Only schedule the alarm if one of the following is true:
+ // 1. There isn't one currently scheduled
+ // 2. The new alarm is significantly earlier than the previous alarm. If it's
+ // earlier but not significantly so, then we essentially delay the job a few extra
+ // minutes.
+ // 3. The alarm is after the current alarm.
+ if (mTriggerTimeElapsed == 0
+ || nextTriggerTimeElapsed < mTriggerTimeElapsed - 3 * MINUTE_IN_MILLIS
+ || mTriggerTimeElapsed < nextTriggerTimeElapsed) {
+ if (DEBUG) {
+ Slog.d(TAG, "Scheduling start alarm at " + nextTriggerTimeElapsed);
+ }
+ mAlarmManager.set(AlarmManager.ELAPSED_REALTIME, nextTriggerTimeElapsed,
+ ALARM_TAG_QUOTA_CHECK, this, mHandler);
+ mTriggerTimeElapsed = nextTriggerTimeElapsed;
+ }
+ } else {
+ mAlarmManager.cancel(this);
+ mTriggerTimeElapsed = 0;
+ }
}
@Override
public void onAlarm() {
- mHandler.obtainMessage(MSG_CHECK_PACKAGE, mUserId, 0, mPackageName).sendToTarget();
- mTriggerTimeElapsed = 0;
+ synchronized (mLock) {
+ while (mAlarmQueue.size() > 0) {
+ final Pair<Package, Long> alarm = mAlarmQueue.peek();
+ if (alarm.second <= sElapsedRealtimeClock.millis()) {
+ mHandler.obtainMessage(MSG_CHECK_PACKAGE, alarm.first.userId, 0,
+ alarm.first.packageName).sendToTarget();
+ mAlarmQueue.remove(alarm);
+ } else {
+ break;
+ }
+ }
+ setNextAlarmLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ void dumpLocked(IndentingPrintWriter pw) {
+ pw.println("In quota alarms:");
+ pw.increaseIndent();
+
+ if (mAlarmQueue.size() == 0) {
+ pw.println("NOT WAITING");
+ } else {
+ Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+ for (int i = 0; i < alarms.length; ++i) {
+ final Package pkg = (Package) alarms[i].first;
+ pw.print(pkg);
+ pw.print(": ");
+ pw.print(alarms[i].second);
+ pw.println();
+ }
+ }
+
+ pw.decreaseIndent();
+ }
+
+ @GuardedBy("mLock")
+ void dumpLocked(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(
+ StateControllerProto.QuotaController.InQuotaAlarmListener.TRIGGER_TIME_ELAPSED,
+ mTriggerTimeElapsed);
+
+ Pair[] alarms = mAlarmQueue.toArray(new Pair[mAlarmQueue.size()]);
+ for (int i = 0; i < alarms.length; ++i) {
+ final long aToken = proto.start(
+ StateControllerProto.QuotaController.InQuotaAlarmListener.ALARMS);
+
+ final Package pkg = (Package) alarms[i].first;
+ pkg.dumpDebug(proto,
+ StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.PKG);
+ proto.write(
+ StateControllerProto.QuotaController.InQuotaAlarmListener.Alarm.IN_QUOTA_TIME_ELAPSED,
+ (Long) alarms[i].second);
+
+ proto.end(aToken);
+ }
+
+ proto.end(token);
}
}
@@ -2618,23 +2705,7 @@ public final class QuotaController extends StateController {
pw.decreaseIndent();
pw.println();
- pw.println("In quota alarms:");
- pw.increaseIndent();
- for (int u = 0; u < mInQuotaAlarmListeners.numMaps(); ++u) {
- final int userId = mInQuotaAlarmListeners.keyAt(u);
- for (int p = 0; p < mInQuotaAlarmListeners.numElementsForKey(userId); ++p) {
- final String pkgName = mInQuotaAlarmListeners.keyAt(u, p);
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.valueAt(u, p);
-
- pw.print(string(userId, pkgName));
- pw.print(": ");
- if (alarmListener.isWaiting()) {
- pw.println(alarmListener.getTriggerTimeElapsed());
- } else {
- pw.println("NOT WAITING");
- }
- }
- }
+ mInQuotaAlarmListener.dumpLocked(pw);
pw.decreaseIndent();
}
@@ -2768,22 +2839,13 @@ public final class QuotaController extends StateController {
}
}
- QcAlarmListener alarmListener = mInQuotaAlarmListeners.get(userId, pkgName);
- if (alarmListener != null) {
- final long alToken = proto.start(
- StateControllerProto.QuotaController.PackageStats.IN_QUOTA_ALARM_LISTENER);
- proto.write(StateControllerProto.QuotaController.AlarmListener.IS_WAITING,
- alarmListener.isWaiting());
- proto.write(
- StateControllerProto.QuotaController.AlarmListener.TRIGGER_TIME_ELAPSED,
- alarmListener.getTriggerTimeElapsed());
- proto.end(alToken);
- }
-
proto.end(psToken);
}
}
+ mInQuotaAlarmListener.dumpLocked(proto,
+ StateControllerProto.QuotaController.IN_QUOTA_ALARM_LISTENER);
+
proto.end(mToken);
proto.end(token);
}
diff --git a/core/proto/android/server/jobscheduler.proto b/core/proto/android/server/jobscheduler.proto
index 4bef2e38ad55..bd1bae6d83fb 100644
--- a/core/proto/android/server/jobscheduler.proto
+++ b/core/proto/android/server/jobscheduler.proto
@@ -670,7 +670,7 @@ message StateControllerProto {
repeated ExecutionStats execution_stats = 4;
- optional AlarmListener in_quota_alarm_listener = 5;
+ reserved 5; // in_quota_alarm_listener
}
repeated PackageStats package_stats = 5;
@@ -683,7 +683,25 @@ message StateControllerProto {
}
repeated UidPackageMapping uid_to_package_cache = 7;
- // Next tag: 8
+ message InQuotaAlarmListener {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+ // The time at which the alarm is set to go off, in the elapsed realtime timebase.
+ optional int64 trigger_time_elapsed = 1;
+
+ message Alarm {
+ option (.android.msg_privacy).dest = DEST_AUTOMATIC;
+
+ optional Package pkg = 1;
+
+ // The time at which the package will be in quota, in the elapsed realtime timebase.
+ optional int64 in_quota_time_elapsed = 2;
+ }
+ repeated Alarm alarms = 2;
+ }
+ optional InQuotaAlarmListener in_quota_alarm_listener = 8;
+
+ // Next tag: 9
}
message StorageController {
option (.android.msg_privacy).dest = DEST_AUTOMATIC;
diff --git a/services/core/java/com/android/server/utils/quota/QuotaTracker.java b/services/core/java/com/android/server/utils/quota/QuotaTracker.java
index a8cf9f6c0ec4..115b5c8d4010 100644
--- a/services/core/java/com/android/server/utils/quota/QuotaTracker.java
+++ b/services/core/java/com/android/server/utils/quota/QuotaTracker.java
@@ -552,6 +552,7 @@ abstract class QuotaTracker {
mTriggerTimeElapsed = nextTriggerTimeElapsed;
}
} else {
+ cancelAlarm(this);
mTriggerTimeElapsed = 0;
}
}