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