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