UsageStats: Fix time change handling

When the time changed, we would end up creating a new stats file, even if we could
reuse an old one. On systems where the time toggled a lot, this would generate
hundreds of files and slow down other IO operations on this set.

Now we properly reuse stats files after time changes. The time at which a particular
file began recording was always clamped to the start of the interval (day, month, year).
However, this makes no sense when the time changes, and clamping to the start of the interval
would always cause a new file to be created, since the current time no longer fell into that
time interval. So now the time is just offset from the last file's end time.

Bug:22716352
Change-Id: I6cec156e2e93b4402116600fa09c1018f3b870fe
diff --git a/services/usage/java/com/android/server/usage/UnixCalendar.java b/services/usage/java/com/android/server/usage/UnixCalendar.java
index ce06a91..db7b42d 100644
--- a/services/usage/java/com/android/server/usage/UnixCalendar.java
+++ b/services/usage/java/com/android/server/usage/UnixCalendar.java
@@ -15,40 +15,22 @@
  */
 package com.android.server.usage;
 
-import android.app.usage.UsageStatsManager;
-
 /**
  * A handy calendar object that knows nothing of Locale's or TimeZones. This simplifies
  * interval book-keeping. It is *NOT* meant to be used as a user-facing calendar, as it has
  * no concept of Locale or TimeZone.
  */
 public class UnixCalendar {
-    private static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
-    private static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
-    private static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
-    private static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
+    public static final long DAY_IN_MILLIS = 24 * 60 * 60 * 1000;
+    public static final long WEEK_IN_MILLIS = 7 * DAY_IN_MILLIS;
+    public static final long MONTH_IN_MILLIS = 30 * DAY_IN_MILLIS;
+    public static final long YEAR_IN_MILLIS = 365 * DAY_IN_MILLIS;
     private long mTime;
 
     public UnixCalendar(long time) {
         mTime = time;
     }
 
-    public void truncateToDay() {
-        mTime -= mTime % DAY_IN_MILLIS;
-    }
-
-    public void truncateToWeek() {
-        mTime -= mTime % WEEK_IN_MILLIS;
-    }
-
-    public void truncateToMonth() {
-        mTime -= mTime % MONTH_IN_MILLIS;
-    }
-
-    public void truncateToYear() {
-        mTime -= mTime % YEAR_IN_MILLIS;
-    }
-
     public void addDays(int val) {
         mTime += val * DAY_IN_MILLIS;
     }
@@ -72,28 +54,4 @@
     public long getTimeInMillis() {
         return mTime;
     }
-
-    public static void truncateTo(UnixCalendar calendar, int intervalType) {
-        switch (intervalType) {
-            case UsageStatsManager.INTERVAL_YEARLY:
-                calendar.truncateToYear();
-                break;
-
-            case UsageStatsManager.INTERVAL_MONTHLY:
-                calendar.truncateToMonth();
-                break;
-
-            case UsageStatsManager.INTERVAL_WEEKLY:
-                calendar.truncateToWeek();
-                break;
-
-            case UsageStatsManager.INTERVAL_DAILY:
-                calendar.truncateToDay();
-                break;
-
-            default:
-                throw new UnsupportedOperationException("Can't truncate date to interval " +
-                        intervalType);
-        }
-    }
 }
diff --git a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
index f8ae03f..0ca4bd8 100644
--- a/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
+++ b/services/usage/java/com/android/server/usage/UsageStatsDatabase.java
@@ -350,23 +350,6 @@
     }
 
     /**
-     * Get the time at which the latest stats begin for this interval type.
-     */
-    public long getLatestUsageStatsBeginTime(int intervalType) {
-        synchronized (mLock) {
-            if (intervalType < 0 || intervalType >= mIntervalDirs.length) {
-                throw new IllegalArgumentException("Bad interval type " + intervalType);
-            }
-
-            final int statsFileCount = mSortedStatFiles[intervalType].size();
-            if (statsFileCount > 0) {
-                return mSortedStatFiles[intervalType].keyAt(statsFileCount - 1);
-            }
-            return -1;
-        }
-    }
-
-    /**
      * Figures out what to extract from the given IntervalStats object.
      */
     interface StatCombiner<T> {
diff --git a/services/usage/java/com/android/server/usage/UserUsageStatsService.java b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
index b07b815..5188e5f 100644
--- a/services/usage/java/com/android/server/usage/UserUsageStatsService.java
+++ b/services/usage/java/com/android/server/usage/UserUsageStatsService.java
@@ -65,6 +65,11 @@
     private final String mLogPrefix;
     private final int mUserId;
 
+    private static final long[] INTERVAL_LENGTH = new long[] {
+            UnixCalendar.DAY_IN_MILLIS, UnixCalendar.WEEK_IN_MILLIS,
+            UnixCalendar.MONTH_IN_MILLIS, UnixCalendar.YEAR_IN_MILLIS
+    };
+
     interface StatsUpdatedListener {
         void onStatsUpdated();
     }
@@ -104,18 +109,12 @@
 
             // By calling loadActiveStats, we will
             // generate new stats for each bucket.
-            loadActiveStats(currentTimeMillis,/*force=*/ false, /*resetBeginIdleTime=*/ false);
+            loadActiveStats(currentTimeMillis, /*resetBeginIdleTime=*/ false);
         } else {
             // Set up the expiry date to be one day from the latest daily stat.
             // This may actually be today and we will rollover on the first event
             // that is reported.
-            mDailyExpiryDate.setTimeInMillis(
-                    mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
-            mDailyExpiryDate.addDays(1);
-            mDailyExpiryDate.truncateToDay();
-            Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
-                    sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) +
-                    "(" + mDailyExpiryDate.getTimeInMillis() + ")");
+            updateRolloverDeadline();
         }
 
         // Now close off any events that were open at the time this was saved.
