chromium/components/module_installer/android/java/src/org/chromium/components/module_installer/logger/SplitAvailabilityLogger.java

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.components.module_installer.logger;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.SystemClock;
import android.util.SparseLongArray;

import com.google.android.play.core.splitinstall.SplitInstallManager;
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory;
import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus;

import org.chromium.base.ContextUtils;
import org.chromium.base.metrics.RecordHistogram;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Record start time in order to later report the install duration via UMA. We want to make
 * a difference between modules that have been requested first before and after the last
 * Chrome start. Modules that have been requested before may install quicker as they may be
 * installed form cache. To do this, we use shared prefs to track modules previously
 * requested. Additionally, storing requested modules helps us to record module install
 * status at next Chrome start.
 */
public class SplitAvailabilityLogger {
    // These values are persisted to logs. Entries should not be renumbered and
    // numeric values should never be reused.
    private static final int REQUESTED = 0;
    private static final int INSTALLED_REQUESTED = 1;
    private static final int INSTALLED_UNREQUESTED = 2;

    // Keep this one at the end and increment appropriately when adding new status.
    private static final int COUNT = 3;

    private static final String ONDEMAND_REQ_PREV = "key_modules_requested_previously";
    private static final String DEFERRED_REQ_PREV = "key_modules_deferred_requested_previously";

    private final Map<String, InstallTimes> mInstallTimesMap = new HashMap<>();

    /** Records via UMA all modules that have been requested and are currently installed. */
    public static void logModuleAvailability() {
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        Set<String> requestedModules = new HashSet<>();
        requestedModules.addAll(prefs.getStringSet(ONDEMAND_REQ_PREV, new HashSet<>()));
        requestedModules.addAll(prefs.getStringSet(DEFERRED_REQ_PREV, new HashSet<>()));

        Context context = ContextUtils.getApplicationContext();
        SplitInstallManager manager = SplitInstallManagerFactory.create(context);
        Set<String> installedModules = manager.getInstalledModules();

        for (String name : requestedModules) {
            recordAvailabilityStatus(
                    name, installedModules.contains(name) ? INSTALLED_REQUESTED : REQUESTED);
        }

        for (String name : installedModules) {
            if (!requestedModules.contains(name)) {
                recordAvailabilityStatus(name, INSTALLED_UNREQUESTED);
            }
        }
    }

    private static void recordAvailabilityStatus(String moduleName, int status) {
        String key = "Android.FeatureModules.AvailabilityStatus." + moduleName;
        RecordHistogram.recordEnumeratedHistogram(key, status, COUNT);
    }

    /**
     * Records via UMA module install times divided into install steps.
     *
     * @param moduleName The module name.
     */
    public void logInstallTimes(String moduleName) {
        recordInstallTime(
                moduleName,
                "",
                SplitInstallSessionStatus.UNKNOWN,
                SplitInstallSessionStatus.INSTALLED);
        recordInstallTime(
                moduleName,
                ".PendingDownload",
                SplitInstallSessionStatus.UNKNOWN,
                SplitInstallSessionStatus.DOWNLOADING);
        recordInstallTime(
                moduleName,
                ".Download",
                SplitInstallSessionStatus.DOWNLOADING,
                SplitInstallSessionStatus.INSTALLING);
        recordInstallTime(
                moduleName,
                ".Installing",
                SplitInstallSessionStatus.INSTALLING,
                SplitInstallSessionStatus.INSTALLED);
    }

    /**
     * Records the start time of an on-demand install request.
     *
     * @param moduleName The module name.
     */
    public void storeRequestStart(String moduleName) {
        // Ignore previously failed requests (orphan keys).
        boolean moduleRequested = storeModuleRequested(moduleName, ONDEMAND_REQ_PREV);
        mInstallTimesMap.put(moduleName, new InstallTimes(moduleRequested));
    }

    /**
     * Records module deferred requested.
     *
     * @param moduleName The module name.
     */
    public void storeRequestDeferredStart(String moduleName) {
        storeModuleRequested(moduleName, DEFERRED_REQ_PREV);
    }

    /**
     * Records that a module has been installed on-demand.
     *
     * @param moduleName The module name.
     * @param status The install status.
     */
    public void storeModuleInstalled(String moduleName, int status) {
        if (!mInstallTimesMap.containsKey(moduleName)) {
            return;
        }
        InstallTimes times = mInstallTimesMap.get(moduleName);
        times.mInstallTimes.put(status, SystemClock.uptimeMillis());
    }

    /**
     * Stores to shared prevs that a module has been requested.
     *
     * @param moduleName Module that has been requested.
     * @param prefKey Pref key pointing to a string set to which the requested module will be added.
     * @return Whether the module has been requested previously.
     */
    private boolean storeModuleRequested(String moduleName, String prefKey) {
        SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
        Set<String> modulesRequestedPreviously = prefs.getStringSet(prefKey, new HashSet<>());
        Set<String> newModulesRequestedPreviously = new HashSet<>(modulesRequestedPreviously);
        newModulesRequestedPreviously.add(moduleName);
        SharedPreferences.Editor editor = prefs.edit();
        editor.putStringSet(prefKey, newModulesRequestedPreviously);
        editor.apply();
        return modulesRequestedPreviously.contains(moduleName);
    }

    private void recordInstallTime(
            String moduleName, String histogramSubname, int startKey, int endKey) {
        if (!mInstallTimesMap.containsKey(moduleName)) {
            return;
        }

        InstallTimes installTimes = mInstallTimesMap.get(moduleName);
        long startTime = installTimes.mInstallTimes.get(startKey);
        long endTime = installTimes.mInstallTimes.get(endKey);

        if (startTime == 0 || endTime == 0) {
            // Time stamps for install times have not been stored.
            // Don't record anything to not skew data.
            return;
        }

        String cacheKey = installTimes.mIsCached ? "Cached" : "Uncached";
        long timing = endTime - startTime;
        String key =
                String.format(
                        "Android.FeatureModules.%sAwakeInstallDuration%s.%s",
                        cacheKey, histogramSubname, moduleName);

        RecordHistogram.recordLongTimesHistogram(key, timing);
    }

    private static class InstallTimes {
        public final boolean mIsCached;
        public final SparseLongArray mInstallTimes = new SparseLongArray();

        public InstallTimes(boolean isCached) {
            mIsCached = isCached;
            mInstallTimes.put(SplitInstallSessionStatus.UNKNOWN, SystemClock.uptimeMillis());
        }
    }
}