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 android.text.format.DateUtils.YEAR_IN_MILLIS; 23 24 import static com.android.internal.util.Preconditions.checkNotNull; 25 26 import android.annotation.Nullable; 27 import android.net.NetworkStats; 28 import android.net.NetworkStats.NonMonotonicObserver; 29 import android.net.NetworkStatsHistory; 30 import android.net.NetworkTemplate; 31 import android.net.TrafficStats; 32 import android.os.Binder; 33 import android.os.DropBoxManager; 34 import android.service.NetworkStatsRecorderProto; 35 import android.util.Log; 36 import android.util.MathUtils; 37 import android.util.Slog; 38 import android.util.proto.ProtoOutputStream; 39 40 import com.android.internal.net.VpnInfo; 41 import com.android.internal.util.FileRotator; 42 import com.android.internal.util.IndentingPrintWriter; 43 44 import libcore.io.IoUtils; 45 46 import com.google.android.collect.Sets; 47 48 import java.io.ByteArrayOutputStream; 49 import java.io.DataOutputStream; 50 import java.io.File; 51 import java.io.IOException; 52 import java.io.InputStream; 53 import java.io.OutputStream; 54 import java.io.PrintWriter; 55 import java.lang.ref.WeakReference; 56 import java.util.Arrays; 57 import java.util.HashSet; 58 import java.util.Map; 59 60 /** 61 * Logic to record deltas between periodic {@link NetworkStats} snapshots into 62 * {@link NetworkStatsHistory} that belong to {@link NetworkStatsCollection}. 63 * Keeps pending changes in memory until they pass a specific threshold, in 64 * bytes. Uses {@link FileRotator} for persistence logic if present. 65 * <p> 66 * Not inherently thread safe. 67 */ 68 public class NetworkStatsRecorder { 69 private static final String TAG = "NetworkStatsRecorder"; 70 private static final boolean LOGD = false; 71 private static final boolean LOGV = false; 72 73 private static final String TAG_NETSTATS_DUMP = "netstats_dump"; 74 75 /** Dump before deleting in {@link #recoverFromWtf()}. */ 76 private static final boolean DUMP_BEFORE_DELETE = true; 77 78 private final FileRotator mRotator; 79 private final NonMonotonicObserver<String> mObserver; 80 private final DropBoxManager mDropBox; 81 private final String mCookie; 82 83 private final long mBucketDuration; 84 private final boolean mOnlyTags; 85 86 private long mPersistThresholdBytes = 2 * MB_IN_BYTES; 87 private NetworkStats mLastSnapshot; 88 89 private final NetworkStatsCollection mPending; 90 private final NetworkStatsCollection mSinceBoot; 91 92 private final CombiningRewriter mPendingRewriter; 93 94 private WeakReference<NetworkStatsCollection> mComplete; 95 96 /** 97 * Non-persisted recorder, with only one bucket. Used by {@link NetworkStatsObservers}. 98 */ NetworkStatsRecorder()99 public NetworkStatsRecorder() { 100 mRotator = null; 101 mObserver = null; 102 mDropBox = null; 103 mCookie = null; 104 105 // set the bucket big enough to have all data in one bucket, but allow some 106 // slack to avoid overflow 107 mBucketDuration = YEAR_IN_MILLIS; 108 mOnlyTags = false; 109 110 mPending = null; 111 mSinceBoot = new NetworkStatsCollection(mBucketDuration); 112 113 mPendingRewriter = null; 114 } 115 116 /** 117 * Persisted recorder. 118 */ NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer, DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags)119 public NetworkStatsRecorder(FileRotator rotator, NonMonotonicObserver<String> observer, 120 DropBoxManager dropBox, String cookie, long bucketDuration, boolean onlyTags) { 121 mRotator = checkNotNull(rotator, "missing FileRotator"); 122 mObserver = checkNotNull(observer, "missing NonMonotonicObserver"); 123 mDropBox = checkNotNull(dropBox, "missing DropBoxManager"); 124 mCookie = cookie; 125 126 mBucketDuration = bucketDuration; 127 mOnlyTags = onlyTags; 128 129 mPending = new NetworkStatsCollection(bucketDuration); 130 mSinceBoot = new NetworkStatsCollection(bucketDuration); 131 132 mPendingRewriter = new CombiningRewriter(mPending); 133 } 134 setPersistThreshold(long thresholdBytes)135 public void setPersistThreshold(long thresholdBytes) { 136 if (LOGV) Slog.v(TAG, "setPersistThreshold() with " + thresholdBytes); 137 mPersistThresholdBytes = MathUtils.constrain( 138 thresholdBytes, 1 * KB_IN_BYTES, 100 * MB_IN_BYTES); 139 } 140 resetLocked()141 public void resetLocked() { 142 mLastSnapshot = null; 143 if (mPending != null) { 144 mPending.reset(); 145 } 146 if (mSinceBoot != null) { 147 mSinceBoot.reset(); 148 } 149 if (mComplete != null) { 150 mComplete.clear(); 151 } 152 } 153 getTotalSinceBootLocked(NetworkTemplate template)154 public NetworkStats.Entry getTotalSinceBootLocked(NetworkTemplate template) { 155 return mSinceBoot.getSummary(template, Long.MIN_VALUE, Long.MAX_VALUE, 156 NetworkStatsAccess.Level.DEVICE, Binder.getCallingUid()).getTotal(null); 157 } 158 getSinceBoot()159 public NetworkStatsCollection getSinceBoot() { 160 return mSinceBoot; 161 } 162 163 /** 164 * Load complete history represented by {@link FileRotator}. Caches 165 * internally as a {@link WeakReference}, and updated with future 166 * {@link #recordSnapshotLocked(NetworkStats, Map, long)} snapshots as long 167 * as reference is valid. 168 */ getOrLoadCompleteLocked()169 public NetworkStatsCollection getOrLoadCompleteLocked() { 170 checkNotNull(mRotator, "missing FileRotator"); 171 NetworkStatsCollection res = mComplete != null ? mComplete.get() : null; 172 if (res == null) { 173 res = loadLocked(Long.MIN_VALUE, Long.MAX_VALUE); 174 mComplete = new WeakReference<NetworkStatsCollection>(res); 175 } 176 return res; 177 } 178 getOrLoadPartialLocked(long start, long end)179 public NetworkStatsCollection getOrLoadPartialLocked(long start, long end) { 180 checkNotNull(mRotator, "missing FileRotator"); 181 NetworkStatsCollection res = mComplete != null ? mComplete.get() : null; 182 if (res == null) { 183 res = loadLocked(start, end); 184 } 185 return res; 186 } 187 loadLocked(long start, long end)188 private NetworkStatsCollection loadLocked(long start, long end) { 189 if (LOGD) Slog.d(TAG, "loadLocked() reading from disk for " + mCookie); 190 final NetworkStatsCollection res = new NetworkStatsCollection(mBucketDuration); 191 try { 192 mRotator.readMatching(res, start, end); 193 res.recordCollection(mPending); 194 } catch (IOException e) { 195 Log.wtf(TAG, "problem completely reading network stats", e); 196 recoverFromWtf(); 197 } catch (OutOfMemoryError e) { 198 Log.wtf(TAG, "problem completely reading network stats", e); 199 recoverFromWtf(); 200 } 201 return res; 202 } 203 204 /** 205 * Record any delta that occurred since last {@link NetworkStats} snapshot, 206 * using the given {@link Map} to identify network interfaces. First 207 * snapshot is considered bootstrap, and is not counted as delta. 208 * 209 * @param vpnArray Optional info about the currently active VPN, if any. This is used to 210 * redistribute traffic from the VPN app to the underlying responsible apps. 211 * This should always be set to null if the provided snapshot is aggregated 212 * across all UIDs (e.g. contains UID_ALL buckets), regardless of VPN state. 213 */ recordSnapshotLocked(NetworkStats snapshot, Map<String, NetworkIdentitySet> ifaceIdent, @Nullable VpnInfo[] vpnArray, long currentTimeMillis)214 public void recordSnapshotLocked(NetworkStats snapshot, 215 Map<String, NetworkIdentitySet> ifaceIdent, @Nullable VpnInfo[] vpnArray, 216 long currentTimeMillis) { 217 final HashSet<String> unknownIfaces = Sets.newHashSet(); 218 219 // skip recording when snapshot missing 220 if (snapshot == null) return; 221 222 // assume first snapshot is bootstrap and don't record 223 if (mLastSnapshot == null) { 224 mLastSnapshot = snapshot; 225 return; 226 } 227 228 final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null; 229 230 final NetworkStats delta = NetworkStats.subtract( 231 snapshot, mLastSnapshot, mObserver, mCookie); 232 final long end = currentTimeMillis; 233 final long start = end - delta.getElapsedRealtime(); 234 235 if (vpnArray != null) { 236 for (VpnInfo info : vpnArray) { 237 delta.migrateTun(info.ownerUid, info.vpnIface, info.primaryUnderlyingIface); 238 } 239 } 240 241 NetworkStats.Entry entry = null; 242 for (int i = 0; i < delta.size(); i++) { 243 entry = delta.getValues(i, entry); 244 245 // As a last-ditch sanity check, report any negative values and 246 // clamp them so recording below doesn't croak. 247 if (entry.isNegative()) { 248 if (mObserver != null) { 249 mObserver.foundNonMonotonic(delta, i, mCookie); 250 } 251 entry.rxBytes = Math.max(entry.rxBytes, 0); 252 entry.rxPackets = Math.max(entry.rxPackets, 0); 253 entry.txBytes = Math.max(entry.txBytes, 0); 254 entry.txPackets = Math.max(entry.txPackets, 0); 255 entry.operations = Math.max(entry.operations, 0); 256 } 257 258 final NetworkIdentitySet ident = ifaceIdent.get(entry.iface); 259 if (ident == null) { 260 unknownIfaces.add(entry.iface); 261 continue; 262 } 263 264 // skip when no delta occurred 265 if (entry.isEmpty()) continue; 266 267 // only record tag data when requested 268 if ((entry.tag == TAG_NONE) != mOnlyTags) { 269 if (mPending != null) { 270 mPending.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry); 271 } 272 273 // also record against boot stats when present 274 if (mSinceBoot != null) { 275 mSinceBoot.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry); 276 } 277 278 // also record against complete dataset when present 279 if (complete != null) { 280 complete.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry); 281 } 282 } 283 } 284 285 mLastSnapshot = snapshot; 286 287 if (LOGV && unknownIfaces.size() > 0) { 288 Slog.w(TAG, "unknown interfaces " + unknownIfaces + ", ignoring those stats"); 289 } 290 } 291 292 /** 293 * Consider persisting any pending deltas, if they are beyond 294 * {@link #mPersistThresholdBytes}. 295 */ maybePersistLocked(long currentTimeMillis)296 public void maybePersistLocked(long currentTimeMillis) { 297 checkNotNull(mRotator, "missing FileRotator"); 298 final long pendingBytes = mPending.getTotalBytes(); 299 if (pendingBytes >= mPersistThresholdBytes) { 300 forcePersistLocked(currentTimeMillis); 301 } else { 302 mRotator.maybeRotate(currentTimeMillis); 303 } 304 } 305 306 /** 307 * Force persisting any pending deltas. 308 */ forcePersistLocked(long currentTimeMillis)309 public void forcePersistLocked(long currentTimeMillis) { 310 checkNotNull(mRotator, "missing FileRotator"); 311 if (mPending.isDirty()) { 312 if (LOGD) Slog.d(TAG, "forcePersistLocked() writing for " + mCookie); 313 try { 314 mRotator.rewriteActive(mPendingRewriter, currentTimeMillis); 315 mRotator.maybeRotate(currentTimeMillis); 316 mPending.reset(); 317 } catch (IOException e) { 318 Log.wtf(TAG, "problem persisting pending stats", e); 319 recoverFromWtf(); 320 } catch (OutOfMemoryError e) { 321 Log.wtf(TAG, "problem persisting pending stats", e); 322 recoverFromWtf(); 323 } 324 } 325 } 326 327 /** 328 * Remove the given UID from all {@link FileRotator} history, migrating it 329 * to {@link TrafficStats#UID_REMOVED}. 330 */ removeUidsLocked(int[] uids)331 public void removeUidsLocked(int[] uids) { 332 if (mRotator != null) { 333 try { 334 // Rewrite all persisted data to migrate UID stats 335 mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uids)); 336 } catch (IOException e) { 337 Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e); 338 recoverFromWtf(); 339 } catch (OutOfMemoryError e) { 340 Log.wtf(TAG, "problem removing UIDs " + Arrays.toString(uids), e); 341 recoverFromWtf(); 342 } 343 } 344 345 // Remove any pending stats 346 if (mPending != null) { 347 mPending.removeUids(uids); 348 } 349 if (mSinceBoot != null) { 350 mSinceBoot.removeUids(uids); 351 } 352 353 // Clear UID from current stats snapshot 354 if (mLastSnapshot != null) { 355 mLastSnapshot = mLastSnapshot.withoutUids(uids); 356 } 357 358 final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null; 359 if (complete != null) { 360 complete.removeUids(uids); 361 } 362 } 363 364 /** 365 * Rewriter that will combine current {@link NetworkStatsCollection} values 366 * with anything read from disk, and write combined set to disk. Clears the 367 * original {@link NetworkStatsCollection} when finished writing. 368 */ 369 private static class CombiningRewriter implements FileRotator.Rewriter { 370 private final NetworkStatsCollection mCollection; 371 CombiningRewriter(NetworkStatsCollection collection)372 public CombiningRewriter(NetworkStatsCollection collection) { 373 mCollection = checkNotNull(collection, "missing NetworkStatsCollection"); 374 } 375 376 @Override reset()377 public void reset() { 378 // ignored 379 } 380 381 @Override read(InputStream in)382 public void read(InputStream in) throws IOException { 383 mCollection.read(in); 384 } 385 386 @Override shouldWrite()387 public boolean shouldWrite() { 388 return true; 389 } 390 391 @Override write(OutputStream out)392 public void write(OutputStream out) throws IOException { 393 mCollection.write(new DataOutputStream(out)); 394 mCollection.reset(); 395 } 396 } 397 398 /** 399 * Rewriter that will remove any {@link NetworkStatsHistory} attributed to 400 * the requested UID, only writing data back when modified. 401 */ 402 public static class RemoveUidRewriter implements FileRotator.Rewriter { 403 private final NetworkStatsCollection mTemp; 404 private final int[] mUids; 405 RemoveUidRewriter(long bucketDuration, int[] uids)406 public RemoveUidRewriter(long bucketDuration, int[] uids) { 407 mTemp = new NetworkStatsCollection(bucketDuration); 408 mUids = uids; 409 } 410 411 @Override reset()412 public void reset() { 413 mTemp.reset(); 414 } 415 416 @Override read(InputStream in)417 public void read(InputStream in) throws IOException { 418 mTemp.read(in); 419 mTemp.clearDirty(); 420 mTemp.removeUids(mUids); 421 } 422 423 @Override shouldWrite()424 public boolean shouldWrite() { 425 return mTemp.isDirty(); 426 } 427 428 @Override write(OutputStream out)429 public void write(OutputStream out) throws IOException { 430 mTemp.write(new DataOutputStream(out)); 431 } 432 } 433 importLegacyNetworkLocked(File file)434 public void importLegacyNetworkLocked(File file) throws IOException { 435 checkNotNull(mRotator, "missing FileRotator"); 436 437 // legacy file still exists; start empty to avoid double importing 438 mRotator.deleteAll(); 439 440 final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration); 441 collection.readLegacyNetwork(file); 442 443 final long startMillis = collection.getStartMillis(); 444 final long endMillis = collection.getEndMillis(); 445 446 if (!collection.isEmpty()) { 447 // process legacy data, creating active file at starting time, then 448 // using end time to possibly trigger rotation. 449 mRotator.rewriteActive(new CombiningRewriter(collection), startMillis); 450 mRotator.maybeRotate(endMillis); 451 } 452 } 453 importLegacyUidLocked(File file)454 public void importLegacyUidLocked(File file) throws IOException { 455 checkNotNull(mRotator, "missing FileRotator"); 456 457 // legacy file still exists; start empty to avoid double importing 458 mRotator.deleteAll(); 459 460 final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration); 461 collection.readLegacyUid(file, mOnlyTags); 462 463 final long startMillis = collection.getStartMillis(); 464 final long endMillis = collection.getEndMillis(); 465 466 if (!collection.isEmpty()) { 467 // process legacy data, creating active file at starting time, then 468 // using end time to possibly trigger rotation. 469 mRotator.rewriteActive(new CombiningRewriter(collection), startMillis); 470 mRotator.maybeRotate(endMillis); 471 } 472 } 473 dumpLocked(IndentingPrintWriter pw, boolean fullHistory)474 public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) { 475 if (mPending != null) { 476 pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes()); 477 } 478 if (fullHistory) { 479 pw.println("Complete history:"); 480 getOrLoadCompleteLocked().dump(pw); 481 } else { 482 pw.println("History since boot:"); 483 mSinceBoot.dump(pw); 484 } 485 } 486 writeToProtoLocked(ProtoOutputStream proto, long tag)487 public void writeToProtoLocked(ProtoOutputStream proto, long tag) { 488 final long start = proto.start(tag); 489 if (mPending != null) { 490 proto.write(NetworkStatsRecorderProto.PENDING_TOTAL_BYTES, mPending.getTotalBytes()); 491 } 492 getOrLoadCompleteLocked().writeToProto(proto, NetworkStatsRecorderProto.COMPLETE_HISTORY); 493 proto.end(start); 494 } 495 dumpCheckin(PrintWriter pw, long start, long end)496 public void dumpCheckin(PrintWriter pw, long start, long end) { 497 // Only load and dump stats from the requested window 498 getOrLoadPartialLocked(start, end).dumpCheckin(pw, start, end); 499 } 500 501 /** 502 * Recover from {@link FileRotator} failure by dumping state to 503 * {@link DropBoxManager} and deleting contents. 504 */ recoverFromWtf()505 private void recoverFromWtf() { 506 if (DUMP_BEFORE_DELETE) { 507 final ByteArrayOutputStream os = new ByteArrayOutputStream(); 508 try { 509 mRotator.dumpAll(os); 510 } catch (IOException e) { 511 // ignore partial contents 512 os.reset(); 513 } finally { 514 IoUtils.closeQuietly(os); 515 } 516 mDropBox.addData(TAG_NETSTATS_DUMP, os.toByteArray(), 0); 517 } 518 519 mRotator.deleteAll(); 520 } 521 } 522