blob: 1a4efb8ffcd9de86e3aa4596b353a00650a7cd7f [file] [log] [blame]
/*
* 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.DEBUG_BATCH;
import static com.android.server.alarm.AlarmManagerService.clampPositive;
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.annotations.VisibleForTesting;
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;
/**
* Batching implementation of an Alarm Store.
* This keeps the alarms in batches, which are sorted on the start time of their delivery window.
*/
public class BatchingAlarmStore implements AlarmStore {
@VisibleForTesting
static final String TAG = BatchingAlarmStore.class.getSimpleName();
private final ArrayList<Batch> mAlarmBatches = new ArrayList<>();
private int mSize;
private Runnable mOnAlarmClockRemoved;
interface Stats {
int REBATCH_ALL_ALARMS = 0;
int GET_COUNT = 1;
}
final StatLogger mStatLogger = new StatLogger(TAG + " stats", new String[]{
"REBATCH_ALL_ALARMS",
"GET_COUNT",
});
private static final Comparator<Batch> sBatchOrder = Comparator.comparingLong(b -> b.mStart);
private static final Comparator<Alarm> sIncreasingTimeOrder = Comparator.comparingLong(
Alarm::getWhenElapsed);
@Override
public void add(Alarm a) {
insertAndBatchAlarm(a);
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<>();
for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
final Batch b = mAlarmBatches.get(i);
removed.addAll(b.remove(whichAlarms));
if (b.size() == 0) {
mAlarmBatches.remove(i);
}
}
if (!removed.isEmpty()) {
mSize -= removed.size();
// Not needed if only whole batches were removed, but keeping existing behavior.
rebatchAllAlarms();
}
return removed;
}
@Override
public void setAlarmClockRemovalListener(Runnable listener) {
mOnAlarmClockRemoved = listener;
}
@Override
public Alarm getNextWakeFromIdleAlarm() {
for (final Batch batch : mAlarmBatches) {
if ((batch.mFlags & AlarmManager.FLAG_WAKE_FROM_IDLE) == 0) {
continue;
}
for (int i = 0; i < batch.size(); i++) {
final Alarm a = batch.get(i);
if ((a.flags & AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
return a;
}
}
}
return null;
}
private void rebatchAllAlarms() {
final long start = mStatLogger.getTime();
final ArrayList<Batch> oldBatches = (ArrayList<Batch>) mAlarmBatches.clone();
mAlarmBatches.clear();
for (final Batch batch : oldBatches) {
for (int i = 0; i < batch.size(); i++) {
insertAndBatchAlarm(batch.get(i));
}
}
mStatLogger.logDurationStat(Stats.REBATCH_ALL_ALARMS, start);
}
@Override
public int size() {
return mSize;
}
@Override
public long getNextWakeupDeliveryTime() {
for (Batch b : mAlarmBatches) {
if (b.hasWakeups()) {
return b.mStart;
}
}
return 0;
}
@Override
public long getNextDeliveryTime() {
if (mAlarmBatches.size() > 0) {
return mAlarmBatches.get(0).mStart;
}
return 0;
}
@Override
public ArrayList<Alarm> removePendingAlarms(long nowElapsed) {
final ArrayList<Alarm> removedAlarms = new ArrayList<>();
while (mAlarmBatches.size() > 0) {
final Batch batch = mAlarmBatches.get(0);
if (batch.mStart > nowElapsed) {
break;
}
mAlarmBatches.remove(0);
for (int i = 0; i < batch.size(); i++) {
removedAlarms.add(batch.get(i));
}
}
mSize -= removedAlarms.size();
return removedAlarms;
}
@Override
public boolean updateAlarmDeliveries(AlarmDeliveryCalculator deliveryCalculator) {
boolean changed = false;
for (final Batch b : mAlarmBatches) {
for (int i = 0; i < b.size(); i++) {
changed |= deliveryCalculator.updateAlarmDelivery(b.get(i));
}
}
if (changed) {
rebatchAllAlarms();
}
return changed;
}
@Override
public ArrayList<Alarm> asList() {
final ArrayList<Alarm> allAlarms = new ArrayList<>();
for (final Batch batch : mAlarmBatches) {
for (int i = 0; i < batch.size(); i++) {
allAlarms.add(batch.get(i));
}
}
return allAlarms;
}
@Override
public void dump(IndentingPrintWriter ipw, long nowElapsed, SimpleDateFormat sdf) {
ipw.print("Pending alarm batches: ");
ipw.println(mAlarmBatches.size());
for (Batch b : mAlarmBatches) {
ipw.print(b);
ipw.println(':');
ipw.increaseIndent();
dumpAlarmList(ipw, b.mAlarms, nowElapsed, sdf);
ipw.decreaseIndent();
}
mStatLogger.dump(ipw);
}
@Override
public void dumpProto(ProtoOutputStream pos, long nowElapsed) {
for (Batch b : mAlarmBatches) {
b.dumpDebug(pos, AlarmManagerServiceDumpProto.PENDING_ALARM_BATCHES, nowElapsed);
}
}
@Override
public String getName() {
return TAG;
}
@Override
public int getCount(Predicate<Alarm> condition) {
long start = mStatLogger.getTime();
int count = 0;
for (Batch b : mAlarmBatches) {
for (int i = 0; i < b.size(); i++) {
if (condition.test(b.get(i))) {
count++;
}
}
}
mStatLogger.logDurationStat(Stats.GET_COUNT, start);
return count;
}
private void insertAndBatchAlarm(Alarm alarm) {
final int whichBatch = ((alarm.flags & AlarmManager.FLAG_STANDALONE) != 0) ? -1
: attemptCoalesce(alarm.getWhenElapsed(), alarm.getMaxWhenElapsed());
if (whichBatch < 0) {
addBatch(mAlarmBatches, new Batch(alarm));
} else {
final Batch batch = mAlarmBatches.get(whichBatch);
if (batch.add(alarm)) {
// The start time of this batch advanced, so batch ordering may
// have just been broken. Move it to where it now belongs.
mAlarmBatches.remove(whichBatch);
addBatch(mAlarmBatches, batch);
}
}
}
static void addBatch(ArrayList<Batch> list, Batch newBatch) {
int index = Collections.binarySearch(list, newBatch, sBatchOrder);
if (index < 0) {
index = 0 - index - 1;
}
list.add(index, newBatch);
}
// Return the index of the matching batch, or -1 if none found.
private int attemptCoalesce(long whenElapsed, long maxWhen) {
final int n = mAlarmBatches.size();
for (int i = 0; i < n; i++) {
Batch b = mAlarmBatches.get(i);
if ((b.mFlags & AlarmManager.FLAG_STANDALONE) == 0 && b.canHold(whenElapsed, maxWhen)) {
return i;
}
}
return -1;
}
final class Batch {
long mStart; // These endpoints are always in ELAPSED
long mEnd;
int mFlags; // Flags for alarms, such as FLAG_STANDALONE.
final ArrayList<Alarm> mAlarms = new ArrayList<>();
Batch(Alarm seed) {
mStart = seed.getWhenElapsed();
mEnd = clampPositive(seed.getMaxWhenElapsed());
mFlags = seed.flags;
mAlarms.add(seed);
}
int size() {
return mAlarms.size();
}
Alarm get(int index) {
return mAlarms.get(index);
}
boolean canHold(long whenElapsed, long maxWhen) {
return (mEnd >= whenElapsed) && (mStart <= maxWhen);
}
boolean add(Alarm alarm) {
boolean newStart = false;
// narrows the batch if necessary; presumes that canHold(alarm) is true
int index = Collections.binarySearch(mAlarms, alarm, sIncreasingTimeOrder);
if (index < 0) {
index = 0 - index - 1;
}
mAlarms.add(index, alarm);
if (DEBUG_BATCH) {
Slog.v(TAG, "Adding " + alarm + " to " + this);
}
if (alarm.getWhenElapsed() > mStart) {
mStart = alarm.getWhenElapsed();
newStart = true;
}
if (alarm.getMaxWhenElapsed() < mEnd) {
mEnd = alarm.getMaxWhenElapsed();
}
mFlags |= alarm.flags;
if (DEBUG_BATCH) {
Slog.v(TAG, " => now " + this);
}
return newStart;
}
ArrayList<Alarm> remove(Predicate<Alarm> predicate) {
final ArrayList<Alarm> removed = new ArrayList<>();
long newStart = 0; // recalculate endpoints as we go
long newEnd = Long.MAX_VALUE;
int newFlags = 0;
for (int i = 0; i < mAlarms.size(); ) {
Alarm alarm = mAlarms.get(i);
if (predicate.test(alarm)) {
removed.add(mAlarms.remove(i));
if (alarm.alarmClock != null && mOnAlarmClockRemoved != null) {
mOnAlarmClockRemoved.run();
}
if (isTimeTickAlarm(alarm)) {
// 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");
}
} else {
if (alarm.getWhenElapsed() > newStart) {
newStart = alarm.getWhenElapsed();
}
if (alarm.getMaxWhenElapsed() < newEnd) {
newEnd = alarm.getMaxWhenElapsed();
}
newFlags |= alarm.flags;
i++;
}
}
if (!removed.isEmpty()) {
// commit the new batch bounds
mStart = newStart;
mEnd = newEnd;
mFlags = newFlags;
}
return removed;
}
boolean hasWakeups() {
final int n = mAlarms.size();
for (int i = 0; i < n; i++) {
Alarm a = mAlarms.get(i);
if (a.wakeup) {
return true;
}
}
return false;
}
@Override
public String toString() {
StringBuilder b = new StringBuilder(40);
b.append("Batch{");
b.append(Integer.toHexString(this.hashCode()));
b.append(" num=");
b.append(size());
b.append(" start=");
b.append(mStart);
b.append(" end=");
b.append(mEnd);
if (mFlags != 0) {
b.append(" flgs=0x");
b.append(Integer.toHexString(mFlags));
}
b.append('}');
return b.toString();
}
public void dumpDebug(ProtoOutputStream proto, long fieldId, long nowElapsed) {
final long token = proto.start(fieldId);
proto.write(BatchProto.START_REALTIME, mStart);
proto.write(BatchProto.END_REALTIME, mEnd);
proto.write(BatchProto.FLAGS, mFlags);
for (Alarm a : mAlarms) {
a.dumpDebug(proto, BatchProto.ALARMS, nowElapsed);
}
proto.end(token);
}
}
}