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