1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.location.eventlog; 18 19 import static java.lang.Integer.bitCount; 20 21 import android.annotation.Nullable; 22 23 import com.android.internal.annotations.GuardedBy; 24 import com.android.internal.annotations.VisibleForTesting; 25 import com.android.internal.util.Preconditions; 26 27 import java.lang.reflect.Array; 28 import java.util.ArrayList; 29 import java.util.Arrays; 30 import java.util.ConcurrentModificationException; 31 import java.util.NoSuchElementException; 32 import java.util.Objects; 33 34 /** 35 * An in-memory event log to support historical event information. The log is of a constant size, 36 * and new events will overwrite old events as the log fills up. 37 */ 38 public class LocalEventLog<T> { 39 40 /** Consumer of log events for iterating over the log. */ 41 public interface LogConsumer<T> { 42 /** Invoked with a time and a logEvent. */ acceptLog(long time, T logEvent)43 void acceptLog(long time, T logEvent); 44 } 45 46 // masks for the entries field. 1 bit is used to indicate whether this is a filler event or not, 47 // and 31 bits to store the time delta. 48 private static final int IS_FILLER_MASK = 0b10000000000000000000000000000000; 49 private static final int TIME_DELTA_MASK = 0b01111111111111111111111111111111; 50 51 private static final int IS_FILLER_OFFSET = countTrailingZeros(IS_FILLER_MASK); 52 private static final int TIME_DELTA_OFFSET = countTrailingZeros(TIME_DELTA_MASK); 53 54 @VisibleForTesting 55 static final int MAX_TIME_DELTA = (1 << bitCount(TIME_DELTA_MASK)) - 1; 56 countTrailingZeros(int i)57 private static int countTrailingZeros(int i) { 58 int c = 0; 59 while (i != 0 && (i & 1) == 0) { 60 c++; 61 i = i >>> 1; 62 } 63 return c; 64 } 65 createEntry(boolean isFiller, int timeDelta)66 private static int createEntry(boolean isFiller, int timeDelta) { 67 Preconditions.checkArgument(timeDelta >= 0 && timeDelta <= MAX_TIME_DELTA); 68 return (((isFiller ? 1 : 0) << IS_FILLER_OFFSET) & IS_FILLER_MASK) 69 | ((timeDelta << TIME_DELTA_OFFSET) & TIME_DELTA_MASK); 70 } 71 getTimeDelta(int entry)72 static int getTimeDelta(int entry) { 73 return (entry & TIME_DELTA_MASK) >>> TIME_DELTA_OFFSET; 74 } 75 isFiller(int entry)76 static boolean isFiller(int entry) { 77 return (entry & IS_FILLER_MASK) != 0; 78 } 79 80 // circular buffer of log entries and events. each entry corresponds to the log event at the 81 // same index. the log entry holds the filler status and time delta according to the bit masks 82 // above, and the log event is the log event. 83 84 @GuardedBy("this") 85 final int[] mEntries; 86 87 @GuardedBy("this") 88 final @Nullable T[] mLogEvents; 89 90 @GuardedBy("this") 91 int mLogSize; 92 93 @GuardedBy("this") 94 int mLogEndIndex; 95 96 // invalid if log is empty 97 98 @GuardedBy("this") 99 long mStartTime; 100 101 @GuardedBy("this") 102 long mLastLogTime; 103 104 @GuardedBy("this") 105 long mModificationCount; 106 107 @SuppressWarnings("unchecked") LocalEventLog(int size, Class<T> clazz)108 public LocalEventLog(int size, Class<T> clazz) { 109 Preconditions.checkArgument(size > 0); 110 111 mEntries = new int[size]; 112 mLogEvents = (T[]) Array.newInstance(clazz, size); 113 mLogSize = 0; 114 mLogEndIndex = 0; 115 116 mStartTime = -1; 117 mLastLogTime = -1; 118 } 119 120 /** Call to add a new log event at the given time. */ addLog(long time, T logEvent)121 protected synchronized void addLog(long time, T logEvent) { 122 Preconditions.checkArgument(logEvent != null); 123 124 // calculate delta 125 long delta = 0; 126 if (!isEmpty()) { 127 delta = time - mLastLogTime; 128 129 // if the delta is negative, or if the delta is great enough using filler elements would 130 // result in an empty log anyways, just clear the log and continue, otherwise insert 131 // filler elements until we have a reasonable delta 132 if (delta < 0 || (delta / MAX_TIME_DELTA) >= mEntries.length - 1) { 133 clear(); 134 delta = 0; 135 } else { 136 while (delta >= MAX_TIME_DELTA) { 137 addLogEventInternal(true, MAX_TIME_DELTA, null); 138 delta -= MAX_TIME_DELTA; 139 } 140 } 141 } 142 143 // for first log entry, set initial times 144 if (isEmpty()) { 145 mStartTime = time; 146 mLastLogTime = mStartTime; 147 mModificationCount++; 148 } 149 150 addLogEventInternal(false, (int) delta, logEvent); 151 } 152 153 @GuardedBy("this") addLogEventInternal(boolean isFiller, int timeDelta, @Nullable T logEvent)154 private void addLogEventInternal(boolean isFiller, int timeDelta, @Nullable T logEvent) { 155 Preconditions.checkArgument(isFiller || logEvent != null); 156 Preconditions.checkState(mStartTime != -1 && mLastLogTime != -1); 157 158 if (mLogSize == mEntries.length) { 159 // if log is full, size will remain the same, but update the start time 160 mStartTime += getTimeDelta(mEntries[startIndex()]); 161 mModificationCount++; 162 } else { 163 // otherwise add an item 164 mLogSize++; 165 } 166 167 // set log and increment end index 168 mEntries[mLogEndIndex] = createEntry(isFiller, timeDelta); 169 mLogEvents[mLogEndIndex] = logEvent; 170 mLogEndIndex = incrementIndex(mLogEndIndex); 171 mLastLogTime = mLastLogTime + timeDelta; 172 } 173 174 /** Clears the log of all entries. */ clear()175 public synchronized void clear() { 176 // clear entries to aid gc 177 Arrays.fill(mLogEvents, null); 178 179 mLogEndIndex = 0; 180 mLogSize = 0; 181 mModificationCount++; 182 183 mStartTime = -1; 184 mLastLogTime = -1; 185 } 186 187 // checks if the log is empty (if empty, times are invalid) 188 @GuardedBy("this") isEmpty()189 private boolean isEmpty() { 190 return mLogSize == 0; 191 } 192 193 /** 194 * Iterates over the event log, passing each log event to the given consumer. Locks the log 195 * while executing so that {@link ConcurrentModificationException}s cannot occur. 196 */ iterate(LogConsumer<? super T> consumer)197 public synchronized void iterate(LogConsumer<? super T> consumer) { 198 LogIterator it = new LogIterator(); 199 while (it.hasNext()) { 200 it.next(); 201 consumer.acceptLog(it.getTime(), it.getLog()); 202 } 203 } 204 205 /** 206 * Iterates over all the given event logs in time order, passing each log event to the given 207 * consumer. It is the caller's responsibility to ensure that {@link 208 * ConcurrentModificationException}s cannot occur, whether through locking or other means. 209 */ 210 @SafeVarargs iterate(LogConsumer<? super T> consumer, LocalEventLog<T>... logs)211 public static <T> void iterate(LogConsumer<? super T> consumer, LocalEventLog<T>... logs) { 212 ArrayList<LocalEventLog<T>.LogIterator> its = new ArrayList<>(logs.length); 213 for (LocalEventLog<T> log : logs) { 214 LocalEventLog<T>.LogIterator it = log.new LogIterator(); 215 if (it.hasNext()) { 216 its.add(it); 217 it.next(); 218 } 219 } 220 221 while (true) { 222 LocalEventLog<T>.LogIterator next = null; 223 for (LocalEventLog<T>.LogIterator it : its) { 224 if (it != null && (next == null || it.getTime() < next.getTime())) { 225 next = it; 226 } 227 } 228 229 if (next == null) { 230 return; 231 } 232 233 consumer.acceptLog(next.getTime(), next.getLog()); 234 235 if (next.hasNext()) { 236 next.next(); 237 } else { 238 its.remove(next); 239 } 240 } 241 } 242 243 // returns the index of the first element 244 @GuardedBy("this") startIndex()245 int startIndex() { 246 return wrapIndex(mLogEndIndex - mLogSize); 247 } 248 249 // returns the index after this one 250 @GuardedBy("this") incrementIndex(int index)251 int incrementIndex(int index) { 252 if (index == -1) { 253 return startIndex(); 254 } else if (index >= 0) { 255 return wrapIndex(index + 1); 256 } else { 257 throw new IllegalArgumentException(); 258 } 259 } 260 261 // rolls over the given index if necessary 262 @GuardedBy("this") wrapIndex(int index)263 int wrapIndex(int index) { 264 // java modulo will keep negative sign, we need to rollover 265 return (index % mEntries.length + mEntries.length) % mEntries.length; 266 } 267 268 /** Iterator over log times and events. */ 269 protected final class LogIterator { 270 271 private final long mModificationCount; 272 273 private long mLogTime; 274 private int mIndex; 275 private int mCount; 276 277 private long mCurrentTime; 278 private T mCurrentLogEvent; 279 LogIterator()280 public LogIterator() { 281 synchronized (LocalEventLog.this) { 282 mModificationCount = LocalEventLog.this.mModificationCount; 283 284 mLogTime = mStartTime; 285 mIndex = -1; 286 mCount = -1; 287 288 increment(); 289 } 290 } 291 hasNext()292 public boolean hasNext() { 293 synchronized (LocalEventLog.this) { 294 checkModifications(); 295 return mCount < mLogSize; 296 } 297 } 298 next()299 public void next() { 300 synchronized (LocalEventLog.this) { 301 if (!hasNext()) { 302 throw new NoSuchElementException(); 303 } 304 305 mCurrentTime = mLogTime + getTimeDelta(mEntries[mIndex]); 306 mCurrentLogEvent = Objects.requireNonNull(mLogEvents[mIndex]); 307 308 increment(); 309 } 310 } 311 getTime()312 public long getTime() { 313 return mCurrentTime; 314 } 315 getLog()316 public T getLog() { 317 return mCurrentLogEvent; 318 } 319 320 @GuardedBy("LocalEventLog.this") increment(LogIterator this)321 private void increment(LogIterator this) { 322 long nextDeltaMs = mIndex == -1 ? 0 : getTimeDelta(mEntries[mIndex]); 323 do { 324 mLogTime += nextDeltaMs; 325 mIndex = incrementIndex(mIndex); 326 if (++mCount < mLogSize) { 327 nextDeltaMs = getTimeDelta(mEntries[mIndex]); 328 } 329 } while (mCount < mLogSize && isFiller(mEntries[mIndex])); 330 } 331 332 @GuardedBy("LocalEventLog.this") checkModifications()333 private void checkModifications() { 334 if (mModificationCount != LocalEventLog.this.mModificationCount) { 335 throw new ConcurrentModificationException(); 336 } 337 } 338 } 339 } 340