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