@@ -170,7 +169,7 @@
     void onTimeChanged(long oldTime, long newTime, boolean resetBeginIdleTime) {
         persistActiveStats();
         mDatabase.onTimeChanged(newTime - oldTime);
-        loadActiveStats(newTime, /* force= */ true, resetBeginIdleTime);
+        loadActiveStats(newTime, resetBeginIdleTime);
     }
 
     void reportEvent(UsageEvents.Event event, long deviceUsageTime) {
@@ -237,7 +236,7 @@
             new StatCombiner<UsageStats>() {
                 @Override
                 public void combine(IntervalStats stats, boolean mutable,
-                        List<UsageStats> accResult) {
+                                    List<UsageStats> accResult) {
                     if (!mutable) {
                         accResult.addAll(stats.packageStats.values());
                         return;
@@ -254,7 +253,7 @@
             new StatCombiner<ConfigurationStats>() {
                 @Override
                 public void combine(IntervalStats stats, boolean mutable,
-                        List<ConfigurationStats> accResult) {
+                                    List<ConfigurationStats> accResult) {
                     if (!mutable) {
                         accResult.addAll(stats.configurations.values());
                         return;
@@ -448,7 +447,7 @@
 
         persistActiveStats();
         mDatabase.prune(currentTimeMillis);
-        loadActiveStats(currentTimeMillis, /*force=*/ false, /*resetBeginIdleTime=*/ false);
+        loadActiveStats(currentTimeMillis, /*resetBeginIdleTime=*/ false);
 
         final int continueCount = continuePreviousDay.size();
         for (int i = 0; i < continueCount; i++) {
@@ -474,45 +473,28 @@
         }
     }
 
-    /**
-     * @param force To force all in-memory stats to be reloaded.
-     */
-    private void loadActiveStats(final long currentTimeMillis, boolean force,
-            boolean resetBeginIdleTime) {
-        final UnixCalendar tempCal = mDailyExpiryDate;
+    private void loadActiveStats(final long currentTimeMillis, boolean resetBeginIdleTime) {
         for (int intervalType = 0; intervalType < mCurrentStats.length; intervalType++) {
-            tempCal.setTimeInMillis(currentTimeMillis);
-            UnixCalendar.truncateTo(tempCal, intervalType);
-
-            if (!force && mCurrentStats[intervalType] != null &&
-                    mCurrentStats[intervalType].beginTime == tempCal.getTimeInMillis()) {
-                // These are the same, no need to load them (in memory stats are always newer
-                // than persisted stats).
-                continue;
-            }
-
-            final long lastBeginTime = mDatabase.getLatestUsageStatsBeginTime(intervalType);
-            if (lastBeginTime >= tempCal.getTimeInMillis()) {
+            final IntervalStats stats = mDatabase.getLatestUsageStats(intervalType);
+            if (stats != null && currentTimeMillis - 500 >= stats.endTime &&
+                    currentTimeMillis < stats.beginTime + INTERVAL_LENGTH[intervalType]) {
                 if (DEBUG) {
                     Slog.d(TAG, mLogPrefix + "Loading existing stats @ " +
-                            sDateFormat.format(lastBeginTime) + "(" + lastBeginTime +
+                            sDateFormat.format(stats.beginTime) + "(" + stats.beginTime +
                             ") for interval " + intervalType);
                 }
-                mCurrentStats[intervalType] = mDatabase.getLatestUsageStats(intervalType);
+                mCurrentStats[intervalType] = stats;
             } else {
-                mCurrentStats[intervalType] = null;
-            }
-
-            if (mCurrentStats[intervalType] == null) {
+                // No good fit remains.
                 if (DEBUG) {
                     Slog.d(TAG, "Creating new stats @ " +
-                            sDateFormat.format(tempCal.getTimeInMillis()) + "(" +
-                            tempCal.getTimeInMillis() + ") for interval " + intervalType);
-
+                            sDateFormat.format(currentTimeMillis) + "(" +
+                            currentTimeMillis + ") for interval " + intervalType);
                 }
+
                 mCurrentStats[intervalType] = new IntervalStats();
-                mCurrentStats[intervalType].beginTime = tempCal.getTimeInMillis();
-                mCurrentStats[intervalType].endTime = currentTimeMillis;
+                mCurrentStats[intervalType].beginTime = currentTimeMillis;
+                mCurrentStats[intervalType].endTime = currentTimeMillis + 1;
             }
 
             if (resetBeginIdleTime) {
@@ -522,12 +504,16 @@
             }
         }
         mStatsChanged = false;
-        mDailyExpiryDate.setTimeInMillis(currentTimeMillis);
+        updateRolloverDeadline();
+    }
+
+    private void updateRolloverDeadline() {
+        mDailyExpiryDate.setTimeInMillis(
+                mCurrentStats[UsageStatsManager.INTERVAL_DAILY].beginTime);
         mDailyExpiryDate.addDays(1);
-        mDailyExpiryDate.truncateToDay();
         Slog.i(TAG, mLogPrefix + "Rollover scheduled @ " +
                 sDateFormat.format(mDailyExpiryDate.getTimeInMillis()) + "(" +
-                tempCal.getTimeInMillis() + ")");
+                mDailyExpiryDate.getTimeInMillis() + ")");
     }
 
     //