Update SyncStorageEngine to use protos.

Status and Statistics info within SyncStorageEngine was being stored
using Parcels, which is not recommended. Updating both of them to use
protos now. Additionally, because of protos, storage space on disk
consumed by these files are reduced by more than 50%.

Also added unit tests for SyncStorageEngine.

Bug: 38177679
Test: atest com.android.server.content.SyncStorageEngineTest
Test: build and flash - no errors in logs
Change-Id: I187cb6c271333f366f5c7f651f75b4161fee90d4
This commit is contained in:
Varun Shah
2019-10-30 17:40:15 -07:00
parent 89cc836c2e
commit 816cf63bc5
4 changed files with 811 additions and 74 deletions

View File

@@ -20,6 +20,9 @@ import android.annotation.UnsupportedAppUsage;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
import android.util.Pair;
import com.android.internal.util.ArrayUtils;
import java.util.ArrayList;
import java.util.Calendar;
@@ -139,10 +142,10 @@ public class SyncStatusInfo implements Parcelable {
public final long[] perSourceLastSuccessTimes = new long[SOURCE_COUNT];
public final long[] perSourceLastFailureTimes = new long[SOURCE_COUNT];
// Warning: It is up to the external caller to ensure there are
// no race conditions when accessing this list
@UnsupportedAppUsage
private ArrayList<Long> periodicSyncTimes;
// Warning: It is up to the external caller to ensure there are
// no race conditions when accessing this list
@UnsupportedAppUsage
private ArrayList<Long> periodicSyncTimes;
private final ArrayList<Long> mLastEventTimes = new ArrayList<>();
private final ArrayList<String> mLastEvents = new ArrayList<>();
@@ -292,9 +295,28 @@ public class SyncStatusInfo implements Parcelable {
}
}
/**
* Copies all data from the given SyncStatusInfo object.
*
* @param other the SyncStatusInfo object to copy data from
*/
public SyncStatusInfo(SyncStatusInfo other) {
authorityId = other.authorityId;
copyFrom(other);
}
/**
* Copies all data from the given SyncStatusInfo object except for its authority id.
*
* @param authorityId the new authority id
* @param other the SyncStatusInfo object to copy data from
*/
public SyncStatusInfo(int authorityId, SyncStatusInfo other) {
this.authorityId = authorityId;
copyFrom(other);
}
private void copyFrom(SyncStatusInfo other) {
other.totalStats.copyTo(totalStats);
other.todayStats.copyTo(todayStats);
other.yesterdayStats.copyTo(yesterdayStats);
@@ -323,6 +345,14 @@ public class SyncStatusInfo implements Parcelable {
System.arraycopy(from, 0, to, 0, to.length);
}
public int getPeriodicSyncTimesSize() {
return periodicSyncTimes == null ? 0 : periodicSyncTimes.size();
}
public void addPeriodicSyncTime(long time) {
periodicSyncTimes = ArrayUtils.add(periodicSyncTimes, time);
}
@UnsupportedAppUsage
public void setPeriodicSyncTime(int index, long when) {
// The list is initialized lazily when scheduling occurs so we need to make sure
@@ -347,6 +377,24 @@ public class SyncStatusInfo implements Parcelable {
}
}
/**
* Populates {@code mLastEventTimes} and {@code mLastEvents} with the given list. <br>
* <i>Note: This method is mainly used to repopulate the event info from disk and it will clear
* both {@code mLastEventTimes} and {@code mLastEvents} before populating.</i>
*
* @param lastEventInformation the list to populate with
*/
public void populateLastEventsInformation(ArrayList<Pair<Long, String>> lastEventInformation) {
mLastEventTimes.clear();
mLastEvents.clear();
final int size = lastEventInformation.size();
for (int i = 0; i < size; i++) {
final Pair<Long, String> lastEventInfo = lastEventInformation.get(i);
mLastEventTimes.add(lastEventInfo.first);
mLastEvents.add(lastEventInfo.second);
}
}
/** */
public void addEvent(String message) {
if (mLastEventTimes.size() >= MAX_EVENT_COUNT) {

View File

@@ -0,0 +1,86 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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.
*/
syntax = "proto2";
package com.android.server.content;
import "frameworks/base/core/proto/android/privacy.proto";
option java_multiple_files = true;
/**
* Stores relevant information from a DayStats object in SyncStorageEngine.
*/
message SyncStatisticsProto {
message DayStats {
optional int32 day = 1; // day of the year - defined by SyncStorageEngine#getCurrentDayLocked()
optional int32 success_count = 2;
optional int64 success_time = 3; // time since epoch
optional int32 failure_count = 4;
optional int64 failure_time = 5; // time since epoch
}
repeated DayStats stats = 1;
}
/**
* Stores relevant information from a SyncStatusInfo object.
*/
message SyncStatusProto {
message StatusInfo {
message Stats {
optional int64 total_elapsed_time = 1; // time since epoch
optional int32 num_syncs = 2;
optional int32 num_failures = 3;
optional int32 num_cancels = 4;
optional int32 num_source_other = 5;
optional int32 num_source_local = 6;
optional int32 num_source_poll = 7;
optional int32 num_source_user = 8;
optional int32 num_source_periodic = 9;
optional int32 num_source_feed = 10;
}
message LastEventInfo {
optional int64 last_event_time = 1; // time since epoch
optional string last_event = 2;
}
// Note: version doesn't need to be stored in proto because of how protos store information but
// leaving field number 1 open in case we find a usage for it in the future.
optional int32 authority_id = 2;
optional int64 last_success_time = 3; // time since epoch
optional int32 last_success_source = 4;
optional int64 last_failure_time = 5; // time since epoch
optional int32 last_failure_source = 6;
optional string last_failure_message = 7;
optional int64 initial_failure_time = 8; // time since epoch
optional bool pending = 9;
optional bool initialize = 10;
repeated int64 periodic_sync_times = 11; // times since epoch
repeated LastEventInfo last_event_info = 12;
optional int64 last_today_reset_time = 13; // time since epoch
optional Stats total_stats = 14;
optional Stats today_stats = 15;
optional Stats yesterday_stats = 16;
repeated int64 per_source_last_success_times = 17; // times since epoch
repeated int64 per_source_last_failure_times = 18; // times since epoch
}
repeated StatusInfo status = 1;
}

View File

@@ -51,6 +51,8 @@ import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
@@ -64,6 +66,9 @@ import org.xmlpull.v1.XmlSerializer;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Calendar;
@@ -428,7 +433,8 @@ public class SyncStorageEngine {
}
// Primary list of all syncable authorities. Also our global lock.
private final SparseArray<AuthorityInfo> mAuthorities =
@VisibleForTesting
final SparseArray<AuthorityInfo> mAuthorities =
new SparseArray<AuthorityInfo>();
private final HashMap<AccountAndUser, AccountInfo> mAccounts
@@ -437,7 +443,8 @@ public class SyncStorageEngine {
private final SparseArray<ArrayList<SyncInfo>> mCurrentSyncs
= new SparseArray<ArrayList<SyncInfo>>();
private final SparseArray<SyncStatusInfo> mSyncStatus =
@VisibleForTesting
final SparseArray<SyncStatusInfo> mSyncStatus =
new SparseArray<SyncStatusInfo>();
private final ArrayList<SyncHistoryItem> mSyncHistory =
@@ -453,7 +460,8 @@ public class SyncStorageEngine {
private int mNextAuthorityId = 0;
// We keep 4 weeks of stats.
private final DayStats[] mDayStats = new DayStats[7*4];
@VisibleForTesting
final DayStats[] mDayStats = new DayStats[7*4];
private final Calendar mCal;
private int mYear;
private int mYearInDays;
@@ -464,6 +472,18 @@ public class SyncStorageEngine {
private int mSyncRandomOffset;
// STOPSHIP: b/143656271 this should be true on launch
private static final boolean DELETE_LEGACY_PARCEL_FILES = false;
private static final String LEGACY_STATUS_FILE_NAME = "status.bin";
private static final String LEGACY_STATISTICS_FILE_NAME = "stats.bin";
private static final String SYNC_DIR_NAME = "sync";
private static final String ACCOUNT_INFO_FILE_NAME = "accounts.xml";
private static final String STATUS_FILE_NAME = "status";
private static final String STATISTICS_FILE_NAME = "stats";
private File mSyncDir;
/**
* This file contains the core engine state: all accounts and the
* settings for them. It must never be lost, and should be changed
@@ -508,14 +528,15 @@ public class SyncStorageEngine {
com.android.internal.R.bool.config_syncstorageengine_masterSyncAutomatically);
File systemDir = new File(dataDir, "system");
File syncDir = new File(systemDir, "sync");
syncDir.mkdirs();
mSyncDir = new File(systemDir, SYNC_DIR_NAME);
mSyncDir.mkdirs();
maybeDeleteLegacyPendingInfoLocked(syncDir);
maybeDeleteLegacyPendingInfoLocked(mSyncDir);
mAccountInfoFile = new AtomicFile(new File(syncDir, "accounts.xml"), "sync-accounts");
mStatusFile = new AtomicFile(new File(syncDir, "status.bin"), "sync-status");
mStatisticsFile = new AtomicFile(new File(syncDir, "stats.bin"), "sync-stats");
mAccountInfoFile = new AtomicFile(new File(mSyncDir, ACCOUNT_INFO_FILE_NAME),
"sync-accounts");
mStatusFile = new AtomicFile(new File(mSyncDir, STATUS_FILE_NAME), "sync-status");
mStatisticsFile = new AtomicFile(new File(mSyncDir, STATISTICS_FILE_NAME), "sync-stats");
readAccountInfoLocked();
readStatusLocked();
@@ -2017,15 +2038,10 @@ public class SyncStorageEngine {
public static final int STATUS_FILE_END = 0;
public static final int STATUS_FILE_ITEM = 100;
/**
* Read all sync status back in to the initial engine state.
*/
private void readStatusLocked() {
if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
Slog.v(TAG_FILE, "Reading " + mStatusFile.getBaseFile());
}
private void readStatusParcelLocked(File parcel) {
try {
byte[] data = mStatusFile.readFully();
final AtomicFile parcelFile = new AtomicFile(parcel);
byte[] data = parcelFile.readFully();
Parcel in = Parcel.obtain();
in.unmarshall(data, 0, data.length);
in.setDataPosition(0);
@@ -2036,9 +2052,6 @@ public class SyncStorageEngine {
SyncStatusInfo status = new SyncStatusInfo(in);
if (mAuthorities.indexOfKey(status.authorityId) >= 0) {
status.pending = false;
if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
Slog.v(TAG_FILE, "Adding status for id " + status.authorityId);
}
mSyncStatus.put(status.authorityId, status);
}
} catch (Exception e) {
@@ -2050,15 +2063,247 @@ public class SyncStorageEngine {
break;
}
}
} catch (java.io.IOException e) {
} catch (IOException e) {
Slog.i(TAG, "No initial status");
}
}
private void upgradeStatusIfNeededLocked() {
final File parcelStatus = new File(mSyncDir, LEGACY_STATUS_FILE_NAME);
if (parcelStatus.exists() && !mStatusFile.exists()) {
readStatusParcelLocked(parcelStatus);
writeStatusLocked();
}
// if upgrade to proto was successful, delete parcel file
if (DELETE_LEGACY_PARCEL_FILES && mStatusFile.exists()) {
parcelStatus.delete();
}
}
/**
* Read all sync status back in to the initial engine state.
*/
@VisibleForTesting
void readStatusLocked() {
upgradeStatusIfNeededLocked();
if (!mStatusFile.exists()) {
return;
}
try {
try (FileInputStream in = mStatusFile.openRead()) {
readStatusInfoLocked(in);
}
} catch (IOException e) {
Slog.e(TAG, "Unable to read status info file.", e);
}
}
private void readStatusInfoLocked(InputStream in) throws IOException {
final ProtoInputStream proto = new ProtoInputStream(in);
while (true) {
switch (proto.nextField()) {
case (int) SyncStatusProto.STATUS:
final long token = proto.start(SyncStatusProto.STATUS);
final SyncStatusInfo status = readSyncStatusInfoLocked(proto);
proto.end(token);
if (mAuthorities.indexOfKey(status.authorityId) >= 0) {
status.pending = false;
mSyncStatus.put(status.authorityId, status);
}
break;
case ProtoInputStream.NO_MORE_FIELDS:
return;
}
}
}
private SyncStatusInfo readSyncStatusInfoLocked(ProtoInputStream proto) throws IOException {
SyncStatusInfo status;
if (proto.nextField(SyncStatusProto.StatusInfo.AUTHORITY_ID)) {
//fast-path; this should work for most cases since the authority id is written first
status = new SyncStatusInfo(proto.readInt(SyncStatusProto.StatusInfo.AUTHORITY_ID));
} else {
// placeholder to read other data; assume the default authority id as 0
status = new SyncStatusInfo(0);
}
int successTimesCount = 0;
int failureTimesCount = 0;
ArrayList<Pair<Long, String>> lastEventInformation = new ArrayList<>();
while (true) {
switch (proto.nextField()) {
case (int) SyncStatusProto.StatusInfo.AUTHORITY_ID:
// fast-path failed for some reason, rebuild the status from placeholder object
Slog.w(TAG, "Failed to read the authority id via fast-path; "
+ "some data might not have been read.");
status = new SyncStatusInfo(
proto.readInt(SyncStatusProto.StatusInfo.AUTHORITY_ID), status);
break;
case (int) SyncStatusProto.StatusInfo.LAST_SUCCESS_TIME:
status.lastSuccessTime = proto.readLong(
SyncStatusProto.StatusInfo.LAST_SUCCESS_TIME);
break;
case (int) SyncStatusProto.StatusInfo.LAST_SUCCESS_SOURCE:
status.lastSuccessSource = proto.readInt(
SyncStatusProto.StatusInfo.LAST_SUCCESS_SOURCE);
break;
case (int) SyncStatusProto.StatusInfo.LAST_FAILURE_TIME:
status.lastFailureTime = proto.readLong(
SyncStatusProto.StatusInfo.LAST_FAILURE_TIME);
break;
case (int) SyncStatusProto.StatusInfo.LAST_FAILURE_SOURCE:
status.lastFailureSource = proto.readInt(
SyncStatusProto.StatusInfo.LAST_FAILURE_SOURCE);
break;
case (int) SyncStatusProto.StatusInfo.LAST_FAILURE_MESSAGE:
status.lastFailureMesg = proto.readString(
SyncStatusProto.StatusInfo.LAST_FAILURE_MESSAGE);
break;
case (int) SyncStatusProto.StatusInfo.INITIAL_FAILURE_TIME:
status.initialFailureTime = proto.readLong(
SyncStatusProto.StatusInfo.INITIAL_FAILURE_TIME);
break;
case (int) SyncStatusProto.StatusInfo.PENDING:
status.pending = proto.readBoolean(SyncStatusProto.StatusInfo.PENDING);
break;
case (int) SyncStatusProto.StatusInfo.INITIALIZE:
status.initialize = proto.readBoolean(SyncStatusProto.StatusInfo.INITIALIZE);
break;
case (int) SyncStatusProto.StatusInfo.PERIODIC_SYNC_TIMES:
status.addPeriodicSyncTime(
proto.readLong(SyncStatusProto.StatusInfo.PERIODIC_SYNC_TIMES));
break;
case (int) SyncStatusProto.StatusInfo.LAST_EVENT_INFO:
final long eventToken = proto.start(SyncStatusProto.StatusInfo.LAST_EVENT_INFO);
final Pair<Long, String> lastEventInfo = parseLastEventInfoLocked(proto);
if (lastEventInfo != null) {
lastEventInformation.add(lastEventInfo);
}
proto.end(eventToken);
break;
case (int) SyncStatusProto.StatusInfo.LAST_TODAY_RESET_TIME:
status.lastTodayResetTime = proto.readLong(
SyncStatusProto.StatusInfo.LAST_TODAY_RESET_TIME);
break;
case (int) SyncStatusProto.StatusInfo.TOTAL_STATS:
final long totalStatsToken = proto.start(
SyncStatusProto.StatusInfo.TOTAL_STATS);
readSyncStatusStatsLocked(proto, status.totalStats);
proto.end(totalStatsToken);
break;
case (int) SyncStatusProto.StatusInfo.TODAY_STATS:
final long todayStatsToken = proto.start(
SyncStatusProto.StatusInfo.TODAY_STATS);
readSyncStatusStatsLocked(proto, status.todayStats);
proto.end(todayStatsToken);
break;
case (int) SyncStatusProto.StatusInfo.YESTERDAY_STATS:
final long yesterdayStatsToken = proto.start(
SyncStatusProto.StatusInfo.YESTERDAY_STATS);
readSyncStatusStatsLocked(proto, status.yesterdayStats);
proto.end(yesterdayStatsToken);
break;
case (int) SyncStatusProto.StatusInfo.PER_SOURCE_LAST_SUCCESS_TIMES:
final long successTime = proto.readLong(
SyncStatusProto.StatusInfo.PER_SOURCE_LAST_SUCCESS_TIMES);
if (successTimesCount == status.perSourceLastSuccessTimes.length) {
Slog.w(TAG, "Attempted to read more per source last success times "
+ "than expected; data might be corrupted.");
break;
}
status.perSourceLastSuccessTimes[successTimesCount] = successTime;
successTimesCount++;
break;
case (int) SyncStatusProto.StatusInfo.PER_SOURCE_LAST_FAILURE_TIMES:
final long failureTime = proto.readLong(
SyncStatusProto.StatusInfo.PER_SOURCE_LAST_FAILURE_TIMES);
if (failureTimesCount == status.perSourceLastFailureTimes.length) {
Slog.w(TAG, "Attempted to read more per source last failure times "
+ "than expected; data might be corrupted.");
break;
}
status.perSourceLastFailureTimes[failureTimesCount] = failureTime;
failureTimesCount++;
break;
case ProtoInputStream.NO_MORE_FIELDS:
status.populateLastEventsInformation(lastEventInformation);
return status;
}
}
}
private Pair<Long, String> parseLastEventInfoLocked(ProtoInputStream proto) throws IOException {
long time = 0;
String message = null;
while (true) {
switch (proto.nextField()) {
case (int) SyncStatusProto.StatusInfo.LastEventInfo.LAST_EVENT_TIME:
time = proto.readLong(SyncStatusProto.StatusInfo.LastEventInfo.LAST_EVENT_TIME);
break;
case (int) SyncStatusProto.StatusInfo.LastEventInfo.LAST_EVENT:
message = proto.readString(SyncStatusProto.StatusInfo.LastEventInfo.LAST_EVENT);
break;
case ProtoInputStream.NO_MORE_FIELDS:
return message == null ? null : new Pair<>(time, message);
}
}
}
private void readSyncStatusStatsLocked(ProtoInputStream proto, SyncStatusInfo.Stats stats)
throws IOException {
while (true) {
switch (proto.nextField()) {
case (int) SyncStatusProto.StatusInfo.Stats.TOTAL_ELAPSED_TIME:
stats.totalElapsedTime = proto.readLong(
SyncStatusProto.StatusInfo.Stats.TOTAL_ELAPSED_TIME);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_SYNCS:
stats.numSyncs = proto.readInt(SyncStatusProto.StatusInfo.Stats.NUM_SYNCS);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_FAILURES:
stats.numFailures = proto.readInt(
SyncStatusProto.StatusInfo.Stats.NUM_FAILURES);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_CANCELS:
stats.numCancels = proto.readInt(SyncStatusProto.StatusInfo.Stats.NUM_CANCELS);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_OTHER:
stats.numSourceOther = proto.readInt(
SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_OTHER);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_LOCAL:
stats.numSourceLocal = proto.readInt(
SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_LOCAL);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_POLL:
stats.numSourcePoll = proto.readInt(
SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_POLL);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_USER:
stats.numSourceUser = proto.readInt(
SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_USER);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_PERIODIC:
stats.numSourcePeriodic = proto.readInt(
SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_PERIODIC);
break;
case (int) SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_FEED:
stats.numSourceFeed = proto.readInt(
SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_FEED);
break;
case ProtoInputStream.NO_MORE_FIELDS:
return;
}
}
}
/**
* Write all sync status to the sync status file.
*/
private void writeStatusLocked() {
@VisibleForTesting
void writeStatusLocked() {
if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
Slog.v(TAG_FILE, "Writing new " + mStatusFile.getBaseFile());
}
@@ -2070,26 +2315,87 @@ public class SyncStorageEngine {
FileOutputStream fos = null;
try {
fos = mStatusFile.startWrite();
Parcel out = Parcel.obtain();
final int N = mSyncStatus.size();
for (int i=0; i<N; i++) {
SyncStatusInfo status = mSyncStatus.valueAt(i);
out.writeInt(STATUS_FILE_ITEM);
status.writeToParcel(out, 0);
}
out.writeInt(STATUS_FILE_END);
fos.write(out.marshall());
out.recycle();
writeStatusInfoLocked(fos);
mStatusFile.finishWrite(fos);
} catch (java.io.IOException e1) {
Slog.w(TAG, "Error writing status", e1);
if (fos != null) {
mStatusFile.failWrite(fos);
}
fos = null;
} catch (IOException | IllegalArgumentException e) {
Slog.e(TAG, "Unable to write sync status to proto.", e);
} finally {
// when fos is null (successful write), this is a no-op.
mStatusFile.failWrite(fos);
}
}
private void writeStatusInfoLocked(OutputStream out) {
final ProtoOutputStream proto = new ProtoOutputStream(out);
final int size = mSyncStatus.size();
for (int i = 0; i < size; i++) {
final SyncStatusInfo info = mSyncStatus.valueAt(i);
final long token = proto.start(SyncStatusProto.STATUS);
// authority id should be written first to take advantage of the fast path in read
proto.write(SyncStatusProto.StatusInfo.AUTHORITY_ID, info.authorityId);
proto.write(SyncStatusProto.StatusInfo.LAST_SUCCESS_TIME, info.lastSuccessTime);
proto.write(SyncStatusProto.StatusInfo.LAST_SUCCESS_SOURCE, info.lastSuccessSource);
proto.write(SyncStatusProto.StatusInfo.LAST_FAILURE_TIME, info.lastFailureTime);
proto.write(SyncStatusProto.StatusInfo.LAST_FAILURE_SOURCE, info.lastFailureSource);
proto.write(SyncStatusProto.StatusInfo.LAST_FAILURE_MESSAGE, info.lastFailureMesg);
proto.write(SyncStatusProto.StatusInfo.INITIAL_FAILURE_TIME, info.initialFailureTime);
proto.write(SyncStatusProto.StatusInfo.PENDING, info.pending);
proto.write(SyncStatusProto.StatusInfo.INITIALIZE, info.initialize);
final int periodicSyncTimesSize = info.getPeriodicSyncTimesSize();
for (int j = 0; j < periodicSyncTimesSize; j++) {
proto.write(SyncStatusProto.StatusInfo.PERIODIC_SYNC_TIMES,
info.getPeriodicSyncTime(j));
}
final int lastEventsSize = info.getEventCount();
for (int j = 0; j < lastEventsSize; j++) {
final long eventToken = proto.start(SyncStatusProto.StatusInfo.LAST_EVENT_INFO);
proto.write(SyncStatusProto.StatusInfo.LastEventInfo.LAST_EVENT_TIME,
info.getEventTime(j));
proto.write(SyncStatusProto.StatusInfo.LastEventInfo.LAST_EVENT, info.getEvent(j));
proto.end(eventToken);
}
proto.write(SyncStatusProto.StatusInfo.LAST_TODAY_RESET_TIME, info.lastTodayResetTime);
final long totalStatsToken = proto.start(SyncStatusProto.StatusInfo.TOTAL_STATS);
writeStatusStatsLocked(proto, info.totalStats);
proto.end(totalStatsToken);
final long todayStatsToken = proto.start(SyncStatusProto.StatusInfo.TODAY_STATS);
writeStatusStatsLocked(proto, info.todayStats);
proto.end(todayStatsToken);
final long yesterdayStatsToken = proto.start(
SyncStatusProto.StatusInfo.YESTERDAY_STATS);
writeStatusStatsLocked(proto, info.yesterdayStats);
proto.end(yesterdayStatsToken);
final int lastSuccessTimesSize = info.perSourceLastSuccessTimes.length;
for (int j = 0; j < lastSuccessTimesSize; j++) {
proto.write(SyncStatusProto.StatusInfo.PER_SOURCE_LAST_SUCCESS_TIMES,
info.perSourceLastSuccessTimes[j]);
}
final int lastFailureTimesSize = info.perSourceLastFailureTimes.length;
for (int j = 0; j < lastFailureTimesSize; j++) {
proto.write(SyncStatusProto.StatusInfo.PER_SOURCE_LAST_FAILURE_TIMES,
info.perSourceLastFailureTimes[j]);
}
proto.end(token);
}
proto.flush();
}
private void writeStatusStatsLocked(ProtoOutputStream proto, SyncStatusInfo.Stats stats) {
proto.write(SyncStatusProto.StatusInfo.Stats.TOTAL_ELAPSED_TIME, stats.totalElapsedTime);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_SYNCS, stats.numSyncs);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_FAILURES, stats.numFailures);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_CANCELS, stats.numCancels);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_OTHER, stats.numSourceOther);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_LOCAL, stats.numSourceLocal);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_POLL, stats.numSourcePoll);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_USER, stats.numSourceUser);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_PERIODIC, stats.numSourcePeriodic);
proto.write(SyncStatusProto.StatusInfo.Stats.NUM_SOURCE_FEED, stats.numSourceFeed);
}
private void requestSync(AuthorityInfo authorityInfo, int reason, Bundle extras,
@SyncExemption int syncExemptionFlag, int callingUid, int callingPid) {
if (android.os.Process.myUid() == android.os.Process.SYSTEM_UID
@@ -2126,20 +2432,17 @@ public class SyncStorageEngine {
public static final int STATISTICS_FILE_ITEM_OLD = 100;
public static final int STATISTICS_FILE_ITEM = 101;
/**
* Read all sync statistics back in to the initial engine state.
*/
private void readStatisticsLocked() {
private void readStatsParcelLocked(File parcel) {
try {
byte[] data = mStatisticsFile.readFully();
final AtomicFile parcelFile = new AtomicFile(parcel);
byte[] data = parcelFile.readFully();
Parcel in = Parcel.obtain();
in.unmarshall(data, 0, data.length);
in.setDataPosition(0);
int token;
int index = 0;
while ((token=in.readInt()) != STATISTICS_FILE_END) {
if (token == STATISTICS_FILE_ITEM
|| token == STATISTICS_FILE_ITEM_OLD) {
if (token == STATISTICS_FILE_ITEM || token == STATISTICS_FILE_ITEM_OLD) {
int day = in.readInt();
if (token == STATISTICS_FILE_ITEM_OLD) {
day = day - 2009 + 14245; // Magic!
@@ -2159,15 +2462,110 @@ public class SyncStorageEngine {
break;
}
}
} catch (java.io.IOException e) {
} catch (IOException e) {
Slog.i(TAG, "No initial statistics");
}
}
private void upgradeStatisticsIfNeededLocked() {
final File parcelStats = new File(mSyncDir, LEGACY_STATISTICS_FILE_NAME);
if (parcelStats.exists() && !mStatisticsFile.exists()) {
readStatsParcelLocked(parcelStats);
writeStatisticsLocked();
}
// if upgrade to proto was successful, delete parcel file
if (DELETE_LEGACY_PARCEL_FILES && mStatisticsFile.exists()) {
parcelStats.delete();
}
}
/**
* Read all sync statistics back in to the initial engine state.
*/
private void readStatisticsLocked() {
upgradeStatisticsIfNeededLocked();
if (!mStatisticsFile.exists()) {
return;
}
try {
try (FileInputStream in = mStatisticsFile.openRead()) {
readDayStatsLocked(in);
}
} catch (IOException e) {
Slog.e(TAG, "Unable to read day stats file.", e);
}
}
private void readDayStatsLocked(InputStream in) throws IOException {
final ProtoInputStream proto = new ProtoInputStream(in);
int statsCount = 0;
while (true) {
switch (proto.nextField()) {
case (int) SyncStatisticsProto.STATS:
final long token = proto.start(SyncStatisticsProto.STATS);
final DayStats stats = readIndividualDayStatsLocked(proto);
proto.end(token);
mDayStats[statsCount] = stats;
statsCount++;
if (statsCount == mDayStats.length) {
return;
}
break;
case ProtoInputStream.NO_MORE_FIELDS:
return;
}
}
}
private DayStats readIndividualDayStatsLocked(ProtoInputStream proto) throws IOException {
DayStats stats;
if (proto.nextField(SyncStatisticsProto.DayStats.DAY)) {
// fast-path; this should work for most cases since the day is written first
stats = new DayStats(proto.readInt(SyncStatisticsProto.DayStats.DAY));
} else {
// placeholder to read other data; assume the default day as 0
stats = new DayStats(0);
}
while (true) {
switch (proto.nextField()) {
case (int) SyncStatisticsProto.DayStats.DAY:
// fast-path failed for some reason, rebuild stats from placeholder object
Slog.w(TAG, "Failed to read the day via fast-path; some data "
+ "might not have been read.");
final DayStats temp = new DayStats(
proto.readInt(SyncStatisticsProto.DayStats.DAY));
temp.successCount = stats.successCount;
temp.successTime = stats.successTime;
temp.failureCount = stats.failureCount;
temp.failureTime = stats.failureTime;
stats = temp;
break;
case (int) SyncStatisticsProto.DayStats.SUCCESS_COUNT:
stats.successCount = proto.readInt(SyncStatisticsProto.DayStats.SUCCESS_COUNT);
break;
case (int) SyncStatisticsProto.DayStats.SUCCESS_TIME:
stats.successTime = proto.readLong(SyncStatisticsProto.DayStats.SUCCESS_TIME);
break;
case (int) SyncStatisticsProto.DayStats.FAILURE_COUNT:
stats.failureCount = proto.readInt(SyncStatisticsProto.DayStats.FAILURE_COUNT);
break;
case (int) SyncStatisticsProto.DayStats.FAILURE_TIME:
stats.failureTime = proto.readLong(SyncStatisticsProto.DayStats.FAILURE_TIME);
break;
case ProtoInputStream.NO_MORE_FIELDS:
return stats;
}
}
}
/**
* Write all sync statistics to the sync status file.
*/
private void writeStatisticsLocked() {
@VisibleForTesting
void writeStatisticsLocked() {
if (Log.isLoggable(TAG_FILE, Log.VERBOSE)) {
Slog.v(TAG, "Writing new " + mStatisticsFile.getBaseFile());
}
@@ -2179,33 +2577,38 @@ public class SyncStorageEngine {
FileOutputStream fos = null;
try {
fos = mStatisticsFile.startWrite();
Parcel out = Parcel.obtain();
final int N = mDayStats.length;
for (int i=0; i<N; i++) {
DayStats ds = mDayStats[i];
if (ds == null) {
break;
}
out.writeInt(STATISTICS_FILE_ITEM);
out.writeInt(ds.day);
out.writeInt(ds.successCount);
out.writeLong(ds.successTime);
out.writeInt(ds.failureCount);
out.writeLong(ds.failureTime);
}
out.writeInt(STATISTICS_FILE_END);
fos.write(out.marshall());
out.recycle();
writeDayStatsLocked(fos);
mStatisticsFile.finishWrite(fos);
} catch (java.io.IOException e1) {
Slog.w(TAG, "Error writing stats", e1);
if (fos != null) {
mStatisticsFile.failWrite(fos);
}
fos = null;
} catch (IOException | IllegalArgumentException e) {
Slog.e(TAG, "Unable to write day stats to proto.", e);
} finally {
// when fos is null (successful write), this is a no-op.
mStatisticsFile.failWrite(fos);
}
}
private void writeDayStatsLocked(OutputStream out)
throws IOException, IllegalArgumentException {
final ProtoOutputStream proto = new ProtoOutputStream(out);
final int size = mDayStats.length;
for (int i = 0; i < size; i++) {
final DayStats stats = mDayStats[i];
if (stats == null) {
break;
}
final long token = proto.start(SyncStatisticsProto.STATS);
// day should be written first to take advantage of the fast path in read
proto.write(SyncStatisticsProto.DayStats.DAY, stats.day);
proto.write(SyncStatisticsProto.DayStats.SUCCESS_COUNT, stats.successCount);
proto.write(SyncStatisticsProto.DayStats.SUCCESS_TIME, stats.successTime);
proto.write(SyncStatisticsProto.DayStats.FAILURE_COUNT, stats.failureCount);
proto.write(SyncStatisticsProto.DayStats.FAILURE_TIME, stats.failureTime);
proto.end(token);
}
proto.flush();
}
/**
* Let the BackupManager know that account sync settings have changed. This will trigger
* {@link com.android.server.backup.SystemBackupAgent} to run.

View File

@@ -0,0 +1,200 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* 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.android.server.content;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import android.content.Context;
import android.content.SyncStatusInfo;
import android.util.Pair;
import android.util.SparseArray;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Random;
/**
* Tests for {@link SyncStorageEngine}.
*/
@RunWith(AndroidJUnit4.class)
public class SyncStorageEngineTest {
private Context mContext;
private SyncStorageEngine mSyncStorageEngine;
private static final int NUM_SYNC_STATUS = 100;
private static final int NUM_PERIODIC_SYNC_TIMES = 20;
private static final int NUM_EVENTS = 10;
private static final int NUM_SOURCES = 6;
@Before
public void setUp() {
mContext = InstrumentationRegistry.getTargetContext();
mSyncStorageEngine = SyncStorageEngine.newTestInstance(mContext);
}
@Test
public void testStatisticsReadWrite() {
populateDayStats(mSyncStorageEngine.mDayStats);
mSyncStorageEngine.writeStatisticsLocked();
final SyncStorageEngine other = SyncStorageEngine.newTestInstance(mContext);
verifyDayStats(mSyncStorageEngine.mDayStats, other.getDayStatistics());
}
@Test
public void testStatusReadWrite() {
populateStatus(mSyncStorageEngine.mSyncStatus);
mSyncStorageEngine.writeStatusLocked();
final SyncStorageEngine other = SyncStorageEngine.newTestInstance(mContext);
for (int i = 0; i < NUM_SYNC_STATUS; i++) {
other.mAuthorities.put(i, null);
}
other.readStatusLocked();
verifyStatus(mSyncStorageEngine.mSyncStatus, other.mSyncStatus);
}
private void populateDayStats(SyncStorageEngine.DayStats[] dayStats) {
final Random r = new Random(1);
for (int i = 0; i < dayStats.length; i++) {
final SyncStorageEngine.DayStats ds = new SyncStorageEngine.DayStats(i);
ds.successCount = r.nextInt();
ds.successTime = r.nextLong();
ds.failureCount = r.nextInt();
ds.failureTime = r.nextLong();
dayStats[i] = ds;
}
}
private void verifyDayStats(SyncStorageEngine.DayStats[] dayStats,
SyncStorageEngine.DayStats[] dayStatsOther) {
assertEquals(dayStatsOther.length, dayStats.length);
for (int i = 0; i < dayStatsOther.length; i++) {
final SyncStorageEngine.DayStats ds = dayStats[i];
final SyncStorageEngine.DayStats dsOther = dayStatsOther[i];
assertEquals(dsOther.day, ds.day);
assertEquals(dsOther.successCount, ds.successCount);
assertEquals(dsOther.successTime, ds.successTime);
assertEquals(dsOther.failureCount, ds.failureCount);
assertEquals(dsOther.failureTime, ds.failureTime);
}
}
private void populateStatus(SparseArray<SyncStatusInfo> syncStatus) {
final Random r = new Random(1);
for (int i = 0; i < NUM_SYNC_STATUS; i++) {
final SyncStatusInfo ss = new SyncStatusInfo(i);
ss.lastSuccessTime = r.nextLong();
ss.lastSuccessSource = r.nextInt();
ss.lastFailureTime = r.nextLong();
ss.lastFailureSource = r.nextInt();
ss.lastFailureMesg = "fail_msg_" + r.nextInt();
ss.initialFailureTime = r.nextLong();
ss.initialize = r.nextBoolean();
for (int j = 0; j < NUM_PERIODIC_SYNC_TIMES; j++) {
ss.addPeriodicSyncTime(r.nextLong());
}
final ArrayList<Pair<Long, String>> lastEventInfos = new ArrayList<>();
for (int j = 0; j < NUM_EVENTS; j++) {
lastEventInfos.add(new Pair<>(r.nextLong(), "event_" + r.nextInt()));
}
ss.populateLastEventsInformation(lastEventInfos);
ss.lastTodayResetTime = r.nextLong();
populateStats(ss.totalStats, r);
populateStats(ss.todayStats, r);
populateStats(ss.yesterdayStats, r);
for (int j = 0; j < NUM_SOURCES; j++) {
ss.perSourceLastSuccessTimes[j] = r.nextLong();
}
for (int j = 0; j < NUM_SOURCES; j++) {
ss.perSourceLastFailureTimes[j] = r.nextLong();
}
syncStatus.put(i, ss);
}
}
private void populateStats(SyncStatusInfo.Stats stats, Random r) {
stats.totalElapsedTime = r.nextLong();
stats.numSyncs = r.nextInt();
stats.numFailures = r.nextInt();
stats.numCancels = r.nextInt();
stats.numSourceOther = r.nextInt();
stats.numSourceLocal = r.nextInt();
stats.numSourcePoll = r.nextInt();
stats.numSourceUser = r.nextInt();
stats.numSourcePeriodic = r.nextInt();
stats.numSourceFeed = r.nextInt();
}
private void verifyStatus(SparseArray<SyncStatusInfo> syncStatus,
SparseArray<SyncStatusInfo> syncStatusOther) {
assertEquals(syncStatusOther.size(), syncStatus.size());
for (int i = 0; i < NUM_SYNC_STATUS; i++) {
final SyncStatusInfo ss = syncStatus.valueAt(i);
final SyncStatusInfo ssOther = syncStatusOther.valueAt(i);
assertEquals(ssOther.authorityId, ss.authorityId);
assertEquals(ssOther.lastSuccessTime, ss.lastSuccessTime);
assertEquals(ssOther.lastSuccessSource, ss.lastSuccessSource);
assertEquals(ssOther.lastFailureTime, ss.lastFailureTime);
assertEquals(ssOther.lastFailureSource, ss.lastFailureSource);
assertEquals(ssOther.lastFailureMesg, ss.lastFailureMesg);
assertFalse(ssOther.pending); // pending is always set to false when read
assertEquals(ssOther.initialize, ss.initialize);
assertEquals(ssOther.getPeriodicSyncTimesSize(), NUM_PERIODIC_SYNC_TIMES);
for (int j = 0; j < NUM_PERIODIC_SYNC_TIMES; j++) {
assertEquals(ssOther.getPeriodicSyncTime(j), ss.getPeriodicSyncTime(j));
}
assertEquals(ssOther.getEventCount(), NUM_EVENTS);
for (int j = 0; j < NUM_EVENTS; j++) {
assertEquals(ssOther.getEventTime(j), ss.getEventTime(j));
assertEquals(ssOther.getEvent(j), ss.getEvent(j));
}
assertEquals(ssOther.lastTodayResetTime, ss.lastTodayResetTime);
verifyStats(ss.totalStats, ssOther.totalStats);
verifyStats(ss.todayStats, ssOther.todayStats);
verifyStats(ss.yesterdayStats, ssOther.yesterdayStats);
assertEquals(ssOther.perSourceLastSuccessTimes.length, NUM_SOURCES);
for (int j = 0; j < NUM_SOURCES; j++) {
assertEquals(ssOther.perSourceLastSuccessTimes[j], ss.perSourceLastSuccessTimes[j]);
}
assertEquals(ssOther.perSourceLastFailureTimes.length, NUM_SOURCES);
for (int j = 0; j < NUM_SOURCES; j++) {
assertEquals(ssOther.perSourceLastFailureTimes[j], ss.perSourceLastFailureTimes[j]);
}
}
}
private void verifyStats(SyncStatusInfo.Stats stats, SyncStatusInfo.Stats statsOther) {
assertEquals(statsOther.totalElapsedTime, stats.totalElapsedTime);
assertEquals(statsOther.numSyncs, stats.numSyncs);
assertEquals(statsOther.numFailures, stats.numFailures);
assertEquals(statsOther.numCancels, stats.numCancels);
assertEquals(statsOther.numSourceOther, stats.numSourceOther);
assertEquals(statsOther.numSourceLocal, stats.numSourceLocal);
assertEquals(statsOther.numSourcePoll, stats.numSourcePoll);
assertEquals(statsOther.numSourceUser, stats.numSourceUser);
assertEquals(statsOther.numSourcePeriodic, stats.numSourcePeriodic);
assertEquals(statsOther.numSourceFeed, stats.numSourceFeed);
}
}