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