From 60aa35b756707a16d310c222a36edbcef9d56ed4 Mon Sep 17 00:00:00 2001 From: Suprabh Shukla Date: Tue, 24 Apr 2018 18:52:46 -0700 Subject: [PATCH] Using a list to store usage events Moving UsageEvent.Event objects to an array list sorted on the event timestamps as there can be multiple events with the same timestamps. Test: atest android.app.usage.EventListTest Existing tests: atest android.app.usage.cts.UsageStatsTest Bug: 74406113 Change-Id: Idc7f2a8db6e5a9499b3b0b74efbf014b17fa495f --- core/java/android/app/usage/EventList.java | 106 ++++++++++++++ .../android/app/usage/TimeSparseArray.java | 26 ++-- .../src/android/app/usage/EventListTest.java | 130 ++++++++++++++++++ .../app/usage/TimeSparseArrayTest.java | 47 ------- .../android/server/usage/IntervalStats.java | 3 +- .../android/server/usage/UsageStatsXmlV1.java | 9 +- .../server/usage/UserUsageStatsService.java | 41 ++---- 7 files changed, 265 insertions(+), 97 deletions(-) create mode 100644 core/java/android/app/usage/EventList.java create mode 100644 core/tests/coretests/src/android/app/usage/EventListTest.java delete mode 100644 core/tests/coretests/src/android/app/usage/TimeSparseArrayTest.java diff --git a/core/java/android/app/usage/EventList.java b/core/java/android/app/usage/EventList.java new file mode 100644 index 0000000000000..aaae57e526a06 --- /dev/null +++ b/core/java/android/app/usage/EventList.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2018 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 android.app.usage; + +import java.util.ArrayList; + +/** + * A container to keep {@link UsageEvents.Event usage events} in non-descending order of their + * {@link UsageEvents.Event#mTimeStamp timestamps}. + * + * @hide + */ +public class EventList { + + private final ArrayList mEvents; + + /** + * Create a new event list with default capacity + */ + public EventList() { + mEvents = new ArrayList<>(); + } + + /** + * Returns the size of the list + * @return the number of events in the list + */ + public int size() { + return mEvents.size(); + } + + /** + * Removes all events from the list + */ + public void clear() { + mEvents.clear(); + } + + /** + * Returns the {@link UsageEvents.Event event} at the specified position in this list. + * @param index the index of the event to return, such that {@code 0 <= index < size()} + * @return The {@link UsageEvents.Event event} at position {@code index} + */ + public UsageEvents.Event get(int index) { + return mEvents.get(index); + } + + /** + * Inserts the given {@link UsageEvents.Event event} into the list while keeping the list sorted + * based on the event {@link UsageEvents.Event#mTimeStamp timestamps}. + * + * @param event The event to insert + */ + public void insert(UsageEvents.Event event) { + final int size = mEvents.size(); + // fast case: just append if this is the latest event + if (size == 0 || event.mTimeStamp >= mEvents.get(size - 1).mTimeStamp) { + mEvents.add(event); + return; + } + // To minimize number of elements being shifted, insert at the first occurrence of the next + // greatest timestamp in the list. + final int insertIndex = firstIndexOnOrAfter(event.mTimeStamp + 1); + mEvents.add(insertIndex, event); + } + + /** + * Finds the index of the first event whose timestamp is greater than or equal to the given + * timestamp. + * + * @param timeStamp The timestamp for which to search the list. + * @return The smallest {@code index} for which {@code (get(index).mTimeStamp >= timeStamp)} is + * {@code true}, or {@link #size() size} if no such {@code index} exists. + */ + public int firstIndexOnOrAfter(long timeStamp) { + final int size = mEvents.size(); + int result = size; + int lo = 0; + int hi = size - 1; + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + final long midTimeStamp = mEvents.get(mid).mTimeStamp; + if (midTimeStamp >= timeStamp) { + hi = mid - 1; + result = mid; + } else { + lo = mid + 1; + } + } + return result; + } +} diff --git a/core/java/android/app/usage/TimeSparseArray.java b/core/java/android/app/usage/TimeSparseArray.java index 4ec0e9e4a1bbf..2bd6b24174721 100644 --- a/core/java/android/app/usage/TimeSparseArray.java +++ b/core/java/android/app/usage/TimeSparseArray.java @@ -27,14 +27,12 @@ import android.util.Slog; public class TimeSparseArray extends LongSparseArray { private static final String TAG = TimeSparseArray.class.getSimpleName(); + private boolean mWtfReported; + public TimeSparseArray() { super(); } - public TimeSparseArray(int initialCapacity) { - super(initialCapacity); - } - /** * Finds the index of the first element whose timestamp is greater or equal to * the given time. @@ -75,22 +73,16 @@ public class TimeSparseArray extends LongSparseArray { /** * {@inheritDoc} * - * Overridden to ensure no collisions. The key (time in milliseconds) is incremented till an - * empty place is found. + *

As this container is being used only to keep {@link android.util.AtomicFile files}, + * there should not be any collisions. Reporting a {@link Slog#wtf(String, String)} in case that + * happens, as that will lead to one whole file being dropped. */ @Override public void put(long key, E value) { - final long origKey = key; - int keyIndex = indexOfKey(key); - if (keyIndex >= 0) { - final long sz = size(); - while (keyIndex < sz && keyAt(keyIndex) == key) { - key++; - keyIndex++; - } - if (key >= origKey + 100) { - Slog.w(TAG, "Value " + value + " supposed to be inserted at " + origKey - + " displaced to " + key); + if (indexOfKey(key) >= 0) { + if (!mWtfReported) { + Slog.wtf(TAG, "Overwriting value " + get(key) + " by " + value); + mWtfReported = true; } } super.put(key, value); diff --git a/core/tests/coretests/src/android/app/usage/EventListTest.java b/core/tests/coretests/src/android/app/usage/EventListTest.java new file mode 100644 index 0000000000000..9dc0d4309a8f4 --- /dev/null +++ b/core/tests/coretests/src/android/app/usage/EventListTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2018 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 android.app.usage; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.support.test.filters.SmallTest; +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Random; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class EventListTest { + private static final String TAG = EventListTest.class.getSimpleName(); + + private UsageEvents.Event getUsageEvent(long timeStamp) { + final UsageEvents.Event event = new UsageEvents.Event(); + event.mTimeStamp = timeStamp; + return event; + } + + private static String getListTimeStamps(EventList list) { + final StringBuilder builder = new StringBuilder("["); + for (int i = 0; i < list.size() - 1; i++) { + builder.append(list.get(i).mTimeStamp); + builder.append(", "); + } + builder.append(list.get(list.size() - 1).mTimeStamp); + builder.append("]"); + return builder.toString(); + } + + private static void assertSorted(EventList eventList) { + for (int i = 1; i < eventList.size(); i++) { + final long lastTimeStamp = eventList.get(i - 1).mTimeStamp; + if (eventList.get(i).mTimeStamp < lastTimeStamp) { + Log.e(TAG, "Unsorted timestamps in list: " + getListTimeStamps(eventList)); + fail("Timestamp " + eventList.get(i).mTimeStamp + " at " + i + + " follows larger timestamp " + lastTimeStamp); + } + } + } + + @Test + public void testInsertsSortedRandom() { + final Random random = new Random(128); + final EventList listUnderTest = new EventList(); + for (int i = 0; i < 100; i++) { + listUnderTest.insert(getUsageEvent(random.nextLong())); + } + assertSorted(listUnderTest); + } + + @Test + public void testInsertsSortedWithDuplicates() { + final Random random = new Random(256); + final EventList listUnderTest = new EventList(); + for (int i = 0; i < 10; i++) { + final long randomTimeStamp = random.nextLong(); + for (int j = 0; j < 10; j++) { + listUnderTest.insert(getUsageEvent(randomTimeStamp)); + } + } + assertSorted(listUnderTest); + } + + @Test + public void testFirstIndexOnOrAfter() { + final EventList listUnderTest = new EventList(); + listUnderTest.insert(getUsageEvent(2)); + listUnderTest.insert(getUsageEvent(5)); + listUnderTest.insert(getUsageEvent(5)); + listUnderTest.insert(getUsageEvent(5)); + listUnderTest.insert(getUsageEvent(8)); + assertTrue(listUnderTest.firstIndexOnOrAfter(1) == 0); + assertTrue(listUnderTest.firstIndexOnOrAfter(2) == 0); + assertTrue(listUnderTest.firstIndexOnOrAfter(3) == 1); + assertTrue(listUnderTest.firstIndexOnOrAfter(4) == 1); + assertTrue(listUnderTest.firstIndexOnOrAfter(5) == 1); + assertTrue(listUnderTest.firstIndexOnOrAfter(6) == 4); + assertTrue(listUnderTest.firstIndexOnOrAfter(7) == 4); + assertTrue(listUnderTest.firstIndexOnOrAfter(8) == 4); + assertTrue(listUnderTest.firstIndexOnOrAfter(9) == listUnderTest.size()); + assertTrue(listUnderTest.firstIndexOnOrAfter(100) == listUnderTest.size()); + + listUnderTest.clear(); + assertTrue(listUnderTest.firstIndexOnOrAfter(5) == 0); + assertTrue(listUnderTest.firstIndexOnOrAfter(100) == 0); + } + + @Test + public void testClear() { + final EventList listUnderTest = new EventList(); + for (int i = 1; i <= 100; i++) { + listUnderTest.insert(getUsageEvent(i)); + } + listUnderTest.clear(); + assertEquals(0, listUnderTest.size()); + } + + @Test + public void testSize() { + final EventList listUnderTest = new EventList(); + for (int i = 1; i <= 100; i++) { + listUnderTest.insert(getUsageEvent(i)); + } + assertEquals(100, listUnderTest.size()); + } +} diff --git a/core/tests/coretests/src/android/app/usage/TimeSparseArrayTest.java b/core/tests/coretests/src/android/app/usage/TimeSparseArrayTest.java deleted file mode 100644 index db4674070f1d2..0000000000000 --- a/core/tests/coretests/src/android/app/usage/TimeSparseArrayTest.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2018 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 android.app.usage; - -import static org.junit.Assert.assertTrue; - -import android.os.SystemClock; -import android.support.test.filters.SmallTest; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -@RunWith(AndroidJUnit4.class) -@SmallTest -public class TimeSparseArrayTest { - @Test - public void testDuplicateKeysNotDropped() { - final TimeSparseArray testTimeSparseArray = new TimeSparseArray<>(); - final long key = SystemClock.elapsedRealtime(); - for (int i = 0; i < 5; i++) { - testTimeSparseArray.put(key, i); - } - for (int i = 0; i < 5; i++) { - final int valueIndex = testTimeSparseArray.indexOfValue(i); - assertTrue("Value " + i + " not found; intended key: " + key , valueIndex >= 0); - final long keyForValue = testTimeSparseArray.keyAt(valueIndex); - assertTrue("Value " + i + " stored too far (at " + keyForValue + ") from intended key " - + key, Math.abs(keyForValue - key) < 100); - } - } -} diff --git a/services/usage/java/com/android/server/usage/IntervalStats.java b/services/usage/java/com/android/server/usage/IntervalStats.java index c914689183b13..0dce7382290c4 100644 --- a/services/usage/java/com/android/server/usage/IntervalStats.java +++ b/services/usage/java/com/android/server/usage/IntervalStats.java @@ -16,6 +16,7 @@ package com.android.server.usage; import android.app.usage.ConfigurationStats; +import android.app.usage.EventList; import android.app.usage.EventStats; import android.app.usage.TimeSparseArray; import android.app.usage.UsageEvents; @@ -37,7 +38,7 @@ class IntervalStats { public final ArrayMap packageStats = new ArrayMap<>(); public final ArrayMap configurations = new ArrayMap<>(); public Configuration activeConfiguration; - public TimeSparseArray events; + public EventList events; // A string cache. This is important as when we're parsing XML files, we don't want to // keep hundreds of strings that have the same contents. We will read the string diff --git a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java index fe3a8845b3a3f..aa832ad6b0aac 100644 --- a/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java +++ b/services/usage/java/com/android/server/usage/UsageStatsXmlV1.java @@ -22,12 +22,11 @@ import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlSerializer; import android.app.usage.ConfigurationStats; -import android.app.usage.TimeSparseArray; +import android.app.usage.EventList; import android.app.usage.UsageEvents; import android.app.usage.UsageStats; import android.content.res.Configuration; import android.util.ArrayMap; -import android.util.Pair; import java.io.IOException; import java.net.ProtocolException; @@ -193,9 +192,9 @@ final class UsageStatsXmlV1 { } if (statsOut.events == null) { - statsOut.events = new TimeSparseArray<>(); + statsOut.events = new EventList(); } - statsOut.events.put(event.mTimeStamp, event); + statsOut.events.insert(event); } private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats, @@ -411,7 +410,7 @@ final class UsageStatsXmlV1 { xml.startTag(null, EVENT_LOG_TAG); final int eventCount = stats.events != null ? stats.events.size() : 0; for (int i = 0; i < eventCount; i++) { - writeEvent(xml, stats, stats.events.valueAt(i)); + writeEvent(xml, stats, stats.events.get(i)); } xml.endTag(null, EVENT_LOG_TAG); } diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java index d9fc066f2402e..9cb98f326e753 100644 --- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java +++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java @@ -17,15 +17,14 @@ package com.android.server.usage; import android.app.usage.ConfigurationStats; +import android.app.usage.EventList; import android.app.usage.EventStats; -import android.app.usage.TimeSparseArray; import android.app.usage.UsageEvents; import android.app.usage.UsageStats; import android.app.usage.UsageStatsManager; import android.content.res.Configuration; import android.os.SystemClock; import android.content.Context; -import android.text.format.DateFormat; import android.text.format.DateUtils; import android.util.ArrayMap; import android.util.ArraySet; @@ -174,10 +173,10 @@ class UserUsageStatsService { // Add the event to the daily list. if (currentDailyStats.events == null) { - currentDailyStats.events = new TimeSparseArray<>(); + currentDailyStats.events = new EventList(); } if (event.mEventType != UsageEvents.Event.SYSTEM_INTERACTION) { - currentDailyStats.events.put(event.mTimeStamp, event); + currentDailyStats.events.insert(event); } boolean incrementAppLaunch = false; @@ -367,18 +366,14 @@ class UserUsageStatsService { return; } - final int startIndex = stats.events.closestIndexOnOrAfter(beginTime); - if (startIndex < 0) { - return; - } - + final int startIndex = stats.events.firstIndexOnOrAfter(beginTime); final int size = stats.events.size(); for (int i = startIndex; i < size; i++) { - if (stats.events.keyAt(i) >= endTime) { + if (stats.events.get(i).mTimeStamp >= endTime) { return; } - UsageEvents.Event event = stats.events.valueAt(i); + UsageEvents.Event event = stats.events.get(i); if (obfuscateInstantApps) { event = event.getObfuscatedIfInstantApp(); } @@ -410,18 +405,14 @@ class UserUsageStatsService { return; } - final int startIndex = stats.events.closestIndexOnOrAfter(beginTime); - if (startIndex < 0) { - return; - } - + final int startIndex = stats.events.firstIndexOnOrAfter(beginTime); final int size = stats.events.size(); for (int i = startIndex; i < size; i++) { - if (stats.events.keyAt(i) >= endTime) { + if (stats.events.get(i).mTimeStamp >= endTime) { return; } - final UsageEvents.Event event = stats.events.valueAt(i); + final UsageEvents.Event event = stats.events.get(i); if (!packageName.equals(event.mPackage)) { continue; } @@ -633,18 +624,14 @@ class UserUsageStatsService { return; } - final int startIndex = stats.events.closestIndexOnOrAfter(beginTime); - if (startIndex < 0) { - return; - } - + final int startIndex = stats.events.firstIndexOnOrAfter(beginTime); final int size = stats.events.size(); for (int i = startIndex; i < size; i++) { - if (stats.events.keyAt(i) >= endTime) { + if (stats.events.get(i).mTimeStamp >= endTime) { return; } - UsageEvents.Event event = stats.events.valueAt(i); + UsageEvents.Event event = stats.events.get(i); if (pkg != null && !pkg.equals(event.mPackage)) { continue; } @@ -779,10 +766,10 @@ class UserUsageStatsService { if (!skipEvents) { pw.println("events"); pw.increaseIndent(); - final TimeSparseArray events = stats.events; + final EventList events = stats.events; final int eventCount = events != null ? events.size() : 0; for (int i = 0; i < eventCount; i++) { - final UsageEvents.Event event = events.valueAt(i); + final UsageEvents.Event event = events.get(i); if (pkg != null && !pkg.equals(event.mPackage)) { continue; }