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