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