Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c90bc15d authored by Michael Wachenschwanz's avatar Michael Wachenschwanz
Browse files

Pool Package and Class names when writing UsageStats to disk

Write the package and class names at the top of the proto file and
replace each instance of those strings in the rest of the protobuf with
the index of said string.

Some rough number on the impact of this change plus the previous proto
change:
File size on disk reduces to ~13% of XML file size!!!
File read time reduces to ~32-45% of XML read time!
File write time clock at around ~102-107% of XML write time.

Bug: 111422946
Fixes: 111449191
Test: atest UsageStatsDatabaseTest
Change-Id: I6bcce54a2431a964bda2c03bd3be1f3d4b4156e1
parent c8c26365
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -76,6 +76,8 @@ message IntervalStatsProto {
    optional string shortcut_id = 9;
    optional int32 standby_bucket = 11;
    optional string notification_channel = 12;
    // notification_channel_index contains the index + 1 of the channel name in the string pool
    optional int32 notification_channel_index = 13;
  }

  // The following fields contain supplemental data used to build IntervalStats, such as a string
+35 −4
Original line number Diff line number Diff line
@@ -48,7 +48,7 @@ public class IntervalStats {
    // keep hundreds of strings that have the same contents. We will read the string
    // and only keep it if it's not in the cache. The GC will take care of the
    // strings that had identical copies in the cache.
    private final ArraySet<String> mStringCache = new ArraySet<>();
    public final ArraySet<String> mStringCache = new ArraySet<>();

    public static final class EventTracker {
        public long curStartTime;
@@ -135,7 +135,8 @@ public class IntervalStats {
     * Builds a UsageEvents.Event from a proto, but does not add it internally.
     * Built here to take advantage of the cached String Refs
     */
    UsageEvents.Event buildEvent(ProtoInputStream parser) throws IOException {
    UsageEvents.Event buildEvent(ProtoInputStream parser, List<String> stringPool)
            throws IOException {
        final UsageEvents.Event event = new UsageEvents.Event();
        while (true) {
            switch (parser.nextField()) {
@@ -143,10 +144,18 @@ public class IntervalStats {
                    event.mPackage = getCachedStringRef(
                            parser.readString(IntervalStatsProto.Event.PACKAGE));
                    break;
                case (int) IntervalStatsProto.Event.PACKAGE_INDEX:
                    event.mPackage = getCachedStringRef(stringPool.get(
                            parser.readInt(IntervalStatsProto.Event.PACKAGE_INDEX) - 1));
                    break;
                case (int) IntervalStatsProto.Event.CLASS:
                    event.mClass = getCachedStringRef(
                            parser.readString(IntervalStatsProto.Event.CLASS));
                    break;
                case (int) IntervalStatsProto.Event.CLASS_INDEX:
                    event.mClass = getCachedStringRef(stringPool.get(
                            parser.readInt(IntervalStatsProto.Event.CLASS_INDEX) - 1));
                    break;
                case (int) IntervalStatsProto.Event.TIME_MS:
                    event.mTimeStamp = beginTime + parser.readLong(
                            IntervalStatsProto.Event.TIME_MS);
@@ -173,6 +182,11 @@ public class IntervalStats {
                    event.mNotificationChannelId = parser.readString(
                            IntervalStatsProto.Event.NOTIFICATION_CHANNEL);
                    break;
                case (int) IntervalStatsProto.Event.NOTIFICATION_CHANNEL_INDEX:
                    event.mNotificationChannelId = getCachedStringRef(stringPool.get(
                            parser.readInt(IntervalStatsProto.Event.NOTIFICATION_CHANNEL_INDEX)
                                    - 1));
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    // Handle default values for certain events types
                    switch (event.mEventType) {
@@ -215,8 +229,6 @@ public class IntervalStats {
    /**
     * Returns whether the event type is one caused by user visible
     * interaction. Excludes those that are internally generated.
     * @param eventType
     * @return
     */
    private boolean isUserVisibleEvent(int eventType) {
        return eventType != UsageEvents.Event.SYSTEM_INTERACTION
@@ -256,6 +268,25 @@ public class IntervalStats {
        endTime = timeStamp;
    }

    /**
     * @hide
     */
    @VisibleForTesting
    public void addEvent(UsageEvents.Event event) {
        if (events == null) {
            events = new EventList();
        }
        // Cache common use strings
        event.mPackage = getCachedStringRef(event.mPackage);
        if (event.mClass != null) {
            event.mClass = getCachedStringRef(event.mClass);
        }
        if (event.mEventType == UsageEvents.Event.NOTIFICATION_INTERRUPTION) {
            event.mNotificationChannelId = getCachedStringRef(event.mNotificationChannelId);
        }
        events.insert(event);
    }

    void updateChooserCounts(String packageName, String category, String action) {
        UsageStats usageStats = getOrCreateUsageStats(packageName);
        if (usageStats.mChooserCounts == null) {
+113 −19
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.app.usage.UsageStats;
import android.content.res.Configuration;
import android.util.ArrayMap;

import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;

@@ -29,23 +30,51 @@ import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ProtocolException;
import java.util.ArrayList;
import java.util.List;

/**
 * UsageStats reader/writer for Protocol Buffer format
 */
final class UsageStatsProto {
    private static String TAG = "UsageStatsProto";

    // Static-only utility class.
    private UsageStatsProto() {}

    private static List<String> readStringPool(ProtoInputStream proto) throws IOException {

        final long token = proto.start(IntervalStatsProto.STRINGPOOL);
        List<String> stringPool;
        if (proto.isNextField(IntervalStatsProto.StringPool.SIZE)) {
            stringPool = new ArrayList(proto.readInt(IntervalStatsProto.StringPool.SIZE));
        } else {
            stringPool = new ArrayList();
        }
        while (proto.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
            switch (proto.getFieldNumber()) {
                case (int) IntervalStatsProto.StringPool.STRINGS:
                    stringPool.add(proto.readString(IntervalStatsProto.StringPool.STRINGS));
                    break;
            }
        }
        proto.end(token);
        return stringPool;
    }

    private static void loadUsageStats(ProtoInputStream proto, long fieldId,
            IntervalStats statsOut)
            IntervalStats statsOut, List<String> stringPool)
            throws IOException {

        final long token = proto.start(fieldId);
        UsageStats stats;
        if (proto.isNextField(IntervalStatsProto.UsageStats.PACKAGE)) {
            // Fast path reading the package name. Most cases this should work since it is
        if (proto.isNextField(IntervalStatsProto.UsageStats.PACKAGE_INDEX)) {
            // Fast path reading the package name index. Most cases this should work since it is
            // written first
            stats = statsOut.getOrCreateUsageStats(
                    stringPool.get(proto.readInt(IntervalStatsProto.UsageStats.PACKAGE_INDEX) - 1));
        } else if (proto.isNextField(IntervalStatsProto.UsageStats.PACKAGE)) {
            // No package index, try package name instead
            stats = statsOut.getOrCreateUsageStats(
                    proto.readString(IntervalStatsProto.UsageStats.PACKAGE));
        } else {
@@ -57,13 +86,23 @@ final class UsageStatsProto {
            switch (proto.getFieldNumber()) {
                case (int) IntervalStatsProto.UsageStats.PACKAGE:
                    // Fast track failed from some reason, add UsageStats object to statsOut now
                    UsageStats temp = statsOut.getOrCreateUsageStats(
                    UsageStats tempPackage = statsOut.getOrCreateUsageStats(
                            proto.readString(IntervalStatsProto.UsageStats.PACKAGE));
                    temp.mLastTimeUsed = stats.mLastTimeUsed;
                    temp.mTotalTimeInForeground = stats.mTotalTimeInForeground;
                    temp.mLastEvent = stats.mLastEvent;
                    temp.mAppLaunchCount = stats.mAppLaunchCount;
                    stats = temp;
                    tempPackage.mLastTimeUsed = stats.mLastTimeUsed;
                    tempPackage.mTotalTimeInForeground = stats.mTotalTimeInForeground;
                    tempPackage.mLastEvent = stats.mLastEvent;
                    tempPackage.mAppLaunchCount = stats.mAppLaunchCount;
                    stats = tempPackage;
                    break;
                case (int) IntervalStatsProto.UsageStats.PACKAGE_INDEX:
                    // Fast track failed from some reason, add UsageStats object to statsOut now
                    UsageStats tempPackageIndex = statsOut.getOrCreateUsageStats(stringPool.get(
                            proto.readInt(IntervalStatsProto.UsageStats.PACKAGE_INDEX) - 1));
                    tempPackageIndex.mLastTimeUsed = stats.mLastTimeUsed;
                    tempPackageIndex.mTotalTimeInForeground = stats.mTotalTimeInForeground;
                    tempPackageIndex.mLastEvent = stats.mLastEvent;
                    tempPackageIndex.mAppLaunchCount = stats.mAppLaunchCount;
                    stats = tempPackageIndex;
                    break;
                case (int) IntervalStatsProto.UsageStats.LAST_TIME_ACTIVE_MS:
                    stats.mLastTimeUsed = statsOut.beginTime + proto.readLong(
@@ -237,10 +276,10 @@ final class UsageStatsProto {
        }
    }

    private static void loadEvent(ProtoInputStream proto, long fieldId, IntervalStats statsOut)
            throws IOException {
    private static void loadEvent(ProtoInputStream proto, long fieldId, IntervalStats statsOut,
            List<String> stringPool) throws IOException {
        final long token = proto.start(fieldId);
        UsageEvents.Event event = statsOut.buildEvent(proto);
        UsageEvents.Event event = statsOut.buildEvent(proto, stringPool);
        proto.end(token);
        if (event.mPackage == null) {
            throw new ProtocolException("no package field present");
@@ -252,11 +291,30 @@ final class UsageStatsProto {
        statsOut.events.insert(event);
    }

    private static void writeStringPool(ProtoOutputStream proto, final IntervalStats stats)
            throws IOException {
        final long token = proto.start(IntervalStatsProto.STRINGPOOL);
        final int size = stats.mStringCache.size();
        proto.write(IntervalStatsProto.StringPool.SIZE, size);
        for (int i = 0; i < size; i++) {
            proto.write(IntervalStatsProto.StringPool.STRINGS, stats.mStringCache.valueAt(i));
        }
        proto.end(token);
    }

    private static void writeUsageStats(ProtoOutputStream proto, long fieldId,
            final IntervalStats stats, final UsageStats usageStats) throws IOException {
        final long token = proto.start(fieldId);
        // Write the package name first, so loadUsageStats can avoid creating an extra object
        final int packageIndex = stats.mStringCache.indexOf(usageStats.mPackageName);
        if (packageIndex >= 0) {
            proto.write(IntervalStatsProto.UsageStats.PACKAGE_INDEX, packageIndex + 1);
        } else {
            // Package not in Stringpool for some reason, write full string instead
            Slog.w(TAG, "UsageStats package name (" + usageStats.mPackageName
                    + ") not found in IntervalStats string cache");
            proto.write(IntervalStatsProto.UsageStats.PACKAGE, usageStats.mPackageName);
        }
        proto.write(IntervalStatsProto.UsageStats.LAST_TIME_ACTIVE_MS,
                usageStats.mLastTimeUsed - stats.beginTime);
        proto.write(IntervalStatsProto.UsageStats.TOTAL_TIME_ACTIVE_MS,
@@ -329,8 +387,26 @@ final class UsageStatsProto {
    private static void writeEvent(ProtoOutputStream proto, long fieldId, final IntervalStats stats,
            final UsageEvents.Event event) throws IOException {
        final long token = proto.start(fieldId);
        final int packageIndex = stats.mStringCache.indexOf(event.mPackage);
        if (packageIndex >= 0) {
            proto.write(IntervalStatsProto.Event.PACKAGE_INDEX, packageIndex + 1);
        } else {
            // Package not in Stringpool for some reason, write full string instead
            Slog.w(TAG, "Usage event package name (" + event.mPackage
                    + ") not found in IntervalStats string cache");
            proto.write(IntervalStatsProto.Event.PACKAGE, event.mPackage);
        }
        if (event.mClass != null) {
            final int classIndex = stats.mStringCache.indexOf(event.mClass);
            if (classIndex >= 0) {
                proto.write(IntervalStatsProto.Event.CLASS_INDEX, classIndex + 1);
            } else {
                // Class not in Stringpool for some reason, write full string instead
                Slog.w(TAG, "Usage event class name (" + event.mClass
                        + ") not found in IntervalStats string cache");
                proto.write(IntervalStatsProto.Event.CLASS, event.mClass);
            }
        }
        proto.write(IntervalStatsProto.Event.TIME_MS, event.mTimeStamp - stats.beginTime);
        proto.write(IntervalStatsProto.Event.FLAGS, event.mFlags);
        proto.write(IntervalStatsProto.Event.TYPE, event.mEventType);
@@ -352,9 +428,20 @@ final class UsageStatsProto {
                break;
            case UsageEvents.Event.NOTIFICATION_INTERRUPTION:
                if (event.mNotificationChannelId != null) {
                    final int channelIndex = stats.mStringCache.indexOf(
                            event.mNotificationChannelId);
                    if (channelIndex >= 0) {
                        proto.write(IntervalStatsProto.Event.NOTIFICATION_CHANNEL_INDEX,
                                channelIndex + 1);
                    } else {
                        // Channel not in Stringpool for some reason, write full string instead
                        Slog.w(TAG, "Usage event notification channel name ("
                                + event.mNotificationChannelId
                                + ") not found in IntervalStats string cache");
                        proto.write(IntervalStatsProto.Event.NOTIFICATION_CHANNEL,
                                event.mNotificationChannelId);
                    }
                }
                break;
        }
        proto.end(token);
@@ -368,6 +455,7 @@ final class UsageStatsProto {
     */
    public static void read(InputStream in, IntervalStats statsOut) throws IOException {
        final ProtoInputStream proto = new ProtoInputStream(in);
        List<String> stringPool = null;

        statsOut.packageStats.clear();
        statsOut.configurations.clear();
@@ -399,14 +487,18 @@ final class UsageStatsProto {
                    loadCountAndTime(proto, IntervalStatsProto.KEYGUARD_HIDDEN,
                            statsOut.keyguardHiddenTracker);
                    break;
                case (int) IntervalStatsProto.STRINGPOOL:
                    stringPool = readStringPool(proto);
                    statsOut.mStringCache.addAll(stringPool);
                    break;
                case (int) IntervalStatsProto.PACKAGES:
                    loadUsageStats(proto, IntervalStatsProto.PACKAGES, statsOut);
                    loadUsageStats(proto, IntervalStatsProto.PACKAGES, statsOut, stringPool);
                    break;
                case (int) IntervalStatsProto.CONFIGURATIONS:
                    loadConfigStats(proto, IntervalStatsProto.CONFIGURATIONS, statsOut);
                    break;
                case (int) IntervalStatsProto.EVENT_LOG:
                    loadEvent(proto, IntervalStatsProto.EVENT_LOG, statsOut);
                    loadEvent(proto, IntervalStatsProto.EVENT_LOG, statsOut, stringPool);
                    break;
                case ProtoInputStream.NO_MORE_FIELDS:
                    if (statsOut.endTime == 0) {
@@ -426,8 +518,10 @@ final class UsageStatsProto {
     */
    public static void write(OutputStream out, IntervalStats stats) throws IOException {
        final ProtoOutputStream proto = new ProtoOutputStream(out);

        proto.write(IntervalStatsProto.END_TIME_MS, stats.endTime - stats.beginTime);
        // String pool should be written before the rest of the usage stats
        writeStringPool(proto, stats);

        writeCountAndTime(proto, IntervalStatsProto.INTERACTIVE, stats.interactiveTracker.count,
                stats.interactiveTracker.duration);
        writeCountAndTime(proto, IntervalStatsProto.NON_INTERACTIVE,
+1 −5
Original line number Diff line number Diff line
@@ -196,11 +196,7 @@ final class UsageStatsXmlV1 {
                event.mNotificationChannelId = (channelId != null) ? channelId.intern() : null;
                break;
        }

        if (statsOut.events == null) {
            statsOut.events = new EventList();
        }
        statsOut.events.insert(event);
        statsOut.addEvent(event);
    }

    private static void writeUsageStats(XmlSerializer xml, final IntervalStats stats,
+1 −5
Original line number Diff line number Diff line
@@ -176,12 +176,8 @@ class UserUsageStatsService {
                    currentDailyStats.activeConfiguration, newFullConfig);
        }

        // Add the event to the daily list.
        if (currentDailyStats.events == null) {
            currentDailyStats.events = new EventList();
        }
        if (event.mEventType != UsageEvents.Event.SYSTEM_INTERACTION) {
            currentDailyStats.events.insert(event);
            currentDailyStats.addEvent(event);
        }

        boolean incrementAppLaunch = false;