[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:
Junyu Lai
2022-01-28 03:38:43 +00:00
parent 76c0f6f0e8
commit 27e0a9833e
3 changed files with 418 additions and 65 deletions

View File

@@ -21,6 +21,7 @@ import static android.net.TrafficStats.KB_IN_BYTES;
import static android.net.TrafficStats.MB_IN_BYTES; import static android.net.TrafficStats.MB_IN_BYTES;
import static android.text.format.DateUtils.YEAR_IN_MILLIS; import static android.text.format.DateUtils.YEAR_IN_MILLIS;
import android.annotation.NonNull;
import android.net.NetworkIdentitySet; import android.net.NetworkIdentitySet;
import android.net.NetworkStats; import android.net.NetworkStats;
import android.net.NetworkStats.NonMonotonicObserver; import android.net.NetworkStats.NonMonotonicObserver;
@@ -68,7 +69,7 @@ public class NetworkStatsRecorder {
private static final String TAG_NETSTATS_DUMP = "netstats_dump"; 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 static final boolean DUMP_BEFORE_DELETE = true;
private final FileRotator mRotator; private final FileRotator mRotator;
@@ -156,6 +157,15 @@ public class NetworkStatsRecorder {
return mSinceBoot; return mSinceBoot;
} }
public long getBucketDuration() {
return mBucketDuration;
}
@NonNull
public String getCookie() {
return mCookie;
}
/** /**
* Load complete history represented by {@link FileRotator}. Caches * Load complete history represented by {@link FileRotator}. Caches
* internally as a {@link WeakReference}, and updated with future * internally as a {@link WeakReference}, and updated with future
@@ -189,10 +199,10 @@ public class NetworkStatsRecorder {
res.recordCollection(mPending); res.recordCollection(mPending);
} catch (IOException e) { } catch (IOException e) {
Log.wtf(TAG, "problem completely reading network stats", e); Log.wtf(TAG, "problem completely reading network stats", e);
recoverFromWtf(); recoverAndDeleteData();
} catch (OutOfMemoryError e) { } catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem completely reading network stats", e); Log.wtf(TAG, "problem completely reading network stats", e);
recoverFromWtf(); recoverAndDeleteData();
} }
return res; return res;
} }
@@ -300,10 +310,10 @@ public class NetworkStatsRecorder {
mPending.reset(); mPending.reset();
} catch (IOException e) { } catch (IOException e) {
Log.wtf(TAG, "problem persisting pending stats", e); Log.wtf(TAG, "problem persisting pending stats", e);
recoverFromWtf(); recoverAndDeleteData();
} catch (OutOfMemoryError e) { } catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem persisting pending stats", e); Log.wtf(TAG, "problem persisting pending stats", e);
recoverFromWtf(); recoverAndDeleteData();
} }
} }
} }
@@ -319,10 +329,10 @@ public class NetworkStatsRecorder {
mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids)); mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids));
} catch (IOException e) { } catch (IOException e) {
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e); Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e);
recoverFromWtf(); recoverAndDeleteData();
} catch (OutOfMemoryError e) { } catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), 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 * Rewriter that will combine current {@link NetworkStatsCollection} values
* with anything read from disk, and write combined set to disk. Clears the * with anything read from disk, and write combined set to disk.
* original {@link NetworkStatsCollection} when finished writing.
*/ */
private static class CombiningRewriter implements FileRotator.Rewriter { private static class CombiningRewriter implements FileRotator.Rewriter {
private final NetworkStatsCollection mCollection; private final NetworkStatsCollection mCollection;
@@ -375,7 +384,6 @@ public class NetworkStatsRecorder {
@Override @Override
public void write(OutputStream out) throws IOException { public void write(OutputStream out) throws IOException {
mCollection.write(out); 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 * Rewriter that will remove any histories or persisted data points before the
* specified cutoff time, only writing data back when modified. * specified cutoff time, only writing data back when modified.
@@ -501,10 +526,10 @@ public class NetworkStatsRecorder {
mBucketDuration, cutoffMillis)); mBucketDuration, cutoffMillis));
} catch (IOException e) { } catch (IOException e) {
Log.wtf(TAG, "problem importing netstats", e); Log.wtf(TAG, "problem importing netstats", e);
recoverFromWtf(); recoverAndDeleteData();
} catch (OutOfMemoryError e) { } catch (OutOfMemoryError e) {
Log.wtf(TAG, "problem importing netstats", 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 * Recover from {@link FileRotator} failure by dumping state to
* {@link DropBoxManager} and deleting contents. * {@link DropBoxManager} and deleting contents.
*/ */
private void recoverFromWtf() { void recoverAndDeleteData() {
if (DUMP_BEFORE_DELETE) { if (DUMP_BEFORE_DELETE) {
final ByteArrayOutputStream os = new ByteArrayOutputStream(); final ByteArrayOutputStream os = new ByteArrayOutputStream();
try { try {

View File

@@ -67,6 +67,7 @@ import android.annotation.TargetApi;
import android.app.AlarmManager; import android.app.AlarmManager;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.app.usage.NetworkStatsManager; import android.app.usage.NetworkStatsManager;
import android.content.ApexEnvironment;
import android.content.BroadcastReceiver; import android.content.BroadcastReceiver;
import android.content.ContentResolver; import android.content.ContentResolver;
import android.content.Context; import android.content.Context;
@@ -100,6 +101,7 @@ import android.net.TrafficStats;
import android.net.UnderlyingNetworkInfo; import android.net.UnderlyingNetworkInfo;
import android.net.Uri; import android.net.Uri;
import android.net.netstats.IUsageCallback; import android.net.netstats.IUsageCallback;
import android.net.netstats.NetworkStatsDataMigrationUtils;
import android.net.netstats.provider.INetworkStatsProvider; import android.net.netstats.provider.INetworkStatsProvider;
import android.net.netstats.provider.INetworkStatsProviderCallback; import android.net.netstats.provider.INetworkStatsProviderCallback;
import android.net.netstats.provider.NetworkStatsProvider; import android.net.netstats.provider.NetworkStatsProvider;
@@ -118,6 +120,7 @@ import android.os.ServiceSpecificException;
import android.os.SystemClock; import android.os.SystemClock;
import android.os.Trace; import android.os.Trace;
import android.os.UserHandle; import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.Settings; import android.provider.Settings;
import android.provider.Settings.Global; import android.provider.Settings.Global;
import android.service.NetworkInterfaceProto; 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.BinderUtils;
import com.android.net.module.util.BpfMap; import com.android.net.module.util.BpfMap;
import com.android.net.module.util.CollectionUtils; 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.IBpfMap;
import com.android.net.module.util.LocationPermissionChecker; import com.android.net.module.util.LocationPermissionChecker;
import com.android.net.module.util.NetworkStatsUtils; import com.android.net.module.util.NetworkStatsUtils;
@@ -155,7 +159,9 @@ import java.io.FileDescriptor;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.nio.file.Path;
import java.time.Clock; import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -232,6 +238,22 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
private static final String STATS_MAP_B_PATH = private static final String STATS_MAP_B_PATH =
"/sys/fs/bpf/net_shared/map_netd_stats_map_B"; "/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 Context mContext;
private final NetworkStatsFactory mStatsFactory; private final NetworkStatsFactory mStatsFactory;
private final AlarmManager mAlarmManager; private final AlarmManager mAlarmManager;
@@ -239,8 +261,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
private final NetworkStatsSettings mSettings; private final NetworkStatsSettings mSettings;
private final NetworkStatsObservers mStatsObservers; private final NetworkStatsObservers mStatsObservers;
private final File mSystemDir; private final File mStatsDir;
private final File mBaseDir;
private final PowerManager.WakeLock mWakeLock; private final PowerManager.WakeLock mWakeLock;
@@ -250,6 +271,12 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
protected INetd mNetd; protected INetd mNetd;
private final AlertObserver mAlertObserver = new AlertObserver(); 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 @VisibleForTesting
public static final String ACTION_NETWORK_STATS_POLL = public static final String ACTION_NETWORK_STATS_POLL =
"com.android.server.action.NETWORK_STATS_POLL"; "com.android.server.action.NETWORK_STATS_POLL";
@@ -405,16 +432,6 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
@NonNull @NonNull
private final BpfInterfaceMapUpdater mInterfaceMapUpdater; 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() { private static @NonNull Clock getDefaultClock() {
return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(), return new BestClock(ZoneOffset.UTC, SystemClock.currentNetworkTimeClock(),
Clock.systemUTC()); Clock.systemUTC());
@@ -506,8 +523,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)), INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE)),
alarmManager, wakeLock, getDefaultClock(), alarmManager, wakeLock, getDefaultClock(),
new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context), new DefaultNetworkStatsSettings(), new NetworkStatsFactory(context),
new NetworkStatsObservers(), getDefaultSystemDir(), getDefaultBaseDir(), new NetworkStatsObservers(), new Dependencies());
new Dependencies());
return service; return service;
} }
@@ -517,8 +533,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
@VisibleForTesting @VisibleForTesting
NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager, NetworkStatsService(Context context, INetd netd, AlarmManager alarmManager,
PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings, PowerManager.WakeLock wakeLock, Clock clock, NetworkStatsSettings settings,
NetworkStatsFactory factory, NetworkStatsObservers statsObservers, File systemDir, NetworkStatsFactory factory, NetworkStatsObservers statsObservers,
File baseDir, @NonNull Dependencies deps) { @NonNull Dependencies deps) {
mContext = Objects.requireNonNull(context, "missing Context"); mContext = Objects.requireNonNull(context, "missing Context");
mNetd = Objects.requireNonNull(netd, "missing Netd"); mNetd = Objects.requireNonNull(netd, "missing Netd");
mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager"); mAlarmManager = Objects.requireNonNull(alarmManager, "missing AlarmManager");
@@ -527,9 +543,11 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock"); mWakeLock = Objects.requireNonNull(wakeLock, "missing WakeLock");
mStatsFactory = Objects.requireNonNull(factory, "missing factory"); mStatsFactory = Objects.requireNonNull(factory, "missing factory");
mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers"); mStatsObservers = Objects.requireNonNull(statsObservers, "missing NetworkStatsObservers");
mSystemDir = Objects.requireNonNull(systemDir, "missing systemDir");
mBaseDir = Objects.requireNonNull(baseDir, "missing baseDir");
mDeps = Objects.requireNonNull(deps, "missing Dependencies"); 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(); final HandlerThread handlerThread = mDeps.makeHandlerThread();
handlerThread.start(); handlerThread.start();
@@ -555,6 +573,79 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
// TODO: Move more stuff into dependencies object. // TODO: Move more stuff into dependencies object.
@VisibleForTesting @VisibleForTesting
public static class Dependencies { 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. * Create a HandlerThread to use in NetworkStatsService.
*/ */
@@ -697,7 +788,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
updatePersistThresholdsLocked(); updatePersistThresholdsLocked();
// upgrade any legacy stats, migrating them to rotated files // upgrade any legacy stats
maybeUpgradeLegacyStatsLocked(); maybeUpgradeLegacyStatsLocked();
// read historical network stats from disk, since policy service // 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( final DropBoxManager dropBox = (DropBoxManager) mContext.getSystemService(
Context.DROPBOX_SERVICE); Context.DROPBOX_SERVICE);
return new NetworkStatsRecorder(new FileRotator( return new NetworkStatsRecorder(new FileRotator(
mBaseDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis), mStatsDir, prefix, config.rotateAgeMillis, config.deleteAgeMillis),
mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags); mNonMonotonicObserver, dropBox, prefix, config.bucketDuration, includeTags);
} }
@@ -791,32 +882,151 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
mSystemReady = false; 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") @GuardedBy("mStatsLock")
private void maybeUpgradeLegacyStatsLocked() { private void maybeUpgradeLegacyStatsLocked() {
File file; final boolean storeFilesInApexData = mDeps.getStoreFilesInApexData();
try { if (!storeFilesInApexData) {
file = new File(mSystemDir, "netstats.bin"); return;
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);
} }
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; return;
} }
pw.println("Directory:");
pw.increaseIndent();
pw.println(mStatsDir);
pw.decreaseIndent();
pw.println("Configs:"); pw.println("Configs:");
pw.increaseIndent(); pw.increaseIndent();
pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled()); pw.print(NETSTATS_COMBINE_SUBTYPE_ENABLED, mSettings.getCombineSubtypeEnabled());
pw.println(); 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.decreaseIndent();
pw.println("Active interfaces:"); pw.println("Active interfaces:");

