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