blob: 90d3677df9a5d2eeafeddb514e689f22ced519fc [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.background_task_scheduler.internal;
import android.content.SharedPreferences;
import androidx.annotation.VisibleForTesting;
import org.chromium.base.ContextUtils;
import org.chromium.base.ThreadUtils;
import org.chromium.base.library_loader.LibraryLoader;
import org.chromium.base.metrics.RecordHistogram;
import org.chromium.components.background_task_scheduler.BackgroundTaskSchedulerExternalUma;
import java.util.HashSet;
import java.util.Set;
/**
* Helper class to report UMA.
*/
public class BackgroundTaskSchedulerUma extends BackgroundTaskSchedulerExternalUma {
static final String KEY_CACHED_UMA = "bts_cached_uma";
private static BackgroundTaskSchedulerUma sInstance;
private static class CachedUmaEntry {
private static final String SEPARATOR = ":";
private String mEvent;
private int mValue;
private int mCount;
/**
* Parses a cached UMA entry from a string.
*
* @param entry A serialized entry from preferences store.
* @return A parsed CachedUmaEntry object, or <c>null</c> if parsing failed.
*/
public static CachedUmaEntry parseEntry(String entry) {
if (entry == null) return null;
String[] entryParts = entry.split(SEPARATOR);
if (entryParts.length != 3 || entryParts[0].isEmpty() || entryParts[1].isEmpty()
|| entryParts[2].isEmpty()) {
return null;
}
int value = -1;
int count = -1;
try {
value = Integer.parseInt(entryParts[1]);
count = Integer.parseInt(entryParts[2]);
} catch (NumberFormatException e) {
return null;
}
return new CachedUmaEntry(entryParts[0], value, count);
}
/** Returns a string for partial matching of the prefs entry. */
public static String getStringForPartialMatching(String event, int value) {
return event + SEPARATOR + value + SEPARATOR;
}
public CachedUmaEntry(String event, int value, int count) {
mEvent = event;
mValue = value;
mCount = count;
}
/** Converts cached UMA entry to a string in format: EVENT:VALUE:COUNT. */
@Override
public String toString() {
return mEvent + SEPARATOR + mValue + SEPARATOR + mCount;
}
/** Gets the name of the event (UMA). */
public String getEvent() {
return mEvent;
}
/** Gets the value of the event (concrete value of the enum). */
public int getValue() {
return mValue;
}
/** Gets the count of events that happened. */
public int getCount() {
return mCount;
}
/** Increments the count of the event. */
public void increment() {
mCount++;
}
}
public static BackgroundTaskSchedulerUma getInstance() {
if (sInstance == null) {
sInstance = new BackgroundTaskSchedulerUma();
}
return sInstance;
}
@VisibleForTesting
public static void setInstanceForTesting(BackgroundTaskSchedulerUma instance) {
sInstance = instance;
}
/** Reports metrics for task scheduling and whether it was successful. */
public void reportTaskScheduled(int taskId, boolean success) {
if (success) {
cacheEvent("Android.BackgroundTaskScheduler.TaskScheduled.Success",
toUmaEnumValueFromTaskId(taskId));
} else {
cacheEvent("Android.BackgroundTaskScheduler.TaskScheduled.Failure",
toUmaEnumValueFromTaskId(taskId));
}
}
/** Reports metrics for creating an exact tasks. */
public void reportExactTaskCreated(int taskId) {
cacheEvent("Android.BackgroundTaskScheduler.ExactTaskCreated",
toUmaEnumValueFromTaskId(taskId));
}
/** Reports metrics for task scheduling with the expiration feature activated. */
public void reportTaskCreatedAndExpirationState(int taskId, boolean expires) {
if (expires) {
cacheEvent("Android.BackgroundTaskScheduler.TaskCreated.WithExpiration",
toUmaEnumValueFromTaskId(taskId));
} else {
cacheEvent("Android.BackgroundTaskScheduler.TaskCreated.WithoutExpiration",
toUmaEnumValueFromTaskId(taskId));
}
}
/** Reports metrics for not starting a task because of expiration. */
public void reportTaskExpired(int taskId) {
cacheEvent("Android.BackgroundTaskScheduler.TaskExpired", toUmaEnumValueFromTaskId(taskId));
}
/** Reports metrics for task canceling. */
public void reportTaskCanceled(int taskId) {
cacheEvent(
"Android.BackgroundTaskScheduler.TaskCanceled", toUmaEnumValueFromTaskId(taskId));
}
/** Reports metrics for starting a task. */
public void reportTaskStarted(int taskId) {
cacheEvent("Android.BackgroundTaskScheduler.TaskStarted", toUmaEnumValueFromTaskId(taskId));
}
/** Reports metrics for stopping a task. */
public void reportTaskStopped(int taskId) {
cacheEvent("Android.BackgroundTaskScheduler.TaskStopped", toUmaEnumValueFromTaskId(taskId));
}
/** Reports metrics for rescheduling a task. */
public void reportTaskRescheduled() {
cacheEvent("Android.BackgroundTaskScheduler.TaskRescheduled", 0);
}
/** Reports metrics for migrating scheduled tasks to Protocol Buffer data format. */
public void reportMigrationToProto(int taskId) {
cacheEvent("Android.BackgroundTaskScheduler.MigrationToProto",
toUmaEnumValueFromTaskId(taskId));
}
@Override
public void reportTaskStartedNative(int taskId, boolean serviceManagerOnlyMode) {
int umaEnumValue = toUmaEnumValueFromTaskId(taskId);
cacheEvent("Android.BackgroundTaskScheduler.TaskLoadedNative", umaEnumValue);
if (serviceManagerOnlyMode) {
cacheEvent(
"Android.BackgroundTaskScheduler.TaskLoadedNative.ReducedMode", umaEnumValue);
} else {
cacheEvent(
"Android.BackgroundTaskScheduler.TaskLoadedNative.FullBrowser", umaEnumValue);
}
}
@Override
public void reportNativeTaskStarted(int taskId, boolean serviceManagerOnlyMode) {
int umaEnumValue = toUmaEnumValueFromTaskId(taskId);
cacheEvent("Android.NativeBackgroundTask.TaskStarted", umaEnumValue);
if (serviceManagerOnlyMode) {
cacheEvent("Android.NativeBackgroundTask.TaskStarted.ReducedMode", umaEnumValue);
} else {
cacheEvent("Android.NativeBackgroundTask.TaskStarted.FullBrowser", umaEnumValue);
}
}
@Override
public void reportNativeTaskFinished(int taskId, boolean serviceManagerOnlyMode) {
int umaEnumValue = toUmaEnumValueFromTaskId(taskId);
cacheEvent("Android.NativeBackgroundTask.TaskFinished", umaEnumValue);
if (serviceManagerOnlyMode) {
cacheEvent("Android.NativeBackgroundTask.TaskFinished.ReducedMode", umaEnumValue);
} else {
cacheEvent("Android.NativeBackgroundTask.TaskFinished.FullBrowser", umaEnumValue);
}
}
@Override
public void reportStartupMode(int startupMode) {
// We don't record full browser's warm startup since most of the full browser warm startup
// don't even reach here.
if (startupMode < 0) return;
cacheEvent("Servicification.Startup3", startupMode);
}
/** Method that actually invokes histogram recording. Extracted for testing. */
@VisibleForTesting
void recordEnumeratedHistogram(String histogram, int value, int maxCount) {
RecordHistogram.recordEnumeratedHistogram(histogram, value, maxCount);
}
/** Records histograms for cached stats. Should only be called when native is initialized. */
public void flushStats() {
assertNativeIsLoaded();
ThreadUtils.assertOnUiThread();
Set<String> cachedUmaStrings = getCachedUmaEntries(ContextUtils.getAppSharedPreferences());
for (String cachedUmaString : cachedUmaStrings) {
CachedUmaEntry entry = CachedUmaEntry.parseEntry(cachedUmaString);
if (entry == null) continue;
for (int i = 0; i < entry.getCount(); i++) {
recordEnumeratedHistogram(
entry.getEvent(), entry.getValue(), BACKGROUND_TASK_COUNT);
}
}
// Once all metrics are reported, we can simply remove the shared preference key.
removeCachedStats();
}
/** Removes all of the cached stats without reporting. */
public void removeCachedStats() {
ThreadUtils.assertOnUiThread();
ContextUtils.getAppSharedPreferences().edit().remove(KEY_CACHED_UMA).apply();
}
/** Caches the event to be reported through UMA in shared preferences. */
@VisibleForTesting
void cacheEvent(String event, int value) {
SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
Set<String> cachedUmaStrings = getCachedUmaEntries(prefs);
String partialMatch = CachedUmaEntry.getStringForPartialMatching(event, value);
String existingEntry = null;
for (String cachedUmaString : cachedUmaStrings) {
if (cachedUmaString.startsWith(partialMatch)) {
existingEntry = cachedUmaString;
break;
}
}
Set<String> setToWriteBack = new HashSet<>(cachedUmaStrings);
CachedUmaEntry entry = null;
if (existingEntry != null) {
entry = CachedUmaEntry.parseEntry(existingEntry);
if (entry == null) {
entry = new CachedUmaEntry(event, value, 1);
}
setToWriteBack.remove(existingEntry);
entry.increment();
} else {
entry = new CachedUmaEntry(event, value, 1);
}
setToWriteBack.add(entry.toString());
updateCachedUma(prefs, setToWriteBack);
}
@VisibleForTesting
static Set<String> getCachedUmaEntries(SharedPreferences prefs) {
Set<String> cachedUmaEntries = prefs.getStringSet(KEY_CACHED_UMA, new HashSet<>());
return sanitizeEntrySet(cachedUmaEntries);
}
@VisibleForTesting
static void updateCachedUma(SharedPreferences prefs, Set<String> cachedUma) {
ThreadUtils.assertOnUiThread();
SharedPreferences.Editor editor = prefs.edit();
editor.putStringSet(KEY_CACHED_UMA, sanitizeEntrySet(cachedUma));
editor.apply();
}
void assertNativeIsLoaded() {
assert LibraryLoader.getInstance().isInitialized();
}
private static Set<String> sanitizeEntrySet(Set<String> set) {
if (set != null && set.contains(null)) {
set.remove(null);
}
return set;
}
}