[MS82.1] Support network stats data migration process
This includes:
1. Provide NetworkStatsRecorder#importCollectionLocked to
allow caller to write a NetworkStatsCollection object to
a standalone file.
2. Do not clear the collection in NetworkStatsRecorder#
CombiningRewriter, this is safe since pending set will
still be cleared during forcePersistLocked.
3. Implement the migration process, including recovery and
safety measures.
Test: NetworkStatsServiceTest NetworkStatsCollectionTest
Test: manual test with
adb shell device_config put tethering \
netstats_store_files_in_apexdata true
adb shell device_config put tethering \
netstats_import_legacy_target_attempts 1
Ignore-AOSP-First: in a topic with internal-only changes
Bug: 230289468
Change-Id: Ic2002cbfd5a6b3c2226fa8dab22481f8ce656574
This commit is contained in:
@@ -21,6 +21,7 @@ import static android.net.TrafficStats.KB_IN_BYTES;
|
||||
import static android.net.TrafficStats.MB_IN_BYTES;
|
||||
import static android.text.format.DateUtils.YEAR_IN_MILLIS;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.net.NetworkIdentitySet;
|
||||
import android.net.NetworkStats;
|
||||
import android.net.NetworkStats.NonMonotonicObserver;
|
||||
@@ -68,7 +69,7 @@ public class NetworkStatsRecorder {
|
||||
|
||||
private static final String TAG_NETSTATS_DUMP = "netstats_dump";
|
||||
|
||||
/** Dump before deleting in {@link #recoverFromWtf()}. */
|
||||
/** Dump before deleting in {@link #recoverAndDeleteData()}. */
|
||||
private static final boolean DUMP_BEFORE_DELETE = true;
|
||||
|
||||
private final FileRotator mRotator;
|
||||
@@ -156,6 +157,15 @@ public class NetworkStatsRecorder {
|
||||
return mSinceBoot;
|
||||
}
|
||||
|
||||
public long getBucketDuration() {
|
||||
return mBucketDuration;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getCookie() {
|
||||
return mCookie;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load complete history represented by {@link FileRotator}. Caches
|
||||
* internally as a {@link WeakReference}, and updated with future
|
||||
@@ -189,10 +199,10 @@ public class NetworkStatsRecorder {
|
||||
res.recordCollection(mPending);
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "problem completely reading network stats", e);
|
||||
recoverFromWtf();
|
||||
recoverAndDeleteData();
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.wtf(TAG, "problem completely reading network stats", e);
|
||||
recoverFromWtf();
|
||||
recoverAndDeleteData();
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -300,10 +310,10 @@ public class NetworkStatsRecorder {
|
||||
mPending.reset();
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "problem persisting pending stats", e);
|
||||
recoverFromWtf();
|
||||
recoverAndDeleteData();
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.wtf(TAG, "problem persisting pending stats", e);
|
||||
recoverFromWtf();
|
||||
recoverAndDeleteData();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -319,10 +329,10 @@ public class NetworkStatsRecorder {
|
||||
mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
|
||||
recoverFromWtf();
|
||||
recoverAndDeleteData();
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
|
||||
recoverFromWtf();
|
||||
recoverAndDeleteData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -347,8 +357,7 @@ public class NetworkStatsRecorder {
|
||||
|
||||
/**
|
||||
* Rewriter that will combine current {@link NetworkStatsCollection} values
|
||||
* with anything read from disk, and write combined set to disk. Clears the
|
||||
* original {@link NetworkStatsCollection} when finished writing.
|
||||
* with anything read from disk, and write combined set to disk.
|
||||
*/
|
||||
private static class CombiningRewriter implements FileRotator.Rewriter {
|
||||
private final NetworkStatsCollection mCollection;
|
||||
@@ -375,7 +384,6 @@ public class NetworkStatsRecorder {
|
||||
@Override
|
||||
public void write(OutputStream out) throws IOException {
|
||||
mCollection.write(out);
|
||||
mCollection.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,6 +463,23 @@ public class NetworkStatsRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a specified {@link NetworkStatsCollection} instance into this recorder,
|
||||
* and write it into a standalone file.
|
||||
* @param collection The target {@link NetworkStatsCollection} instance to be imported.
|
||||
*/
|
||||
public void importCollectionLocked(@NonNull NetworkStatsCollection collection)
|
||||
throws IOException {
|
||||
if (mRotator != null) {
|
||||
mRotator.rewriteSingle(new CombiningRewriter(collection), collection.getStartMillis(),
|
||||
collection.getEndMillis());
|
||||
}
|
||||
|
||||
if (mComplete != null) {
|
||||
throw new IllegalStateException("cannot import data when data already loaded");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewriter that will remove any histories or persisted data points before the
|
||||
* specified cutoff time, only writing data back when modified.
|
||||
@@ -501,10 +526,10 @@ public class NetworkStatsRecorder {
|
||||
mBucketDuration, cutoffMillis));
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "problem importing netstats", e);
|
||||
recoverFromWtf();
|
||||
recoverAndDeleteData();
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.wtf(TAG, "problem importing netstats", e);
|
||||
recoverFromWtf();
|
||||
recoverAndDeleteData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,7 +580,7 @@ public class NetworkStatsRecorder {
|
||||
* Recover from {@link FileRotator} failure by dumping state to
|
||||
* {@link DropBoxManager} and deleting contents.
|
||||
*/
|
||||
private void recoverFromWtf() {
|
||||
void recoverAndDeleteData() {
|
||||
if (DUMP_BEFORE_DELETE) {
|
||||
final ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
|
||||
@@ -67,6 +67,7 @@ import android.annotation.TargetApi;
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.usage.NetworkStatsManager;
|
||||
import android.content.ApexEnvironment;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
@@ -100,6 +101,7 @@ import android.net.TrafficStats;
|
||||
import android.net.UnderlyingNetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.net.netstats.IUsageCallback;
|
||||
import android.net.netstats.NetworkStatsDataMigrationUtils;
|
||||
import android.net.netstats.provider.INetworkStatsProvider;
|
||||
import android.net.netstats.provider.INetworkStatsProviderCallback;
|
||||
import android.net.netstats.provider.NetworkStatsProvider;
|
||||
@@ -118,6 +120,7 @@ import android.os.ServiceSpecificException;
|
||||
import android.os.SystemClock;
|
||||
import android.os.Trace;
|
||||
import android.os.UserHandle;
|
||||
import android.provider.DeviceConfig;
|
||||
import android.provider.Settings;
|
||||
import android.provider.Settings.Global;
|
||||
import android.service.NetworkInterfaceProto;
|
||||
@@ -143,6 +146,7 @@ import com.android.net.module.util.BestClock;
|
||||
import com.android.net.module.util.BinderUtils;
|
||||
import com.android.net.module.util.BpfMap;
|
||||
import com.android.net.module.util.CollectionUtils;
|
||||
import com.android.net.module.util.DeviceConfigUtils;
|
||||
import com.android.net.module.util.IBpfMap;
|
||||
import com.android.net.module.util.LocationPermissionChecker;
|
||||
import com.android.net.module.util.NetworkStatsUtils;
|
||||
@@ -155,7 +159,9 @@ import java.io.FileDescriptor;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -232,6 +238,22 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
private static final String STATS_MAP_B_PATH =
|
||||
"/sys/fs/bpf/net_shared/map_netd_stats_map_B";
|
||||
|
||||
/**
|
||||
* DeviceConfig flag used to indicate whether the files should be stored in the apex data
|
||||
* directory.
|
||||
*/
|
||||
static final String NETSTATS_STORE_FILES_IN_APEXDATA = "netstats_store_files_in_apexdata";
|
||||
/**
|
||||
* DeviceConfig flag is used to indicate whether the legacy files need to be imported, and
|
||||
* retry count before giving up. Only valid when {@link #NETSTATS_STORE_FILES_IN_APEXDATA}
|
||||
* set to true. Note that the value gets rollback when the mainline module gets rollback.
|
||||
*/
|
||||
static final String NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS =
|
||||
"netstats_import_legacy_target_attempts";
|
||||
static final int DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS = 1;
|
||||
static final String NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME = "import.attempts";
|
||||
static final String NETSTATS_IMPORT_SUCCESS_COUNTER_NAME = "import.successes";
|
||||
|
||||
private final Context mContext;
|
||||
private final NetworkStatsFactory mStatsFactory;
|
||||
private final AlarmManager mAlarmManager;
|
||||
@@ -239,8 +261,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
private final NetworkStatsSettings mSettings;
|
||||
private final NetworkStatsObservers mStatsObservers;
|
||||
|
||||
private final File mSystemDir;
|
||||
private final File mBaseDir;
|
||||
private final File mStatsDir;
|
||||
|
||||
private final PowerManager.WakeLock mWakeLock;
|
||||
|
||||
@@ -250,6 +271,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
protected INetd mNetd;
|
||||
private final AlertObserver mAlertObserver = new AlertObserver();
|
||||
|
||||
// Persistent counters that backed by AtomicFile which stored in the data directory as a file,
|
||||
// to track attempts/successes count across reboot. Note that these counter values will be
|
||||
// rollback as the module rollbacks.
|
||||
private PersistentInt mImportLegacyAttemptsCounter = null;
|
||||
private PersistentInt mImportLegacySuccessesCounter = null;
|
||||
|
||||
@VisibleForTesting
|
||||
public static final String ACTION_NETWORK_STATS_POLL =
|
||||
"com.android.server.action.NETWORK_STATS_POLL";
|
||||
@@ -405,16 +432,6 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
@NonNull
|
||||
private final BpfInterfaceMapUpdater mInterfaceMapUpdater;
|
||||
|
||||
private static @NonNull File getDefaultSystemDir() {
|
||||
return new File(Environment.getDataDirectory(), "system");
|
||||
}
|
||||
|
||||
private static @NonNull File getDefaultBaseDir() {
|
||||
File baseDir = new File(getDefaultSystemDir(), "netstats");
|
||||
baseDir.mkdirs();
|
||||
return baseDir;
|
||||
}
|
||||
|
||||
private static @NonNull Clock getDefaultClock() {
|
||||
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
|
||||
Clock.systemUTC());
|
||||
@@ -506,8 +523,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
|
||||
alarmManager, wakeLock, getDefaultClock(),
|
||||
new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
|
||||
new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(),
|
||||
new Dependencies());
|
||||
new NetworkStatsObservers(), new Dependencies());
|
||||
|
||||
return service;
|
||||
}
|
||||
@@ -517,8 +533,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
@VisibleForTesting
|
||||
NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
|
||||
PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
|
||||
NetworkStatsFactory factory, NetworkStatsObservers statsObservers, File systemDir,
|
||||
File baseDir, @NonNull Dependencies deps) {
|
||||
NetworkStatsFactory factory, NetworkStatsObservers statsObservers,
|
||||
@NonNull Dependencies deps) {
|
||||
mContext = Objects.requireNonNull(context, "missing Context");
|
||||
mNetd = Objects.requireNonNull(netd, "missing Netd");
|
||||
mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
|
||||
@@ -527,9 +543,11 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
|
||||
mStatsFactory = Objects.requireNonNull(factory, "missing factory");
|
||||
mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
|
||||
mSystemDir = Objects.requireNonNull(systemDir, "missing systemDir");
|
||||
mBaseDir = Objects.requireNonNull(baseDir, "missing baseDir");
|
||||
mDeps = Objects.requireNonNull(deps, "missing Dependencies");
|
||||
mStatsDir = mDeps.getOrCreateStatsDir();
|
||||
if (!mStatsDir.exists()) {
|
||||
throw new IllegalStateException("Persist data directory does not exist: " + mStatsDir);
|
||||
}
|
||||
|
||||
final HandlerThread handlerThread = mDeps.makeHandlerThread();
|
||||
handlerThread.start();
|
||||
@@ -555,6 +573,79 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
// TODO: Move more stuff into dependencies object.
|
||||
@VisibleForTesting
|
||||
public static class Dependencies {
|
||||
/**
|
||||
* Get or create the directory that stores the persisted data usage.
|
||||
*/
|
||||
@NonNull
|
||||
public File getOrCreateStatsDir() {
|
||||
final boolean storeInApexDataDir = getStoreFilesInApexData();
|
||||
|
||||
final File statsDataDir;
|
||||
if (storeInApexDataDir) {
|
||||
final File apexDataDir = ApexEnvironment
|
||||
.getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
|
||||
.getDeviceProtectedDataDir();
|
||||
statsDataDir = new File(apexDataDir, "netstats");
|
||||
|
||||
} else {
|
||||
final File systemDataDir = new File(Environment.getDataDirectory(), "system");
|
||||
statsDataDir = new File(systemDataDir, "netstats");
|
||||
}
|
||||
|
||||
if (statsDataDir.exists() || statsDataDir.mkdirs()) {
|
||||
return statsDataDir;
|
||||
}
|
||||
throw new IllegalStateException("Cannot write into stats data directory: "
|
||||
+ statsDataDir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the count of import legacy target attempts.
|
||||
*/
|
||||
public int getImportLegacyTargetAttempts() {
|
||||
return DeviceConfigUtils.getDeviceConfigPropertyInt(
|
||||
DeviceConfig.NAMESPACE_TETHERING,
|
||||
NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS,
|
||||
DEFAULT_NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the persistent counter that counts total import legacy stats attempts.
|
||||
*/
|
||||
public PersistentInt createImportLegacyAttemptsCounter(@NonNull Path path)
|
||||
throws IOException {
|
||||
// TODO: Modify PersistentInt to call setStartTime every time a write is made.
|
||||
// Create and pass a real logger here.
|
||||
return new PersistentInt(path.toString(), null /* logger */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the persistent counter that counts total import legacy stats successes.
|
||||
*/
|
||||
public PersistentInt createImportLegacySuccessesCounter(@NonNull Path path)
|
||||
throws IOException {
|
||||
return new PersistentInt(path.toString(), null /* logger */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the flag of storing files in the apex data directory.
|
||||
* @return whether to store files in the apex data directory.
|
||||
*/
|
||||
public boolean getStoreFilesInApexData() {
|
||||
return DeviceConfigUtils.getDeviceConfigPropertyBoolean(
|
||||
DeviceConfig.NAMESPACE_TETHERING,
|
||||
NETSTATS_STORE_FILES_IN_APEXDATA, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read legacy persisted network stats from disk.
|
||||
*/
|
||||
@NonNull
|
||||
public NetworkStatsCollection readPlatformCollection(
|
||||
@NonNull String prefix, long bucketDuration) throws IOException {
|
||||
return NetworkStatsDataMigrationUtils.readPlatformCollection(prefix, bucketDuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a HandlerThread to use in NetworkStatsService.
|
||||
*/
|
||||
@@ -697,7 +788,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
|
||||
updatePersistThresholdsLocked();
|
||||
|
||||
// upgrade any legacy stats, migrating them to rotated files
|
||||
// upgrade any legacy stats
|
||||
maybeUpgradeLegacyStatsLocked();
|
||||
|
||||
// read historical network stats from disk, since policy service
|
||||
@@ -761,7 +852,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
|
||||
Context.DROPBOX_SERVICE);
|
||||
return new NetworkStatsRecorder(new FileRotator(
|
||||
mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
|
||||
mStatsDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
|
||||
mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
|
||||
}
|
||||
|
||||
@@ -791,32 +882,151 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
mSystemReady = false;
|
||||
}
|
||||
|
||||
private static class MigrationInfo {
|
||||
public final NetworkStatsRecorder recorder;
|
||||
public NetworkStatsCollection collection;
|
||||
public boolean imported;
|
||||
MigrationInfo(@NonNull final NetworkStatsRecorder recorder) {
|
||||
this.recorder = recorder;
|
||||
collection = null;
|
||||
imported = false;
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mStatsLock")
|
||||
private void maybeUpgradeLegacyStatsLocked() {
|
||||
File file;
|
||||
try {
|
||||
file = new File(mSystemDir, "netstats.bin");
|
||||
if (file.exists()) {
|
||||
mDevRecorder.importLegacyNetworkLocked(file);
|
||||
file.delete();
|
||||
}
|
||||
|
||||
file = new File(mSystemDir, "netstats_xt.bin");
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
file = new File(mSystemDir, "netstats_uid.bin");
|
||||
if (file.exists()) {
|
||||
mUidRecorder.importLegacyUidLocked(file);
|
||||
mUidTagRecorder.importLegacyUidLocked(file);
|
||||
file.delete();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "problem during legacy upgrade", e);
|
||||
} catch (OutOfMemoryError e) {
|
||||
Log.wtf(TAG, "problem during legacy upgrade", e);
|
||||
final boolean storeFilesInApexData = mDeps.getStoreFilesInApexData();
|
||||
if (!storeFilesInApexData) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
mImportLegacyAttemptsCounter = mDeps.createImportLegacyAttemptsCounter(
|
||||
mStatsDir.toPath().resolve(NETSTATS_IMPORT_ATTEMPTS_COUNTER_NAME));
|
||||
mImportLegacySuccessesCounter = mDeps.createImportLegacySuccessesCounter(
|
||||
mStatsDir.toPath().resolve(NETSTATS_IMPORT_SUCCESS_COUNTER_NAME));
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "Failed to create persistent counters, skip.", e);
|
||||
return;
|
||||
}
|
||||
|
||||
final int targetAttempts = mDeps.getImportLegacyTargetAttempts();
|
||||
final int attempts;
|
||||
try {
|
||||
attempts = mImportLegacyAttemptsCounter.get();
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "Failed to read attempts counter, skip.", e);
|
||||
return;
|
||||
}
|
||||
if (attempts >= targetAttempts) return;
|
||||
|
||||
Log.i(TAG, "Starting import : attempts " + attempts + "/" + targetAttempts);
|
||||
|
||||
final List<MigrationInfo> migrations = List.of(
|
||||
new MigrationInfo(mDevRecorder), new MigrationInfo(mXtRecorder),
|
||||
new MigrationInfo(mUidRecorder), new MigrationInfo(mUidTagRecorder)
|
||||
);
|
||||
long migrationEndTime = Long.MIN_VALUE;
|
||||
try {
|
||||
// First, read all legacy collections. This is OEM code and it can throw. Don't
|
||||
// commit any data to disk until all are read.
|
||||
for (final MigrationInfo migration : migrations) {
|
||||
migration.collection = readPlatformCollectionForRecorder(migration.recorder);
|
||||
}
|
||||
|
||||
// Find the latest end time.
|
||||
for (final MigrationInfo migration : migrations) {
|
||||
final long migrationEnd = migration.collection.getEndMillis();
|
||||
if (migrationEnd > migrationEndTime) migrationEndTime = migrationEnd;
|
||||
}
|
||||
|
||||
// Reading all collections from legacy data has succeeded. At this point it is
|
||||
// safe to start overwriting the files on disk. The next step is to remove all
|
||||
// data in the new location that overlaps with imported data. This ensures that
|
||||
// any data in the new location that was created by a previous failed import is
|
||||
// ignored. After that, write the imported data into the recorder. The code
|
||||
// below can still possibly throw (disk error or OutOfMemory for example), but
|
||||
// does not depend on code from non-mainline code.
|
||||
Log.i(TAG, "Rewriting data with imported collections with cutoff "
|
||||
+ Instant.ofEpochMilli(migrationEndTime));
|
||||
for (final MigrationInfo migration : migrations) {
|
||||
migration.imported = true;
|
||||
migration.recorder.removeDataBefore(migrationEndTime);
|
||||
if (migration.collection.isEmpty()) continue;
|
||||
migration.recorder.importCollectionLocked(migration.collection);
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully imported platform collections");
|
||||
} catch (Throwable e) {
|
||||
// The code above calls OEM code that may behave differently across devices.
|
||||
// It can throw any exception including RuntimeExceptions and
|
||||
// OutOfMemoryErrors. Try to recover anyway.
|
||||
Log.wtf(TAG, "Platform data import failed. Remaining tries "
|
||||
+ (targetAttempts - attempts), e);
|
||||
|
||||
// Failed this time around : try again next time unless we're out of tries.
|
||||
try {
|
||||
mImportLegacyAttemptsCounter.set(attempts + 1);
|
||||
} catch (IOException ex) {
|
||||
Log.wtf(TAG, "Failed to update attempts counter.", ex);
|
||||
}
|
||||
|
||||
// Try to remove any data from the failed import.
|
||||
if (migrationEndTime > Long.MIN_VALUE) {
|
||||
try {
|
||||
for (final MigrationInfo migration : migrations) {
|
||||
if (migration.imported) {
|
||||
migration.recorder.removeDataBefore(migrationEndTime);
|
||||
}
|
||||
}
|
||||
} catch (Throwable f) {
|
||||
// If rollback still throws, there isn't much left to do. Try nuking
|
||||
// all data, since that's the last stop. If nuking still throws, the
|
||||
// framework will reboot, and if there are remaining tries, the migration
|
||||
// process will retry, which is fine because it's idempotent.
|
||||
for (final MigrationInfo migration : migrations) {
|
||||
migration.recorder.recoverAndDeleteData();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Success ! No need to import again next time.
|
||||
try {
|
||||
mImportLegacyAttemptsCounter.set(targetAttempts);
|
||||
// The successes counter is only for debugging. Hence, the synchronization
|
||||
// between these two counters are not very critical.
|
||||
final int successCount = mImportLegacySuccessesCounter.get();
|
||||
mImportLegacySuccessesCounter.set(successCount + 1);
|
||||
} catch (IOException e) {
|
||||
Log.wtf(TAG, "Succeed but failed to update counters.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@GuardedBy("mStatsLock")
|
||||
@NonNull
|
||||
private NetworkStatsCollection readPlatformCollectionForRecorder(
|
||||
@NonNull final NetworkStatsRecorder rec) throws IOException {
|
||||
final String prefix = rec.getCookie();
|
||||
Log.i(TAG, "Importing platform collection for prefix " + prefix);
|
||||
final NetworkStatsCollection collection = Objects.requireNonNull(
|
||||
mDeps.readPlatformCollection(prefix, rec.getBucketDuration()),
|
||||
"Imported platform collection for prefix " + prefix + " must not be null");
|
||||
|
||||
final long bootTimestamp = System.currentTimeMillis() - SystemClock.elapsedRealtime();
|
||||
if (!collection.isEmpty() && bootTimestamp < collection.getStartMillis()) {
|
||||
throw new IllegalArgumentException("Platform collection for prefix " + prefix
|
||||
+ " contains data that could not possibly come from the previous boot "
|
||||
+ "(start timestamp = " + Instant.ofEpochMilli(collection.getStartMillis())
|
||||
+ ", last booted at " + Instant.ofEpochMilli(bootTimestamp));
|
||||
}
|
||||
|
||||
Log.i(TAG, "Successfully read platform collection spanning from "
|
||||
// Instant uses ISO-8601 for toString()
|
||||
+ Instant.ofEpochMilli(collection.getStartMillis()).toString() + " to "
|
||||
+ Instant.ofEpochMilli(collection.getEndMillis()).toString());
|
||||
return collection;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2102,10 +2312,32 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
|
||||
return;
|
||||
}
|
||||
|
||||
pw.println("Directory:");
|
||||
pw.increaseIndent();
|
||||
pw.println(mStatsDir);
|
||||
pw.decreaseIndent();
|
||||
|
||||
pw.println("Configs:");
|
||||
pw.increaseIndent();
|
||||
pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
|
||||
pw.println();
|
||||
pw.print(NETSTATS_STORE_FILES_IN_APEXDATA, mDeps.getStoreFilesInApexData());
|
||||
pw.println();
|
||||
pw.print(NETSTATS_IMPORT_LEGACY_TARGET_ATTEMPTS, mDeps.getImportLegacyTargetAttempts());
|
||||
pw.println();
|
||||
if (mDeps.getStoreFilesInApexData()) {
|
||||
try {
|
||||
pw.print("platform legacy stats import attempts count",
|
||||
mImportLegacyAttemptsCounter.get());
|
||||
pw.println();
|
||||
pw.print("platform legacy stats import successes count",
|
||||
mImportLegacySuccessesCounter.get());
|
||||
pw.println();
|
||||
} catch (IOException e) {
|
||||
pw.println("(failed to dump platform legacy stats import counters)");
|
||||
}
|
||||
}
|
||||
|
||||
pw.decreaseIndent();
|
||||
|
||||
pw.println("Active interfaces:");
|
||||
|
||||
@@ -94,8 +94,10 @@ import android.net.INetworkStatsSession;
|
||||
import android.net.LinkProperties;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkIdentity;
|
||||
import android.net.NetworkStateSnapshot;
|
||||
import android.net.NetworkStats;
|
||||
import android.net.NetworkStatsCollection;
|
||||
import android.net.NetworkStatsHistory;
|
||||
import android.net.NetworkTemplate;
|
||||
import android.net.TelephonyNetworkSpecifier;
|
||||
@@ -143,9 +145,11 @@ import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Clock;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@@ -220,6 +224,12 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
||||
private ContentObserver mContentObserver;
|
||||
private Handler mHandler;
|
||||
private TetheringManager.TetheringEventCallback mTetheringEventCallback;
|
||||
private NetworkStatsCollection mPlatformNetworkStatsCollection =
|
||||
new NetworkStatsCollection(30 * HOUR_IN_MILLIS);
|
||||
private boolean mStoreFilesInApexData = false;
|
||||
private int mImportLegacyTargetAttempts = 0;
|
||||
private @Mock PersistentInt mImportLegacyAttemptsCounter;
|
||||
private @Mock PersistentInt mImportLegacySuccessesCounter;
|
||||
|
||||
private class MockContext extends BroadcastInterceptingContext {
|
||||
private final Context mBaseContext;
|
||||
@@ -295,8 +305,7 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
||||
mHandlerThread = new HandlerThread("HandlerThread");
|
||||
final NetworkStatsService.Dependencies deps = makeDependencies();
|
||||
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
|
||||
mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir,
|
||||
getBaseDir(mStatsDir), deps);
|
||||
mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
|
||||
|
||||
mElapsedRealtime = 0L;
|
||||
|
||||
@@ -338,6 +347,39 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
||||
@NonNull
|
||||
private NetworkStatsService.Dependencies makeDependencies() {
|
||||
return new NetworkStatsService.Dependencies() {
|
||||
@Override
|
||||
public File getOrCreateStatsDir() {
|
||||
return mStatsDir;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getStoreFilesInApexData() {
|
||||
return mStoreFilesInApexData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getImportLegacyTargetAttempts() {
|
||||
return mImportLegacyTargetAttempts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersistentInt createImportLegacyAttemptsCounter(
|
||||
@androidx.annotation.NonNull Path path) {
|
||||
return mImportLegacyAttemptsCounter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PersistentInt createImportLegacySuccessesCounter(
|
||||
@androidx.annotation.NonNull Path path) {
|
||||
return mImportLegacySuccessesCounter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public NetworkStatsCollection readPlatformCollection(
|
||||
@NonNull String prefix, long bucketDuration) {
|
||||
return mPlatformNetworkStatsCollection;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerThread makeHandlerThread() {
|
||||
return mHandlerThread;
|
||||
@@ -1704,10 +1746,65 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
||||
assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
|
||||
}
|
||||
|
||||
private static File getBaseDir(File statsDir) {
|
||||
File baseDir = new File(statsDir, "netstats");
|
||||
baseDir.mkdirs();
|
||||
return baseDir;
|
||||
/**
|
||||
* Verify the service will perform data migration process can be controlled by the device flag.
|
||||
*/
|
||||
@Test
|
||||
public void testDataMigration() throws Exception {
|
||||
assertStatsFilesExist(false);
|
||||
expectDefaultSettings();
|
||||
final long bucketDuration = 30 * HOUR_IN_MILLIS;
|
||||
final NetworkIdentity ident = new NetworkIdentity.Builder()
|
||||
.setType(TYPE_MOBILE)
|
||||
.setMetered(true)
|
||||
.setSubscriberId(IMSI_1).build();
|
||||
final NetworkStatsCollection.Key key = new NetworkStatsCollection.Key(
|
||||
Set.of(ident), UID_ALL, SET_FOREGROUND, 0x0 /* tag */);
|
||||
final NetworkStatsHistory history = new NetworkStatsHistory.Builder(bucketDuration, 0)
|
||||
.addEntry(new NetworkStatsHistory.Entry(0, 10, 31, 3, 50, 5, 1)).build();
|
||||
|
||||
// Mock mobile traffic which will be reported by
|
||||
// NetworkStatsDataMigrationUtils and verify it won't be absorbed if the flag is not set.
|
||||
// TODO: Also mock UID traffic when service queries with PREFIX_UID. And
|
||||
// verify with assertUidTotal.
|
||||
mPlatformNetworkStatsCollection = new NetworkStatsCollection.Builder(bucketDuration)
|
||||
.addEntry(key, history).build();
|
||||
mStoreFilesInApexData = true;
|
||||
mImportLegacyTargetAttempts = 0;
|
||||
mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
|
||||
assertStatsFilesExist(false);
|
||||
|
||||
// Mock zero usage and boot through serviceReady(), verify there is no imported data.
|
||||
expectDefaultSettings();
|
||||
expectNetworkStatsUidDetail(buildEmptyStats());
|
||||
expectSystemReady();
|
||||
mService.systemReady();
|
||||
assertStatsFilesExist(false);
|
||||
|
||||
// Set the flag and reboot, verify the imported data is not there until next boot.
|
||||
mImportLegacyTargetAttempts = 3;
|
||||
mServiceContext.sendBroadcast(new Intent(Intent.ACTION_SHUTDOWN));
|
||||
assertStatsFilesExist(false);
|
||||
|
||||
// Boot through systemReady() again.
|
||||
expectDefaultSettings();
|
||||
expectNetworkStatsUidDetail(buildEmptyStats());
|
||||
expectSystemReady();
|
||||
mService.systemReady();
|
||||
|
||||
// After systemReady(), the service should have historical stats loaded again.
|
||||
// Thus, verify
|
||||
// 1. The stats are absorbed by the recorder.
|
||||
// 2. The imported data are persisted.
|
||||
// 3. The attempts count is set to target attempts count to indicate a successful
|
||||
// migration.
|
||||
assertNetworkTotal(sTemplateImsi1, 31L, 3L, 50L, 5L, 1);
|
||||
assertStatsFilesExist(true);
|
||||
verify(mImportLegacyAttemptsCounter).set(3);
|
||||
verify(mImportLegacySuccessesCounter).set(1);
|
||||
|
||||
// TODO: Verify upgrading with Exception won't damege original data and
|
||||
// will decrease the retry counter by 1.
|
||||
}
|
||||
|
||||
private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
|
||||
@@ -1816,11 +1913,10 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
|
||||
}
|
||||
|
||||
private void assertStatsFilesExist(boolean exist) {
|
||||
final File basePath = new File(mStatsDir, "netstats");
|
||||
if (exist) {
|
||||
assertTrue(basePath.list().length > 0);
|
||||
assertTrue(mStatsDir.list().length > 0);
|
||||
} else {
|
||||
assertTrue(basePath.list().length == 0);
|
||||
assertTrue(mStatsDir.list().length == 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user