blob: 5307941eb63a9547c1f1ca339c274af7d401049a [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* 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 com.google.android.libraries.mobiledatadownload.internal.logging;
import static com.google.android.libraries.mobiledatadownload.internal.MddConstants.SPLIT_CHAR;
import android.content.Context;
import android.util.Pair;
import com.google.android.libraries.mobiledatadownload.SilentFeedback;
import com.google.android.libraries.mobiledatadownload.annotations.InstanceId;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.internal.ApplicationContext;
import com.google.android.libraries.mobiledatadownload.internal.FileGroupsMetadata;
import com.google.android.libraries.mobiledatadownload.internal.MddConstants;
import com.google.android.libraries.mobiledatadownload.internal.SharedFileManager;
import com.google.android.libraries.mobiledatadownload.internal.SharedFileMissingException;
import com.google.android.libraries.mobiledatadownload.internal.SharedFilesMetadata;
import com.google.android.libraries.mobiledatadownload.internal.annotations.SequentialControlExecutor;
import com.google.android.libraries.mobiledatadownload.internal.util.FileGroupUtil;
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFluentFuture;
import com.google.android.libraries.mobiledatadownload.tracing.PropagatedFutures;
import com.google.auto.value.AutoValue;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.util.concurrent.FluentFuture;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.mobiledatadownload.internal.MetadataProto.DataFile;
import com.google.mobiledatadownload.internal.MetadataProto.DataFileGroupInternal;
import com.google.mobiledatadownload.internal.MetadataProto.GroupKey;
import com.google.mobiledatadownload.internal.MetadataProto.NewFileKey;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
/**
* Log MDD storage stats at daily maintenance. For each file group, it will log the total bytes used
* on disk for that file group and the bytes used by the downloaded group.
*/
public class StorageLogger {
private static final String TAG = "StorageLogger";
private final FileGroupsMetadata fileGroupsMetadata;
private final SharedFileManager sharedFileManager;
private final SynchronousFileStorage fileStorage;
private final EventLogger eventLogger;
private final Context context;
private final SilentFeedback silentFeedback;
private final Optional<String> instanceId;
private final Executor sequentialControlExecutor;
/** Store the storage stats for a file group. */
static class GroupStorage {
// The sum of all on-disk file sizes of the files belonging to this file group, in bytes.
public long totalBytesUsed;
// The sum of all on-disk inline file sizes of the files belonging to this file group, in bytes.
public long totalInlineBytesUsed;
// The sum of all on-disk file sizes of this downloaded file group in bytes.
public long downloadedGroupBytesUsed;
// The sum of all on-disk inline files sizes of this downloaded file group in bytes.
public long downloadedGroupInlineBytesUsed;
// The total number of files in the group.
public int totalFileCount;
// The number of inline files in the group.
public int totalInlineFileCount;
}
@Inject
public StorageLogger(
@ApplicationContext Context context,
FileGroupsMetadata fileGroupsMetadata,
SharedFileManager sharedFileManager,
SynchronousFileStorage fileStorage,
EventLogger eventLogger,
SilentFeedback silentFeedback,
@InstanceId Optional<String> instanceId,
@SequentialControlExecutor Executor sequentialControlExecutor) {
this.context = context;
this.fileGroupsMetadata = fileGroupsMetadata;
this.sharedFileManager = sharedFileManager;
this.fileStorage = fileStorage;
this.eventLogger = eventLogger;
this.silentFeedback = silentFeedback;
this.instanceId = instanceId;
this.sequentialControlExecutor = sequentialControlExecutor;
}
// TODO(b/64764648): Combine this with MobileDataDownloadManager.createGroupKey
private static GroupKey createGroupKey(DataFileGroupInternal fileGroup) {
GroupKey.Builder groupKey = GroupKey.newBuilder().setGroupName(fileGroup.getGroupName());
if (Strings.isNullOrEmpty(fileGroup.getOwnerPackage())) {
groupKey.setOwnerPackage(MddConstants.GMS_PACKAGE);
} else {
groupKey.setOwnerPackage(fileGroup.getOwnerPackage());
}
return groupKey.build();
}
public ListenableFuture<Void> logStorageStats(int daysSinceLastLog) {
return eventLogger.logMddStorageStats(() -> buildStorageStatsIcingLogData(daysSinceLastLog));
}
private ListenableFuture<Void> buildStorageStatsIcingLogData(int daysSinceLastLog) {
return PropagatedFluentFuture.from(fileGroupsMetadata.getAllFreshGroups())
.transformAsync(
allGroups ->
PropagatedFutures.transformAsync(
fileGroupsMetadata.getAllStaleGroups(),
staleGroups ->
buildStorageStatsInternal(allGroups, staleGroups, daysSinceLastLog),
sequentialControlExecutor),
sequentialControlExecutor);
}
private ListenableFuture<Void> buildStorageStatsInternal(
List<Pair<GroupKey, DataFileGroupInternal>> allKeysAndGroupPairs,
List<DataFileGroupInternal> staleGroups,
int daysSinceLastLog) {
List<GroupKeyAndDataFileGroupInternal> allKeysAndGroups = new ArrayList<>();
for (Pair<GroupKey, DataFileGroupInternal> groupKeyAndGroup : allKeysAndGroupPairs) {
allKeysAndGroups.add(
GroupKeyAndDataFileGroupInternal.create(groupKeyAndGroup.first, groupKeyAndGroup.second));
}
// Adding staleGroups to allGroups.
for (DataFileGroupInternal fileGroup : staleGroups) {
allKeysAndGroups.add(
GroupKeyAndDataFileGroupInternal.create(createGroupKey(fileGroup), fileGroup));
}
Map<String, GroupStorage> groupKeyToGroupStorage = new HashMap<>();
Map<String, Set<NewFileKey>> groupKeyToFileKeys = new HashMap<>();
Map<String, Set<NewFileKey>> downloadedGroupKeyToFileKeys = new HashMap<>();
Map<String, DataFileGroupInternal> downloadedGroupKeyToDataFileGroup = new HashMap<>();
Set<NewFileKey> allFileKeys = new HashSet<>();
// Our bytes counter has to be wrapped in an Object because variables captured by lambda
// expressions need to be "effectively final" - meaning they never appear on the left-hand side
// of an assignment statement. As such, we use AtomicLong.
AtomicLong totalMddBytesUsed = new AtomicLong(0L);
List<ListenableFuture<Void>> futures = new ArrayList<>();
for (GroupKeyAndDataFileGroupInternal groupKeyAndGroup : allKeysAndGroups) {
Set<NewFileKey> fileKeys =
safeGetFileKeys(
groupKeyToFileKeys, getGroupWithOwnerPackageKey(groupKeyAndGroup.groupKey()));
GroupStorage groupStorage =
safeGetGroupStorage(
groupKeyToGroupStorage, getGroupWithOwnerPackageKey(groupKeyAndGroup.groupKey()));
Set<NewFileKey> downloadedFileKeysInit = null;
if (groupKeyAndGroup.groupKey().getDownloaded()) {
downloadedFileKeysInit =
safeGetFileKeys(
downloadedGroupKeyToFileKeys,
getGroupWithOwnerPackageKey(groupKeyAndGroup.groupKey()));
downloadedGroupKeyToDataFileGroup.put(
getGroupWithOwnerPackageKey(groupKeyAndGroup.groupKey()),
groupKeyAndGroup.dataFileGroupInternal());
}
// Variables captured by lambdas must be effectively final.
Set<NewFileKey> downloadedFileKeys = downloadedFileKeysInit;
int totalFileCount = groupKeyAndGroup.dataFileGroupInternal().getFileCount();
for (DataFile dataFile : groupKeyAndGroup.dataFileGroupInternal().getFileList()) {
boolean isInlineFile = FileGroupUtil.isInlineFile(dataFile);
NewFileKey fileKey =
SharedFilesMetadata.createKeyFromDataFile(
dataFile, groupKeyAndGroup.dataFileGroupInternal().getAllowedReadersEnum());
futures.add(
Futures.transform(
computeFileSize(fileKey),
fileSize -> {
if (!allFileKeys.contains(fileKey)) {
totalMddBytesUsed.getAndAdd(fileSize);
allFileKeys.add(fileKey);
}
// Check if we have processed this fileKey before.
if (!fileKeys.contains(fileKey)) {
if (isInlineFile) {
groupStorage.totalInlineBytesUsed += fileSize;
}
groupStorage.totalBytesUsed += fileSize;
fileKeys.add(fileKey);
}
if (groupKeyAndGroup.groupKey().getDownloaded()) {
// Note: Nullness checker is not smart enough to figure out that
// downloadedFileKeys is never null.
Preconditions.checkNotNull(downloadedFileKeys);
// Check if we have processed this fileKey before.
if (!downloadedFileKeys.contains(fileKey)) {
if (isInlineFile) {
groupStorage.downloadedGroupInlineBytesUsed += fileSize;
groupStorage.totalInlineFileCount += 1;
}
groupStorage.downloadedGroupBytesUsed += fileSize;
downloadedFileKeys.add(fileKey);
}
}
return null;
},
sequentialControlExecutor));
}
groupStorage.totalFileCount = totalFileCount;
}
return Futures.whenAllComplete(futures)
.call(
() -> {
Void storageStatsBuilder = null;
return storageStatsBuilder;
},
sequentialControlExecutor);
}
private String getGroupWithOwnerPackageKey(GroupKey groupKey) {
return new StringBuilder(groupKey.getGroupName())
.append(SPLIT_CHAR)
.append(groupKey.getOwnerPackage())
.toString();
}
private Set<NewFileKey> safeGetFileKeys(
Map<String, Set<NewFileKey>> groupNameToFileKeys, String groupName) {
Set<NewFileKey> fileKeys = groupNameToFileKeys.get(groupName);
if (fileKeys == null) {
groupNameToFileKeys.put(groupName, new HashSet<>());
fileKeys = groupNameToFileKeys.get(groupName);
}
return fileKeys;
}
private GroupStorage safeGetGroupStorage(
Map<String, GroupStorage> groupNameToStats, String groupName) {
GroupStorage groupStorage = groupNameToStats.get(groupName);
if (groupStorage == null) {
groupNameToStats.put(groupName, new GroupStorage());
groupStorage = groupNameToStats.get(groupName);
}
return groupStorage;
}
private ListenableFuture<Long> computeFileSize(NewFileKey newFileKey) {
return FluentFuture.from(sharedFileManager.getOnDeviceUri(newFileKey))
.catchingAsync(
SharedFileMissingException.class,
e -> Futures.immediateFuture(null),
sequentialControlExecutor)
.transform(
fileUri -> {
if (fileUri != null) {
try {
return fileStorage.fileSize(fileUri);
} catch (IOException e) {
LogUtil.e(e, "%s: Failed to call mobstore fileSize on uri %s!", TAG, fileUri);
}
}
return 0L;
},
sequentialControlExecutor);
}
@AutoValue
abstract static class GroupKeyAndDataFileGroupInternal {
static GroupKeyAndDataFileGroupInternal create(
GroupKey groupKey, DataFileGroupInternal dataFileGroupInternal) {
return new AutoValue_StorageLogger_GroupKeyAndDataFileGroupInternal(
groupKey, dataFileGroupInternal);
}
abstract GroupKey groupKey();
abstract DataFileGroupInternal dataFileGroupInternal();
}
}