blob: d08e5dc94206fe138085cd37c6e8392b54d3702d [file] [log] [blame]
/*
* Copyright (C) 2021 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.location.eventlog;
import static java.lang.Integer.bitCount;
import android.annotation.Nullable;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.Preconditions;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.NoSuchElementException;
import java.util.Objects;
/**
* An in-memory event log to support historical event information. The log is of a constant size,
* and new events will overwrite old events as the log fills up.
*/
public class LocalEventLog<T> {
/** Consumer of log events for iterating over the log. */
public interface LogConsumer<T> {
/** Invoked with a time and a logEvent. */
void acceptLog(long time, T logEvent);
}
// masks for the entries field. 1 bit is used to indicate whether this is a filler event or not,
// and 31 bits to store the time delta.
private static final int IS_FILLER_MASK = 0b10000000000000000000000000000000;
private static final int TIME_DELTA_MASK = 0b01111111111111111111111111111111;
private static final int IS_FILLER_OFFSET = countTrailingZeros(IS_FILLER_MASK);
private static final int TIME_DELTA_OFFSET = countTrailingZeros(TIME_DELTA_MASK);
@VisibleForTesting
static final int MAX_TIME_DELTA = (1 << bitCount(TIME_DELTA_MASK)) - 1;
private static int countTrailingZeros(int i) {
int c = 0;
while (i != 0 && (i & 1) == 0) {
c++;
i = i >>> 1;
}
return c;
}
private static int createEntry(boolean isFiller, int timeDelta) {
Preconditions.checkArgument(timeDelta >= 0 && timeDelta <= MAX_TIME_DELTA);
return (((isFiller ? 1 : 0) << IS_FILLER_OFFSET) & IS_FILLER_MASK)
| ((timeDelta << TIME_DELTA_OFFSET) & TIME_DELTA_MASK);
}
static int getTimeDelta(int entry) {
return (entry & TIME_DELTA_MASK) >>> TIME_DELTA_OFFSET;
}
static boolean isFiller(int entry) {
return (entry & IS_FILLER_MASK) != 0;
}
// circular buffer of log entries and events. each entry corresponds to the log event at the
// same index. the log entry holds the filler status and time delta according to the bit masks
// above, and the log event is the log event.
@GuardedBy("this")
final int[] mEntries;
@GuardedBy("this")
final @Nullable T[] mLogEvents;
@GuardedBy("this")
int mLogSize;
@GuardedBy("this")
int mLogEndIndex;
// invalid if log is empty
@GuardedBy("this")
long mStartTime;
@GuardedBy("this")
long mLastLogTime;
@GuardedBy("this")
long mModificationCount;
@SuppressWarnings("unchecked")
public LocalEventLog(int size, Class<T> clazz) {
Preconditions.checkArgument(size > 0);
mEntries = new int[size];
mLogEvents = (T[]) Array.newInstance(clazz, size);
mLogSize = 0;
mLogEndIndex = 0;
mStartTime = -1;
mLastLogTime = -1;
}
/** Call to add a new log event at the given time. */
protected synchronized void addLog(long time, T logEvent) {
Preconditions.checkArgument(logEvent != null);
// calculate delta
long delta = 0;
if (!isEmpty()) {
delta = time - mLastLogTime;
// if the delta is negative, or if the delta is great enough using filler elements would
// result in an empty log anyways, just clear the log and continue, otherwise insert
// filler elements until we have a reasonable delta
if (delta < 0 || (delta / MAX_TIME_DELTA) >= mEntries.length - 1) {
clear();
delta = 0;
} else {
while (delta >= MAX_TIME_DELTA) {
addLogEventInternal(true, MAX_TIME_DELTA, null);
delta -= MAX_TIME_DELTA;
}
}
}
// for first log entry, set initial times
if (isEmpty()) {
mStartTime = time;
mLastLogTime = mStartTime;
mModificationCount++;
}
addLogEventInternal(false, (int) delta, logEvent);
}
@GuardedBy("this")
private void addLogEventInternal(boolean isFiller, int timeDelta, @Nullable T logEvent) {
Preconditions.checkArgument(isFiller || logEvent != null);
Preconditions.checkState(mStartTime != -1 && mLastLogTime != -1);
if (mLogSize == mEntries.length) {
// if log is full, size will remain the same, but update the start time
mStartTime += getTimeDelta(mEntries[startIndex()]);
mModificationCount++;
} else {
// otherwise add an item
mLogSize++;
}
// set log and increment end index
mEntries[mLogEndIndex] = createEntry(isFiller, timeDelta);
mLogEvents[mLogEndIndex] = logEvent;
mLogEndIndex = incrementIndex(mLogEndIndex);
mLastLogTime = mLastLogTime + timeDelta;
}
/** Clears the log of all entries. */
public synchronized void clear() {
// clear entries to aid gc
Arrays.fill(mLogEvents, null);
mLogEndIndex = 0;
mLogSize = 0;
mModificationCount++;
mStartTime = -1;
mLastLogTime = -1;
}
// checks if the log is empty (if empty, times are invalid)
@GuardedBy("this")
private boolean isEmpty() {
return mLogSize == 0;
}
/**
* Iterates over the event log, passing each log event to the given consumer. Locks the log
* while executing so that {@link ConcurrentModificationException}s cannot occur.
*/
public synchronized void iterate(LogConsumer<? super T> consumer) {
LogIterator it = new LogIterator();
while (it.hasNext()) {
it.next();
consumer.acceptLog(it.getTime(), it.getLog());
}
}
/**
* Iterates over all the given event logs in time order, passing each log event to the given
* consumer. It is the caller's responsibility to ensure that {@link
* ConcurrentModificationException}s cannot occur, whether through locking or other means.
*/
@SafeVarargs
public static <T> void iterate(LogConsumer<? super T> consumer, LocalEventLog<T>... logs) {
ArrayList<LocalEventLog<T>.LogIterator> its = new ArrayList<>(logs.length);
for (LocalEventLog<T> log : logs) {
LocalEventLog<T>.LogIterator it = log.new LogIterator();
if (it.hasNext()) {
its.add(it);
it.next();
}
}
while (true) {
LocalEventLog<T>.LogIterator next = null;
for (LocalEventLog<T>.LogIterator it : its) {
if (it != null && (next == null || it.getTime() < next.getTime())) {
next = it;
}
}
if (next == null) {
return;
}
consumer.acceptLog(next.getTime(), next.getLog());
if (next.hasNext()) {
next.next();
} else {
its.remove(next);
}
}
}
// returns the index of the first element
@GuardedBy("this")
int startIndex() {
return wrapIndex(mLogEndIndex - mLogSize);
}
// returns the index after this one
@GuardedBy("this")
int incrementIndex(int index) {
if (index == -1) {
return startIndex();
} else if (index >= 0) {
return wrapIndex(index + 1);
} else {
throw new IllegalArgumentException();
}
}
// rolls over the given index if necessary
@GuardedBy("this")
int wrapIndex(int index) {
// java modulo will keep negative sign, we need to rollover
return (index % mEntries.length + mEntries.length) % mEntries.length;
}
/** Iterator over log times and events. */
protected final class LogIterator {
private final long mModificationCount;
private long mLogTime;
private int mIndex;
private int mCount;
private long mCurrentTime;
private T mCurrentLogEvent;
public LogIterator() {
synchronized (LocalEventLog.this) {
mModificationCount = LocalEventLog.this.mModificationCount;
mLogTime = mStartTime;
mIndex = -1;
mCount = -1;
increment();
}
}
public boolean hasNext() {
synchronized (LocalEventLog.this) {
checkModifications();
return mCount < mLogSize;
}
}
public void next() {
synchronized (LocalEventLog.this) {
if (!hasNext()) {
throw new NoSuchElementException();
}
mCurrentTime = mLogTime + getTimeDelta(mEntries[mIndex]);
mCurrentLogEvent = Objects.requireNonNull(mLogEvents[mIndex]);
increment();
}
}
public long getTime() {
return mCurrentTime;
}
public T getLog() {
return mCurrentLogEvent;
}
@GuardedBy("LocalEventLog.this")
private void increment(LogIterator this) {
long nextDeltaMs = mIndex == -1 ? 0 : getTimeDelta(mEntries[mIndex]);
do {
mLogTime += nextDeltaMs;
mIndex = incrementIndex(mIndex);
if (++mCount < mLogSize) {
nextDeltaMs = getTimeDelta(mEntries[mIndex]);
}
} while (mCount < mLogSize && isFiller(mEntries[mIndex]));
}
@GuardedBy("LocalEventLog.this")
private void checkModifications() {
if (mModificationCount != LocalEventLog.this.mModificationCount) {
throw new ConcurrentModificationException();
}
}
}
}