1 /* 2 * Copyright (C) 2012 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.net; 18 19 import static android.net.NetworkStats.TAG_NONE; 20 import static android.net.TrafficStats.KB_IN_BYTES; 21 import static android.net.TrafficStats.MB_IN_BYTES; 22 import static com.android.internal.util.Preconditions.checkNotNull; 23 24 import android.net.NetworkStats; 25 import android.net.NetworkStats.NonMonotonicObserver; 26 import android.net.NetworkStatsHistory; 27 import android.net.NetworkTemplate; 28 import android.net.TrafficStats; 29 import android.os.DropBoxManager; 30 import android.util.Log; 31 import android.util.MathUtils; 32 import android.util.Slog; 33 34 import com.android.internal.net.VpnInfo; 35 import com.android.internal.util.FileRotator; 36 import com.android.internal.util.IndentingPrintWriter; 37 import com.google.android.collect.Sets; 38 39 import java.io.ByteArrayOutputStream; 40 import java.io.DataOutputStream; 41 import java.io.File; 42 import java.io.IOException; 43 import java.io.InputStream; 44 import java.io.OutputStream; 45 import java.io.PrintWriter; 46 import java.lang.ref.WeakReference; 47 import java.util.Arrays; 48 import java.util.HashSet; 49 import java.util.Map; 50 51 import libcore.io.IoUtils; 52 53 /** 54 * Logic to record deltas between periodic {@link NetworkStats} snapshots into 55 * {@link NetworkStatsHistory} that belong to {@link NetworkStatsCollection}. 56 * Keeps pending changes in memory until they pass a specific threshold, in 57 * bytes. Uses {@link FileRotator} for persistence logic. 58 * <p> 59 * Not inherently thread safe. 60 */ 61 public class NetworkStatsRecorder { 62 private static final String TAG = "NetworkStatsRecorder"; 63 private static final boolean LOGD = false; 64 private static final boolean LOGV = false; 65 66 private static final String TAG_NETSTATS_DUMP = "netstats_dump"; 67 68 /** Dump before deleting in {@link #recoverFromWtf()}. */ 69 private static final boolean DUMP_BEFORE_DELETE = true; 70 71 private final FileRotator mRotator; 72 private final NonMonotonicObserver<String> mObserver; 73 private final DropBoxManager mDropBox; 74 private final String mCookie; 75 76 private final long mBucketDuration; 77 private final boolean mOnlyTags; 78 79 private long mPersistThresholdBytes = 2 * MB_IN_BYTES; 80 private NetworkStats mLastSnapshot; 81 82 private final NetworkStatsCollection mPending; 83 private final NetworkStatsCollection mSinceBoot; 84 85 private final CombiningRewriter mPendingRewriter; 86 87 private WeakReference<NetworkStatsCollection> mComplete; 88 89 public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer, 90 DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) { 91 mRotator = checkNotNull(rotator, "missing FileRotator"); 92 mObserver = checkNotNull(observer, "missing NonMonotonicObserver"); 93 mDropBox = checkNotNull(dropBox, "missing DropBoxManager"); 94 mCookie = cookie; 95 96 mBucketDuration = bucketDuration; 97 mOnlyTags = onlyTags; 98 99 mPending = new NetworkStatsCollection(bucketDuration); 100 mSinceBoot = new NetworkStatsCollection(bucketDuration); 101 102 mPendingRewriter = new CombiningRewriter(mPending); 103 } 104 105 public void setPersistThreshold(long thresholdBytes) { 106 if (LOGV) Slog.v(TAG, "setPersistThreshold() with " + thresholdBytes); 107 mPersistThresholdBytes = MathUtils.constrain( 108 thresholdBytes, 1 * KB_IN_BYTES, 100 * MB_IN_BYTES); 109 } 110 111 public void resetLocked() { 112 mLastSnapshot = null; 113 mPending.reset(); 114 mSinceBoot.reset(); 115 mComplete.clear(); 116 } 117 118 public NetworkStats.Entry getTotalSinceBootLocked(NetworkTemplate template) { 119 return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE).getTotal(null); 120 } 121 122 /** 123 * Load complete history represented by {@link FileRotator}. Caches 124 * internally as a {@link WeakReference}, and updated with future 125 * {@link #recordSnapshotLocked(NetworkStats, Map, long)} snapshots as long 126 * as reference is valid. 127 */ 128 public NetworkStatsCollection getOrLoadCompleteLocked() { 129 NetworkStatsCollection res = mComplete != null ? mComplete.get() : null; 130 if (res == null) { 131 res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE); 132 mComplete = new WeakReference<NetworkStatsCollection>(res); 133 } 134 return res; 135 } 136 137 public NetworkStatsCollection getOrLoadPartialLocked(long start, long end) { 138 NetworkStatsCollection res = mComplete != null ? mComplete.get() : null; 139 if (res == null) { 140 res = loadLocked(start, end); 141 } 142 return res; 143 } 144 145 private NetworkStatsCollection loadLocked(long start, long end) { 146 if (LOGD) Slog.d(TAG, "loadLocked() reading from disk for " + mCookie); 147 final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration); 148 try { 149 mRotator.readMatching(res, start, end); 150 res.recordCollection(mPending); 151 } catch (IOException e) { 152 Log.wtf(TAG, "problem completely reading network stats", e); 153 recoverFromWtf(); 154 } catch (OutOfMemoryError e) { 155 Log.wtf(TAG, "problem completely reading network stats", e); 156 recoverFromWtf(); 157 } 158 return res; 159 } 160 161 /** 162 * Record any delta that occurred since last {@link NetworkStats} snapshot, 163 * using the given {@link Map} to identify network interfaces. First 164 * snapshot is considered bootstrap, and is not counted as delta. 165 */ 166 public void recordSnapshotLocked(NetworkStats snapshot, 167 Map<String, NetworkIdentitySet> ifaceIdent, VpnInfo[] vpnArray, 168 long currentTimeMillis) { 169 final HashSet<String> unknownIfaces = Sets.newHashSet(); 170 171 // skip recording when snapshot missing 172 if (snapshot == null) return; 173 174 // assume first snapshot is bootstrap and don't record 175 if (mLastSnapshot == null) { 176 mLastSnapshot = snapshot; 177 return; 178 } 179 180 final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null; 181 182 final NetworkStats delta = NetworkStats.subtract( 183 snapshot, mLastSnapshot, mObserver, mCookie); 184 final long end = currentTimeMillis; 185 final long start = end - delta.getElapsedRealtime(); 186 187 if (vpnArray != null) { 188 for (VpnInfo info : vpnArray) { 189 delta.migrateTun(info.ownerUid, info.vpnIface, info.primaryUnderlyingIface); 190 } 191 } 192 193 NetworkStats.Entry entry = null; 194 for (int i = 0; i < delta.size(); i++) { 195 entry = delta.getValues(i, entry); 196 final NetworkIdentitySet ident = ifaceIdent.get(entry.iface); 197 if (ident == null) { 198 unknownIfaces.add(entry.iface); 199 continue; 200 } 201 202 // skip when no delta occurred 203 if (entry.isEmpty()) continue; 204 205 // only record tag data when requested 206 if ((entry.tag == TAG_NONE) != mOnlyTags) { 207 mPending.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry); 208 209 // also record against boot stats when present 210 if (mSinceBoot != null) { 211 mSinceBoot.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry); 212 } 213 214 // also record against complete dataset when present 215 if (complete != null) { 216 complete.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry); 217 } 218 } 219 } 220 221 mLastSnapshot = snapshot; 222 223 if (LOGV && unknownIfaces.size() > 0) { 224 Slog.w(TAG, "unknown interfaces " + unknownIfaces + ", ignoring those stats"); 225 } 226 } 227 228 /** 229 * Consider persisting any pending deltas, if they are beyond 230 * {@link #mPersistThresholdBytes}. 231 */ 232 public void maybePersistLocked(long currentTimeMillis) { 233 final long pendingBytes = mPending.getTotalBytes(); 234 if (pendingBytes >= mPersistThresholdBytes) { 235 forcePersistLocked(currentTimeMillis); 236 } else { 237 mRotator.maybeRotate(currentTimeMillis); 238 } 239 } 240 241 /** 242 * Force persisting any pending deltas. 243 */ 244 public void forcePersistLocked(long currentTimeMillis) { 245 if (mPending.isDirty()) { 246 if (LOGD) Slog.d(TAG, "forcePersistLocked() writing for " + mCookie); 247 try { 248 mRotator.rewriteActive(mPendingRewriter, currentTimeMillis); 249 mRotator.maybeRotate(currentTimeMillis); 250 mPending.reset(); 251 } catch (IOException e) { 252 Log.wtf(TAG, "problem persisting pending stats", e); 253 recoverFromWtf(); 254 } catch (OutOfMemoryError e) { 255 Log.wtf(TAG, "problem persisting pending stats", e); 256 recoverFromWtf(); 257 } 258 } 259 } 260 261 /** 262 * Remove the given UID from all {@link FileRotator} history, migrating it 263 * to {@link TrafficStats#UID_REMOVED}. 264 */ 265 public void removeUidsLocked(int[] uids) { 266 try { 267 // Rewrite all persisted data to migrate UID stats 268 mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids)); 269 } catch (IOException e) { 270 Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e); 271 recoverFromWtf(); 272 } catch (OutOfMemoryError e) { 273 Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e); 274 recoverFromWtf(); 275 } 276 277 // Remove any pending stats 278 mPending.removeUids(uids); 279 mSinceBoot.removeUids(uids); 280 281 // Clear UID from current stats snapshot 282 if (mLastSnapshot != null) { 283 mLastSnapshot = mLastSnapshot.withoutUids(uids); 284 } 285 286 final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null; 287 if (complete != null) { 288 complete.removeUids(uids); 289 } 290 } 291 292 /** 293 * Rewriter that will combine current {@link NetworkStatsCollection} values 294 * with anything read from disk, and write combined set to disk. Clears the 295 * original {@link NetworkStatsCollection} when finished writing. 296 */ 297 private static class CombiningRewriter implements FileRotator.Rewriter { 298 private final NetworkStatsCollection mCollection; 299 300 public CombiningRewriter(NetworkStatsCollection collection) { 301 mCollection = checkNotNull(collection, "missing NetworkStatsCollection"); 302 } 303 304 @Override 305 public void reset() { 306 // ignored 307 } 308 309 @Override 310 public void read(InputStream in) throws IOException { 311 mCollection.read(in); 312 } 313 314 @Override 315 public boolean shouldWrite() { 316 return true; 317 } 318 319 @Override 320 public void write(OutputStream out) throws IOException { 321 mCollection.write(new DataOutputStream(out)); 322 mCollection.reset(); 323 } 324 } 325 326 /** 327 * Rewriter that will remove any {@link NetworkStatsHistory} attributed to 328 * the requested UID, only writing data back when modified. 329 */ 330 public static class RemoveUidRewriter implements FileRotator.Rewriter { 331 private final NetworkStatsCollection mTemp; 332 private final int[] mUids; 333 334 public RemoveUidRewriter(long bucketDuration, int[] uids) { 335 mTemp = new NetworkStatsCollection(bucketDuration); 336 mUids = uids; 337 } 338 339 @Override 340 public void reset() { 341 mTemp.reset(); 342 } 343 344 @Override 345 public void read(InputStream in) throws IOException { 346 mTemp.read(in); 347 mTemp.clearDirty(); 348 mTemp.removeUids(mUids); 349 } 350 351 @Override 352 public boolean shouldWrite() { 353 return mTemp.isDirty(); 354 } 355 356 @Override 357 public void write(OutputStream out) throws IOException { 358 mTemp.write(new DataOutputStream(out)); 359 } 360 } 361 362 public void importLegacyNetworkLocked(File file) throws IOException { 363 // legacy file still exists; start empty to avoid double importing 364 mRotator.deleteAll(); 365 366 final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration); 367 collection.readLegacyNetwork(file); 368 369 final long startMillis = collection.getStartMillis(); 370 final long endMillis = collection.getEndMillis(); 371 372 if (!collection.isEmpty()) { 373 // process legacy data, creating active file at starting time, then 374 // using end time to possibly trigger rotation. 375 mRotator.rewriteActive(new CombiningRewriter(collection), startMillis); 376 mRotator.maybeRotate(endMillis); 377 } 378 } 379 380 public void importLegacyUidLocked(File file) throws IOException { 381 // legacy file still exists; start empty to avoid double importing 382 mRotator.deleteAll(); 383 384 final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration); 385 collection.readLegacyUid(file, mOnlyTags); 386 387 final long startMillis = collection.getStartMillis(); 388 final long endMillis = collection.getEndMillis(); 389 390 if (!collection.isEmpty()) { 391 // process legacy data, creating active file at starting time, then 392 // using end time to possibly trigger rotation. 393 mRotator.rewriteActive(new CombiningRewriter(collection), startMillis); 394 mRotator.maybeRotate(endMillis); 395 } 396 } 397 398 public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) { 399 pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes()); 400 if (fullHistory) { 401 pw.println("Complete history:"); 402 getOrLoadCompleteLocked().dump(pw); 403 } else { 404 pw.println("History since boot:"); 405 mSinceBoot.dump(pw); 406 } 407 } 408 409 public void dumpCheckin(PrintWriter pw, long start, long end) { 410 // Only load and dump stats from the requested window 411 getOrLoadPartialLocked(start, end).dumpCheckin(pw, start, end); 412 } 413 414 /** 415 * Recover from {@link FileRotator} failure by dumping state to 416 * {@link DropBoxManager} and deleting contents. 417 */ 418 private void recoverFromWtf() { 419 if (DUMP_BEFORE_DELETE) { 420 final ByteArrayOutputStream os = new ByteArrayOutputStream(); 421 try { 422 mRotator.dumpAll(os); 423 } catch (IOException e) { 424 // ignore partial contents 425 os.reset(); 426 } finally { 427 IoUtils.closeQuietly(os); 428 } 429 mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0); 430 } 431 432 mRotator.deleteAll(); 433 } 434 } 435