View File

@@ -94,8 +94,10 @@ import android.net.INetworkStatsSession;
import android.net.LinkProperties; import android.net.LinkProperties;
import android.net.Network; import android.net.Network;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.NetworkIdentity;
import android.net.NetworkStateSnapshot; import android.net.NetworkStateSnapshot;
import android.net.NetworkStats; import android.net.NetworkStats;
import android.net.NetworkStatsCollection;
import android.net.NetworkStatsHistory; import android.net.NetworkStatsHistory;
import android.net.NetworkTemplate; import android.net.NetworkTemplate;
import android.net.TelephonyNetworkSpecifier; import android.net.TelephonyNetworkSpecifier;
@@ -143,9 +145,11 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.io.File; import java.io.File;
import java.nio.file.Path;
import java.time.Clock; import java.time.Clock;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.Objects; import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
@@ -220,6 +224,12 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
private ContentObserver mContentObserver; private ContentObserver mContentObserver;
private Handler mHandler; private Handler mHandler;
private TetheringManager.TetheringEventCallback mTetheringEventCallback; 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 class MockContext extends BroadcastInterceptingContext {
private final Context mBaseContext; private final Context mBaseContext;
@@ -295,8 +305,7 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
mHandlerThread = new HandlerThread("HandlerThread"); mHandlerThread = new HandlerThread("HandlerThread");
final NetworkStatsService.Dependencies deps = makeDependencies(); final NetworkStatsService.Dependencies deps = makeDependencies();
mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock, mService = new NetworkStatsService(mServiceContext, mNetd, mAlarmManager, wakeLock,
mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), mStatsDir, mClock, mSettings, mStatsFactory, new NetworkStatsObservers(), deps);
getBaseDir(mStatsDir), deps);
mElapsedRealtime = 0L; mElapsedRealtime = 0L;
@@ -338,6 +347,39 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
@NonNull @NonNull
private NetworkStatsService.Dependencies makeDependencies() { private NetworkStatsService.Dependencies makeDependencies() {
return new NetworkStatsService.Dependencies() { 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 @Override
public HandlerThread makeHandlerThread() { public HandlerThread makeHandlerThread() {
return mHandlerThread; return mHandlerThread;
@@ -1704,10 +1746,65 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0); assertNetworkTotal(sTemplateImsi1, 0L, 0L, 0L, 0L, 0);
} }
private static File getBaseDir(File statsDir) { /**
File baseDir = new File(statsDir, "netstats"); * Verify the service will perform data migration process can be controlled by the device flag.
baseDir.mkdirs(); */
return baseDir; @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, private void assertNetworkTotal(NetworkTemplate template, long rxBytes, long rxPackets,
@@ -1816,11 +1913,10 @@ public class NetworkStatsServiceTest extends NetworkStatsBaseTest {
} }
private void assertStatsFilesExist(boolean exist) { private void assertStatsFilesExist(boolean exist) {
final File basePath = new File(mStatsDir, "netstats");
if (exist) { if (exist) {
assertTrue(basePath.list().length > 0); assertTrue(mStatsDir.list().length > 0);
} else { } else {
assertTrue(basePath.list().length == 0); assertTrue(mStatsDir.list().length == 0);
} }
} }