NetworkStatsRecorder.java revision e7bb71d26943fbb053139e1e34203df4c2afaa9b
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        // assume first snapshot is bootstrap and don't record
132        if (mLastSnapshot == null) {
133            mLastSnapshot = snapshot;
134            return;
135        }
136
137        final NetworkStatsCollection complete = mComplete != null ? mComplete.get() : null;
138
139        final NetworkStats delta = NetworkStats.subtract(
140                snapshot, mLastSnapshot, mObserver, mCookie);
141        final long end = currentTimeMillis;
142        final long start = end - delta.getElapsedRealtime();
143
144        NetworkStats.Entry entry = null;
145        for (int i = 0; i < delta.size(); i++) {
146            entry = delta.getValues(i, entry);
147            final NetworkIdentitySet ident = ifaceIdent.get(entry.iface);
148            if (ident == null) {
149                unknownIfaces.add(entry.iface);
150                continue;
151            }
152
153            // skip when no delta occured
154            if (entry.isEmpty()) continue;
155
156            // only record tag data when requested
157            if ((entry.tag == TAG_NONE) != mOnlyTags) {
158                mPending.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
159
160                // also record against boot stats when present
161                if (mSinceBoot != null) {
162                    mSinceBoot.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
163                }
164
165                // also record against complete dataset when present
166                if (complete != null) {
167                    complete.recordData(ident, entry.uid, entry.set, entry.tag, start, end, entry);
168                }
169            }
170        }
171
172        mLastSnapshot = snapshot;
173
174        if (LOGV && unknownIfaces.size() > 0) {
175            Slog.w(TAG, "unknown interfaces " + unknownIfaces + ", ignoring those stats");
176        }
177    }
178
179    /**
180     * Consider persisting any pending deltas, if they are beyond
181     * {@link #mPersistThresholdBytes}.
182     */
183    public void maybePersistLocked(long currentTimeMillis) {
184        final long pendingBytes = mPending.getTotalBytes();
185        if (pendingBytes >= mPersistThresholdBytes) {
186            forcePersistLocked(currentTimeMillis);
187        } else {
188            mRotator.maybeRotate(currentTimeMillis);
189        }
190    }
191
192    /**
193     * Force persisting any pending deltas.
194     */
195    public void forcePersistLocked(long currentTimeMillis) {
196        if (mPending.isDirty()) {
197            if (LOGD) Slog.d(TAG, "forcePersistLocked() writing for " + mCookie);
198            try {
199                mRotator.rewriteActive(mPendingRewriter, currentTimeMillis);
200                mRotator.maybeRotate(currentTimeMillis);
201                mPending.reset();
202            } catch (IOException e) {
203                Log.wtf(TAG, "problem persisting pending stats", e);
204            }
205        }
206    }
207
208    /**
209     * Remove the given UID from all {@link FileRotator} history, migrating it
210     * to {@link TrafficStats#UID_REMOVED}.
211     */
212    public void removeUidLocked(int uid) {
213        try {
214            // process all existing data to migrate uid
215            mRotator.rewriteAll(new RemoveUidRewriter(mBucketDuration, uid));
216        } catch (IOException e) {
217            Log.wtf(TAG, "problem removing UID " + uid, e);
218        }
219
220        // clear UID from current stats snapshot
221        if (mLastSnapshot != null) {
222            mLastSnapshot = mLastSnapshot.withoutUid(uid);
223        }
224    }
225
226    /**
227     * Rewriter that will combine current {@link NetworkStatsCollection} values
228     * with anything read from disk, and write combined set to disk. Clears the
229     * original {@link NetworkStatsCollection} when finished writing.
230     */
231    private static class CombiningRewriter implements FileRotator.Rewriter {
232        private final NetworkStatsCollection mCollection;
233
234        public CombiningRewriter(NetworkStatsCollection collection) {
235            mCollection = checkNotNull(collection, "missing NetworkStatsCollection");
236        }
237
238        /** {@inheritDoc} */
239        public void reset() {
240            // ignored
241        }
242
243        /** {@inheritDoc} */
244        public void read(InputStream in) throws IOException {
245            mCollection.read(in);
246        }
247
248        /** {@inheritDoc} */
249        public boolean shouldWrite() {
250            return true;
251        }
252
253        /** {@inheritDoc} */
254        public void write(OutputStream out) throws IOException {
255            mCollection.write(new DataOutputStream(out));
256            mCollection.reset();
257        }
258    }
259
260    /**
261     * Rewriter that will remove any {@link NetworkStatsHistory} attributed to
262     * the requested UID, only writing data back when modified.
263     */
264    public static class RemoveUidRewriter implements FileRotator.Rewriter {
265        private final NetworkStatsCollection mTemp;
266        private final int mUid;
267
268        public RemoveUidRewriter(long bucketDuration, int uid) {
269            mTemp = new NetworkStatsCollection(bucketDuration);
270            mUid = uid;
271        }
272
273        /** {@inheritDoc} */
274        public void reset() {
275            mTemp.reset();
276        }
277
278        /** {@inheritDoc} */
279        public void read(InputStream in) throws IOException {
280            mTemp.read(in);
281            mTemp.clearDirty();
282            mTemp.removeUid(mUid);
283        }
284
285        /** {@inheritDoc} */
286        public boolean shouldWrite() {
287            return mTemp.isDirty();
288        }
289
290        /** {@inheritDoc} */
291        public void write(OutputStream out) throws IOException {
292            mTemp.write(new DataOutputStream(out));
293        }
294    }
295
296    public void importLegacyNetworkLocked(File file) throws IOException {
297        // legacy file still exists; start empty to avoid double importing
298        mRotator.deleteAll();
299
300        final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
301        collection.readLegacyNetwork(file);
302
303        final long startMillis = collection.getStartMillis();
304        final long endMillis = collection.getEndMillis();
305
306        if (!collection.isEmpty()) {
307            // process legacy data, creating active file at starting time, then
308            // using end time to possibly trigger rotation.
309            mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
310            mRotator.maybeRotate(endMillis);
311        }
312    }
313
314    public void importLegacyUidLocked(File file) throws IOException {
315        // legacy file still exists; start empty to avoid double importing
316        mRotator.deleteAll();
317
318        final NetworkStatsCollection collection = new NetworkStatsCollection(mBucketDuration);
319        collection.readLegacyUid(file, mOnlyTags);
320
321        final long startMillis = collection.getStartMillis();
322        final long endMillis = collection.getEndMillis();
323
324        if (!collection.isEmpty()) {
325            // process legacy data, creating active file at starting time, then
326            // using end time to possibly trigger rotation.
327            mRotator.rewriteActive(new CombiningRewriter(collection), startMillis);
328            mRotator.maybeRotate(endMillis);
329        }
330    }
331
332    public void dumpLocked(IndentingPrintWriter pw, boolean fullHistory) {
333        pw.print("Pending bytes: "); pw.println(mPending.getTotalBytes());
334        if (fullHistory) {
335            pw.println("Complete history:");
336            getOrLoadCompleteLocked().dump(pw);
337        } else {
338            pw.println("History since boot:");
339            mSinceBoot.dump(pw);
340        }
341    }
342}
343