Merge "Introducing a lazy alarm store"

This commit is contained in:
TreeHugger Robot
2020-11-03 22:37:04 +00:00
committed by Android (Google) Code Review
7 changed files with 379 additions and 52 deletions

View File

@@ -268,6 +268,7 @@ public class AlarmManagerService extends SystemService {
*/
Bundle mIdleOptions;
// TODO(b/172085676): Move inside alarm store.
private final SparseArray<AlarmManager.AlarmClockInfo> mNextAlarmClockForUser =
new SparseArray<>();
private final SparseArray<AlarmManager.AlarmClockInfo> mTmpSparseAlarmClockArray =
@@ -276,6 +277,9 @@ public class AlarmManagerService extends SystemService {
new SparseBooleanArray();
private boolean mNextAlarmClockMayChange;
@GuardedBy("mLock")
private final Runnable mAlarmClockUpdater = () -> mNextAlarmClockMayChange = true;
// May only use on mHandler's thread, locking not required.
private final SparseArray<AlarmManager.AlarmClockInfo> mHandlerSparseAlarmClockArray =
new SparseArray<>();
@@ -410,6 +414,9 @@ public class AlarmManagerService extends SystemService {
private static final String KEY_APP_STANDBY_RESTRICTED_WINDOW =
"app_standby_restricted_window";
@VisibleForTesting
static final String KEY_LAZY_BATCHING = "lazy_batching";
private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
private static final long DEFAULT_MAX_INTERVAL = 365 * DateUtils.DAY_IN_MILLIS;
@@ -432,6 +439,8 @@ public class AlarmManagerService extends SystemService {
private static final int DEFAULT_APP_STANDBY_RESTRICTED_QUOTA = 1;
private static final long DEFAULT_APP_STANDBY_RESTRICTED_WINDOW = MILLIS_IN_DAY;
private static final boolean DEFAULT_LAZY_BATCHING = false;
// Minimum futurity of a new alarm
public long MIN_FUTURITY = DEFAULT_MIN_FUTURITY;
@@ -460,6 +469,8 @@ public class AlarmManagerService extends SystemService {
public int APP_STANDBY_RESTRICTED_QUOTA = DEFAULT_APP_STANDBY_RESTRICTED_QUOTA;
public long APP_STANDBY_RESTRICTED_WINDOW = DEFAULT_APP_STANDBY_RESTRICTED_WINDOW;
public boolean LAZY_BATCHING = DEFAULT_LAZY_BATCHING;
private long mLastAllowWhileIdleWhitelistDuration = -1;
Constants() {
@@ -538,6 +549,14 @@ public class AlarmManagerService extends SystemService {
case KEY_APP_STANDBY_RESTRICTED_WINDOW:
updateStandbyWindowsLocked();
break;
case KEY_LAZY_BATCHING:
final boolean oldLazyBatching = LAZY_BATCHING;
LAZY_BATCHING = properties.getBoolean(
KEY_LAZY_BATCHING, DEFAULT_LAZY_BATCHING);
if (oldLazyBatching != LAZY_BATCHING) {
migrateAlarmsToNewStoreLocked();
}
break;
default:
if (name.startsWith(KEY_PREFIX_STANDBY_QUOTA) && !standbyQuotaUpdated) {
// The quotas need to be updated in order, so we can't just rely
@@ -551,6 +570,15 @@ public class AlarmManagerService extends SystemService {
}
}
private void migrateAlarmsToNewStoreLocked() {
final AlarmStore newStore = LAZY_BATCHING ? new LazyAlarmStore()
: new BatchingAlarmStore();
final ArrayList<Alarm> allAlarms = mAlarmStore.remove((unused) -> true);
newStore.addAll(allAlarms);
mAlarmStore = newStore;
mAlarmStore.setAlarmClockRemovalListener(mAlarmClockUpdater);
}
private void updateStandbyQuotasLocked() {
// The bucket quotas need to be read as an atomic unit but the properties passed to
// onPropertiesChanged may only have one key populated at a time.
@@ -659,6 +687,9 @@ public class AlarmManagerService extends SystemService {
TimeUtils.formatDuration(APP_STANDBY_RESTRICTED_WINDOW, pw);
pw.println();
pw.print(KEY_LAZY_BATCHING, LAZY_BATCHING);
pw.println();
pw.decreaseIndent();
}
@@ -770,7 +801,7 @@ public class AlarmManagerService extends SystemService {
// minimum recurrence period or alarm futurity for us to be able to fuzz it
static final long MIN_FUZZABLE_INTERVAL = 10000;
@GuardedBy("mLock")
final AlarmStore mAlarmStore;
AlarmStore mAlarmStore;
// set to non-null if in idle mode; while in this mode, any alarms we don't want
// to run during this time are rescehduled to go off after this alarm.
@@ -781,7 +812,6 @@ public class AlarmManagerService extends SystemService {
AlarmManagerService(Context context, Injector injector) {
super(context);
mInjector = injector;
mAlarmStore = new BatchingAlarmStore(() -> mNextAlarmClockMayChange = true);
}
public AlarmManagerService(Context context) {
@@ -1219,6 +1249,11 @@ public class AlarmManagerService extends SystemService {
synchronized (mLock) {
mHandler = new AlarmHandler();
mConstants = new Constants();
mAlarmStore = mConstants.LAZY_BATCHING ? new LazyAlarmStore()
: new BatchingAlarmStore();
mAlarmStore.setAlarmClockRemovalListener(mAlarmClockUpdater);
mAppWakeupHistory = new AppWakeupHistory(Constants.DEFAULT_APP_STANDBY_WINDOW);
mNextWakeup = mNextNonWakeup = 0;
@@ -3055,12 +3090,13 @@ public class AlarmManagerService extends SystemService {
static final void dumpAlarmList(IndentingPrintWriter ipw, ArrayList<Alarm> list,
long nowELAPSED, SimpleDateFormat sdf) {
for (int i = list.size() - 1; i >= 0; i--) {
final int n = list.size();
for (int i = n - 1; i >= 0; i--) {
final Alarm a = list.get(i);
final String label = Alarm.typeToString(a.type);
ipw.print(label);
ipw.print(" #");
ipw.print(i);
ipw.print(n - i);
ipw.print(": ");
ipw.println(a);
ipw.increaseIndent();

View File

@@ -39,6 +39,13 @@ public interface AlarmStore {
*/
void add(Alarm a);
/**
* Adds all the given alarms to this store.
*
* @param alarms The alarms to add.
*/
void addAll(ArrayList<Alarm> alarms);
/**
* Removes alarms that pass the given predicate.
*
@@ -47,12 +54,18 @@ public interface AlarmStore {
*/
ArrayList<Alarm> remove(Predicate<Alarm> whichAlarms);
/**
* Set a listener to be invoked whenever an alarm clock is removed by a call to
* {@link #remove(Predicate) remove} from this store.
*/
void setAlarmClockRemovalListener(Runnable listener);
/**
* Gets the earliest alarm with the flag {@link android.app.AlarmManager#FLAG_WAKE_FROM_IDLE}
* based on {@link Alarm#getWhenElapsed()}.
*
* @return An alarm object matching the description above or {@code null} if no such alarm was
* found.
* found.
*/
Alarm getNextWakeFromIdleAlarm();

View File

@@ -41,45 +41,22 @@ import java.util.function.Predicate;
*/
public class BatchingAlarmStore implements AlarmStore {
private ArrayList<Batch> mAlarmBatches = new ArrayList<>();
private final ArrayList<Batch> mAlarmBatches = new ArrayList<>();
private int mSize;
private AlarmClockRemovalListener mAlarmClockRemovalListener;
private Runnable mOnAlarmClockRemoved;
interface Stats {
int REBATCH_ALL_ALARMS = 0;
}
final StatLogger mStatLogger = new StatLogger("Alarm store stats", new String[]{
final StatLogger mStatLogger = new StatLogger("BatchingAlarmStore stats", new String[]{
"REBATCH_ALL_ALARMS",
});
private static final Comparator<Batch> sBatchOrder = (b1, b2) -> {
long when1 = b1.mStart;
long when2 = b2.mStart;
if (when1 > when2) {
return 1;
}
if (when1 < when2) {
return -1;
}
return 0;
};
private static final Comparator<Batch> sBatchOrder = Comparator.comparingLong(b -> b.mStart);
private static final Comparator<Alarm> sIncreasingTimeOrder = (a1, a2) -> {
long when1 = a1.getWhenElapsed();
long when2 = a2.getWhenElapsed();
if (when1 > when2) {
return 1;
}
if (when1 < when2) {
return -1;
}
return 0;
};
BatchingAlarmStore(AlarmClockRemovalListener listener) {
mAlarmClockRemovalListener = listener;
}
private static final Comparator<Alarm> sIncreasingTimeOrder = Comparator.comparingLong(
Alarm::getWhenElapsed);
@Override
public void add(Alarm a) {
@@ -87,6 +64,16 @@ public class BatchingAlarmStore implements AlarmStore {
mSize++;
}
@Override
public void addAll(ArrayList<Alarm> alarms) {
if (alarms == null) {
return;
}
for (final Alarm a : alarms) {
add(a);
}
}
@Override
public ArrayList<Alarm> remove(Predicate<Alarm> whichAlarms) {
final ArrayList<Alarm> removed = new ArrayList<>();
@@ -105,6 +92,11 @@ public class BatchingAlarmStore implements AlarmStore {
return removed;
}
@Override
public void setAlarmClockRemovalListener(Runnable listener) {
mOnAlarmClockRemoved = listener;
}
@Override
public Alarm getNextWakeFromIdleAlarm() {
for (final Batch batch : mAlarmBatches) {
@@ -317,8 +309,8 @@ public class BatchingAlarmStore implements AlarmStore {
Alarm alarm = mAlarms.get(i);
if (predicate.test(alarm)) {
removed.add(mAlarms.remove(i));
if (alarm.alarmClock != null && mAlarmClockRemovalListener != null) {
mAlarmClockRemovalListener.onRemoved();
if (alarm.alarmClock != null && mOnAlarmClockRemoved != null) {
mOnAlarmClockRemoved.run();
}
if (isTimeTickAlarm(alarm)) {
// This code path is not invoked when delivering alarms, only when removing
@@ -388,9 +380,4 @@ public class BatchingAlarmStore implements AlarmStore {
proto.end(token);
}
}
@FunctionalInterface
interface AlarmClockRemovalListener {
void onRemoved();
}
}

View File

@@ -0,0 +1,217 @@
/*
* Copyright (C) 2020 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.alarm;
import static com.android.server.alarm.AlarmManagerService.TAG;
import static com.android.server.alarm.AlarmManagerService.dumpAlarmList;
import static com.android.server.alarm.AlarmManagerService.isTimeTickAlarm;
import android.app.AlarmManager;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import com.android.internal.util.StatLogger;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.function.Predicate;
/**
* Lazy implementation of an alarm store.
* This keeps the alarms in a sorted list, and only batches them at the time of delivery.
*/
public class LazyAlarmStore implements AlarmStore {
private final ArrayList<Alarm> mAlarms = new ArrayList<>();
private Runnable mOnAlarmClockRemoved;
interface Stats {
int GET_NEXT_DELIVERY_TIME = 0;
int GET_NEXT_WAKEUP_DELIVERY_TIME = 1;
}
final StatLogger mStatLogger = new StatLogger("LazyAlarmStore stats", new String[]{
"GET_NEXT_DELIVERY_TIME",
"GET_NEXT_WAKEUP_DELIVERY_TIME",
});
// Decreasing time order because it is more efficient to remove from the tail of an array list.
private static final Comparator<Alarm> sDecreasingTimeOrder = Comparator.comparingLong(
Alarm::getWhenElapsed).reversed();
@Override
public void add(Alarm a) {
int index = Collections.binarySearch(mAlarms, a, sDecreasingTimeOrder);
if (index < 0) {
index = 0 - index - 1;
}
mAlarms.add(index, a);
}
@Override
public void addAll(ArrayList<Alarm> alarms) {
if (alarms == null) {
return;
}
mAlarms.addAll(alarms);
Collections.sort(alarms, sDecreasingTimeOrder);
}
@Override
public ArrayList<Alarm> remove(Predicate<Alarm> whichAlarms) {
final ArrayList<Alarm> removedAlarms = new ArrayList<>();
for (int i = mAlarms.size() - 1; i >= 0; i--) {
if (whichAlarms.test(mAlarms.get(i))) {
final Alarm removed = mAlarms.remove(i);
if (removed.alarmClock != null && mOnAlarmClockRemoved != null) {
mOnAlarmClockRemoved.run();
}
if (isTimeTickAlarm(removed)) {
// This code path is not invoked when delivering alarms, only when removing
// alarms due to the caller cancelling it or getting uninstalled, etc.
Slog.wtf(TAG, "Removed TIME_TICK alarm");
}
removedAlarms.add(removed);
}
}
return removedAlarms;
}
@Override
public void setAlarmClockRemovalListener(Runnable listener) {
mOnAlarmClockRemoved = listener;
}
@Override
public Alarm getNextWakeFromIdleAlarm() {
for (int i = mAlarms.size() - 1; i >= 0; i--) {
final Alarm alarm = mAlarms.get(i);
if ((alarm.flags & AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
return alarm;
}
}
return null;
}
@Override
public int size() {
return mAlarms.size();
}
@Override
public long getNextWakeupDeliveryTime() {
final long start = mStatLogger.getTime();
long nextWakeup = 0;
for (int i = mAlarms.size() - 1; i >= 0; i--) {
final Alarm a = mAlarms.get(i);
if (!a.wakeup) {
continue;
}
if (nextWakeup == 0) {
nextWakeup = a.getMaxWhenElapsed();
} else {
if (a.getWhenElapsed() > nextWakeup) {
break;
}
nextWakeup = Math.min(nextWakeup, a.getMaxWhenElapsed());
}
}
mStatLogger.logDurationStat(Stats.GET_NEXT_WAKEUP_DELIVERY_TIME, start);
return nextWakeup;
}
@Override
public long getNextDeliveryTime() {
final long start = mStatLogger.getTime();
final int n = mAlarms.size();
if (n == 0) {
return 0;
}
long nextDelivery = mAlarms.get(n - 1).getMaxWhenElapsed();
for (int i = n - 2; i >= 0; i--) {
final Alarm a = mAlarms.get(i);
if (a.getWhenElapsed() > nextDelivery) {
break;
}
nextDelivery = Math.min(nextDelivery, a.getMaxWhenElapsed());
}
mStatLogger.logDurationStat(Stats.GET_NEXT_DELIVERY_TIME, start);
return nextDelivery;
}
@Override
public ArrayList<Alarm> removePendingAlarms(long nowElapsed) {
final ArrayList<Alarm> pending = new ArrayList<>();
final ArrayList<Alarm> standAlones = new ArrayList<>();
for (int i = mAlarms.size() - 1; i >= 0; i--) {
final Alarm alarm = mAlarms.get(i);
if (alarm.getWhenElapsed() > nowElapsed) {
break;
}
pending.add(alarm);
if ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) {
standAlones.add(alarm);
}
}
if (!standAlones.isEmpty()) {
// If there are deliverable standalone alarms, others must not go out yet.
mAlarms.removeAll(standAlones);
return standAlones;
}
mAlarms.removeAll(pending);
return pending;
}
@Override
public boolean updateAlarmDeliveries(AlarmDeliveryCalculator deliveryCalculator) {
boolean changed = false;
for (final Alarm alarm : mAlarms) {
changed |= deliveryCalculator.updateAlarmDelivery(alarm);
}
if (changed) {
Collections.sort(mAlarms, sDecreasingTimeOrder);
}
return changed;
}
@Override
public ArrayList<Alarm> asList() {
final ArrayList<Alarm> copy = new ArrayList<>(mAlarms);
Collections.reverse(copy);
return copy;
}
@Override
public void dump(IndentingPrintWriter ipw, long nowElapsed, SimpleDateFormat sdf) {
ipw.println(mAlarms.size() + " pending alarms: ");
ipw.increaseIndent();
dumpAlarmList(ipw, mAlarms, nowElapsed, sdf);
ipw.decreaseIndent();
mStatLogger.dump(ipw);
}
@Override
public void dumpProto(ProtoOutputStream pos, long nowElapsed) {
for (final Alarm a : mAlarms) {
a.dumpDebug(pos, AlarmManagerServiceDumpProto.PENDING_ALARMS, nowElapsed);
}
}
}

View File

@@ -144,6 +144,8 @@ message AlarmManagerServiceDumpProto {
repeated IdleDispatchEntryProto allow_while_idle_dispatches = 40;
repeated WakeupEventProto recent_wakeup_history = 41;
repeated AlarmProto pending_alarms = 42;
}
// This is a soft wrapper for alarm clock information. It is not representative

View File

@@ -47,6 +47,7 @@ import static com.android.server.alarm.AlarmManagerService.AlarmHandler.REMOVE_F
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_LONG_TIME;
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_SHORT_TIME;
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION;
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LAZY_BATCHING;
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_LISTENER_TIMEOUT;
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MAX_INTERVAL;
import static com.android.server.alarm.AlarmManagerService.Constants.KEY_MIN_FUTURITY;
@@ -62,6 +63,7 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
@@ -365,7 +367,7 @@ public class AlarmManagerServiceTest {
}
private void setIdleUntilAlarm(int type, long triggerTime, PendingIntent pi) {
setTestAlarm(type, triggerTime, pi, 0, FLAG_IDLE_UNTIL, TEST_CALLING_UID);
setTestAlarm(type, triggerTime, pi, 0, FLAG_IDLE_UNTIL | FLAG_STANDALONE, TEST_CALLING_UID);
}
private void setWakeFromIdle(int type, long triggerTime, PendingIntent pi) {
@@ -410,6 +412,12 @@ public class AlarmManagerServiceTest {
mService.mConstants.onPropertiesChanged(mDeviceConfigProperties);
}
private void setDeviceConfigBoolean(String key, boolean val) {
mDeviceConfigKeys.add(key);
doReturn(val).when(mDeviceConfigProperties).getBoolean(eq(key), anyBoolean());
mService.mConstants.onPropertiesChanged(mDeviceConfigProperties);
}
/**
* Lowers quotas to make testing feasible. Careful while calling as this will replace any
* existing settings for the calling test.
@@ -1382,6 +1390,35 @@ public class AlarmManagerServiceTest {
}
}
@Test
public void alarmStoreMigration() {
setDeviceConfigBoolean(KEY_LAZY_BATCHING, false);
final int numAlarms = 10;
final PendingIntent[] pis = new PendingIntent[numAlarms];
for (int i = 0; i < numAlarms; i++) {
pis[i] = getNewMockPendingIntent();
setTestAlarm(ELAPSED_REALTIME, mNowElapsedTest + i + 1, pis[i]);
}
final ArrayList<Alarm> alarmsBefore = mService.mAlarmStore.asList();
assertEquals(numAlarms, alarmsBefore.size());
for (int i = 0; i < numAlarms; i++) {
final PendingIntent pi = pis[i];
assertTrue(i + "th PendingIntent missing: ",
alarmsBefore.removeIf(a -> a.matches(pi, null)));
}
setDeviceConfigBoolean(KEY_LAZY_BATCHING, true);
final ArrayList<Alarm> alarmsAfter = mService.mAlarmStore.asList();
assertEquals(numAlarms, alarmsAfter.size());
for (int i = 0; i < numAlarms; i++) {
final PendingIntent pi = pis[i];
assertTrue(i + "th PendingIntent missing: ",
alarmsAfter.removeIf(a -> a.matches(pi, null)));
}
}
@After
public void tearDown() {
if (mMockingSession != null) {

View File

@@ -16,6 +16,9 @@
package com.android.server.alarm;
import static android.app.AlarmManager.ELAPSED_REALTIME;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
import static com.android.server.alarm.Constants.TEST_CALLING_PACKAGE;
import static com.android.server.alarm.Constants.TEST_CALLING_UID;
@@ -23,35 +26,50 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.platform.test.annotations.Presubmit;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import java.util.ArrayList;
@Presubmit
@RunWith(AndroidJUnit4.class)
@RunWith(Parameterized.class)
public class AlarmStoreTest {
private AlarmStore mAlarmStore;
@Before
public void setUp() {
mAlarmStore = new BatchingAlarmStore(null);
@Parameter
public AlarmStore mAlarmStore;
@Parameters
public static Object[] stores() {
return new AlarmStore[]{
new LazyAlarmStore(),
new BatchingAlarmStore(),
};
}
private static Alarm createAlarm(long whenElapsed, long windowLength) {
return createAlarm(AlarmManager.ELAPSED_REALTIME, whenElapsed, windowLength, 0);
return createAlarm(ELAPSED_REALTIME, whenElapsed, windowLength, 0);
}
private static Alarm createWakeupAlarm(long whenElapsed, long windowLength, int flags) {
return createAlarm(AlarmManager.ELAPSED_REALTIME_WAKEUP, whenElapsed, windowLength, flags);
return createAlarm(ELAPSED_REALTIME_WAKEUP, whenElapsed, windowLength, flags);
}
private static Alarm createAlarmClock(long whenElapsed) {
final AlarmManager.AlarmClockInfo info = new AlarmManager.AlarmClockInfo(whenElapsed,
mock(PendingIntent.class));
return new Alarm(ELAPSED_REALTIME_WAKEUP, whenElapsed, whenElapsed, 0, 0,
mock(PendingIntent.class), null, null, null, 0, info, TEST_CALLING_UID,
TEST_CALLING_PACKAGE);
}
private static Alarm createAlarm(int type, long whenElapsed, long windowLength, int flags) {
@@ -206,4 +224,21 @@ public class AlarmStoreTest {
});
assertEquals(7, mAlarmStore.getNextDeliveryTime());
}
@Test
public void alarmClockRemovalListener() {
final Runnable onRemoved = mock(Runnable.class);
mAlarmStore.setAlarmClockRemovalListener(onRemoved);
final Alarm simpleAlarm = createAlarm(5, 0);
final Alarm alarmClock = createAlarmClock(10);
addAlarmsToStore(simpleAlarm, alarmClock);
mAlarmStore.remove(simpleAlarm::equals);
verifyZeroInteractions(onRemoved);
mAlarmStore.remove(alarmClock::equals);
verify(onRemoved).run();
}
}