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