NetworkStatsCollection.java revision 39606a007a5b1309dd000234f2b8cf156c49fd0f
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.IFACE_ALL;
20import static android.net.NetworkStats.SET_ALL;
21import static android.net.NetworkStats.SET_DEFAULT;
22import static android.net.NetworkStats.TAG_NONE;
23import static android.net.NetworkStats.UID_ALL;
24import static android.net.TrafficStats.UID_REMOVED;
25
26import android.net.NetworkIdentity;
27import android.net.NetworkStats;
28import android.net.NetworkStatsHistory;
29import android.net.NetworkTemplate;
30import android.net.TrafficStats;
31import android.text.format.DateUtils;
32import android.util.AtomicFile;
33
34import com.android.internal.util.FileRotator;
35import com.android.internal.util.IndentingPrintWriter;
36import com.android.internal.util.Objects;
37import com.google.android.collect.Lists;
38import com.google.android.collect.Maps;
39
40import java.io.BufferedInputStream;
41import java.io.DataInputStream;
42import java.io.DataOutputStream;
43import java.io.File;
44import java.io.FileNotFoundException;
45import java.io.IOException;
46import java.io.InputStream;
47import java.net.ProtocolException;
48import java.util.ArrayList;
49import java.util.Collections;
50import java.util.HashMap;
51import java.util.Map;
52
53import libcore.io.IoUtils;
54
55/**
56 * Collection of {@link NetworkStatsHistory}, stored based on combined key of
57 * {@link NetworkIdentitySet}, UID, set, and tag. Knows how to persist itself.
58 */
59public class NetworkStatsCollection implements FileRotator.Reader {
60    /** File header magic number: "ANET" */
61    private static final int FILE_MAGIC = 0x414E4554;
62
63    private static final int VERSION_NETWORK_INIT = 1;
64
65    private static final int VERSION_UID_INIT = 1;
66    private static final int VERSION_UID_WITH_IDENT = 2;
67    private static final int VERSION_UID_WITH_TAG = 3;
68    private static final int VERSION_UID_WITH_SET = 4;
69
70    private static final int VERSION_UNIFIED_INIT = 16;
71
72    private HashMap<Key, NetworkStatsHistory> mStats = Maps.newHashMap();
73
74    private final long mBucketDuration;
75
76    private long mStartMillis;
77    private long mEndMillis;
78    private long mTotalBytes;
79    private boolean mDirty;
80
81    public NetworkStatsCollection(long bucketDuration) {
82        mBucketDuration = bucketDuration;
83        reset();
84    }
85
86    public void reset() {
87        mStats.clear();
88        mStartMillis = Long.MAX_VALUE;
89        mEndMillis = Long.MIN_VALUE;
90        mTotalBytes = 0;
91        mDirty = false;
92    }
93
94    public long getStartMillis() {
95        return mStartMillis;
96    }
97
98    /**
99     * Return first atomic bucket in this collection, which is more conservative
100     * than {@link #mStartMillis}.
101     */
102    public long getFirstAtomicBucketMillis() {
103        if (mStartMillis == Long.MAX_VALUE) {
104            return Long.MAX_VALUE;
105        } else {
106            return mStartMillis + mBucketDuration;
107        }
108    }
109
110    public long getEndMillis() {
111        return mEndMillis;
112    }
113
114    public long getTotalBytes() {
115        return mTotalBytes;
116    }
117
118    public boolean isDirty() {
119        return mDirty;
120    }
121
122    public void clearDirty() {
123        mDirty = false;
124    }
125
126    public boolean isEmpty() {
127        return mStartMillis == Long.MAX_VALUE && mEndMillis == Long.MIN_VALUE;
128    }
129
130    /**
131     * Combine all {@link NetworkStatsHistory} in this collection which match
132     * the requested parameters.
133     */
134    public NetworkStatsHistory getHistory(
135            NetworkTemplate template, int uid, int set, int tag, int fields) {
136        return getHistory(template, uid, set, tag, fields, Long.MIN_VALUE, Long.MAX_VALUE);
137    }
138
139    /**
140     * Combine all {@link NetworkStatsHistory} in this collection which match
141     * the requested parameters.
142     */
143    public NetworkStatsHistory getHistory(
144            NetworkTemplate template, int uid, int set, int tag, int fields, long start, long end) {
145        final NetworkStatsHistory combined = new NetworkStatsHistory(
146                mBucketDuration, estimateBuckets(), fields);
147        for (Map.Entry<Key, NetworkStatsHistory> entry : mStats.entrySet()) {
148            final Key key = entry.getKey();
149            final boolean setMatches = set == SET_ALL || key.set == set;
150            if (key.uid == uid && setMatches && key.tag == tag
151                    && templateMatches(template, key.ident)) {
152                combined.recordHistory(entry.getValue(), start, end);
153            }
154        }
155        return combined;
156    }
157
158    /**
159     * Summarize all {@link NetworkStatsHistory} in this collection which match
160     * the requested parameters.
161     */
162    public NetworkStats getSummary(NetworkTemplate template, long start, long end) {
163        final long now = System.currentTimeMillis();
164
165        final NetworkStats stats = new NetworkStats(end - start, 24);
166        final NetworkStats.Entry entry = new NetworkStats.Entry();
167        NetworkStatsHistory.Entry historyEntry = null;
168
169        // shortcut when we know stats will be empty
170        if (start == end) return stats;
171
172        for (Map.Entry<Key, NetworkStatsHistory> mapEntry : mStats.entrySet()) {
173            final Key key = mapEntry.getKey();
174            if (templateMatches(template, key.ident)) {
175                final NetworkStatsHistory history = mapEntry.getValue();
176                historyEntry = history.getValues(start, end, now, historyEntry);
177
178                entry.iface = IFACE_ALL;
179                entry.uid = key.uid;
180                entry.set = key.set;
181                entry.tag = key.tag;
182                entry.rxBytes = historyEntry.rxBytes;
183                entry.rxPackets = historyEntry.rxPackets;
184                entry.txBytes = historyEntry.txBytes;
185                entry.txPackets = historyEntry.txPackets;
186                entry.operations = historyEntry.operations;
187
188                if (!entry.isEmpty()) {
189                    stats.combineValues(entry);
190                }
191            }
192        }
193
194        return stats;
195    }
196
197    /**
198     * Record given {@link android.net.NetworkStats.Entry} into this collection.
199     */
200    public void recordData(NetworkIdentitySet ident, int uid, int set, int tag, long start,
201            long end, NetworkStats.Entry entry) {
202        final NetworkStatsHistory history = findOrCreateHistory(ident, uid, set, tag);
203        history.recordData(start, end, entry);
204        noteRecordedHistory(history.getStart(), history.getEnd(), entry.rxBytes + entry.txBytes);
205    }
206
207    /**
208     * Record given {@link NetworkStatsHistory} into this collection.
209     */
210    private void recordHistory(Key key, NetworkStatsHistory history) {
211        if (history.size() == 0) return;
212        noteRecordedHistory(history.getStart(), history.getEnd(), history.getTotalBytes());
213
214        NetworkStatsHistory target = mStats.get(key);
215        if (target == null) {
216            target = new NetworkStatsHistory(history.getBucketDuration());
217            mStats.put(key, target);
218        }
219        target.recordEntireHistory(history);
220    }
221
222    /**
223     * Record all {@link NetworkStatsHistory} contained in the given collection
224     * into this collection.
225     */
226    public void recordCollection(NetworkStatsCollection another) {
227        for (Map.Entry<Key, NetworkStatsHistory> entry : another.mStats.entrySet()) {
228            recordHistory(entry.getKey(), entry.getValue());
229        }
230    }
231
232    private NetworkStatsHistory findOrCreateHistory(
233            NetworkIdentitySet ident, int uid, int set, int tag) {
234        final Key key = new Key(ident, uid, set, tag);
235        final NetworkStatsHistory existing = mStats.get(key);
236
237        // update when no existing, or when bucket duration changed
238        NetworkStatsHistory updated = null;
239        if (existing == null) {
240            updated = new NetworkStatsHistory(mBucketDuration, 10);
241        } else if (existing.getBucketDuration() != mBucketDuration) {
242            updated = new NetworkStatsHistory(existing, mBucketDuration);
243        }
244
245        if (updated != null) {
246            mStats.put(key, updated);
247            return updated;
248        } else {
249            return existing;
250        }
251    }
252
253    @Override
254    public void read(InputStream in) throws IOException {
255        read(new DataInputStream(in));
256    }
257
258    public void read(DataInputStream in) throws IOException {
259        // verify file magic header intact
260        final int magic = in.readInt();
261        if (magic != FILE_MAGIC) {
262            throw new ProtocolException("unexpected magic: " + magic);
263        }
264
265        final int version = in.readInt();
266        switch (version) {
267            case VERSION_UNIFIED_INIT: {
268                // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
269                final int identSize = in.readInt();
270                for (int i = 0; i < identSize; i++) {
271                    final NetworkIdentitySet ident = new NetworkIdentitySet(in);
272
273                    final int size = in.readInt();
274                    for (int j = 0; j < size; j++) {
275                        final int uid = in.readInt();
276                        final int set = in.readInt();
277                        final int tag = in.readInt();
278
279                        final Key key = new Key(ident, uid, set, tag);
280                        final NetworkStatsHistory history = new NetworkStatsHistory(in);
281                        recordHistory(key, history);
282                    }
283                }
284                break;
285            }
286            default: {
287                throw new ProtocolException("unexpected version: " + version);
288            }
289        }
290    }
291
292    public void write(DataOutputStream out) throws IOException {
293        // cluster key lists grouped by ident
294        final HashMap<NetworkIdentitySet, ArrayList<Key>> keysByIdent = Maps.newHashMap();
295        for (Key key : mStats.keySet()) {
296            ArrayList<Key> keys = keysByIdent.get(key.ident);
297            if (keys == null) {
298                keys = Lists.newArrayList();
299                keysByIdent.put(key.ident, keys);
300            }
301            keys.add(key);
302        }
303
304        out.writeInt(FILE_MAGIC);
305        out.writeInt(VERSION_UNIFIED_INIT);
306
307        out.writeInt(keysByIdent.size());
308        for (NetworkIdentitySet ident : keysByIdent.keySet()) {
309            final ArrayList<Key> keys = keysByIdent.get(ident);
310            ident.writeToStream(out);
311
312            out.writeInt(keys.size());
313            for (Key key : keys) {
314                final NetworkStatsHistory history = mStats.get(key);
315                out.writeInt(key.uid);
316                out.writeInt(key.set);
317                out.writeInt(key.tag);
318                history.writeToStream(out);
319            }
320        }
321
322        out.flush();
323    }
324
325    @Deprecated
326    public void readLegacyNetwork(File file) throws IOException {
327        final AtomicFile inputFile = new AtomicFile(file);
328
329        DataInputStream in = null;
330        try {
331            in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
332
333            // verify file magic header intact
334            final int magic = in.readInt();
335            if (magic != FILE_MAGIC) {
336                throw new ProtocolException("unexpected magic: " + magic);
337            }
338
339            final int version = in.readInt();
340            switch (version) {
341                case VERSION_NETWORK_INIT: {
342                    // network := size *(NetworkIdentitySet NetworkStatsHistory)
343                    final int size = in.readInt();
344                    for (int i = 0; i < size; i++) {
345                        final NetworkIdentitySet ident = new NetworkIdentitySet(in);
346                        final NetworkStatsHistory history = new NetworkStatsHistory(in);
347
348                        final Key key = new Key(ident, UID_ALL, SET_ALL, TAG_NONE);
349                        recordHistory(key, history);
350                    }
351                    break;
352                }
353                default: {
354                    throw new ProtocolException("unexpected version: " + version);
355                }
356            }
357        } catch (FileNotFoundException e) {
358            // missing stats is okay, probably first boot
359        } finally {
360            IoUtils.closeQuietly(in);
361        }
362    }
363
364    @Deprecated
365    public void readLegacyUid(File file, boolean onlyTags) throws IOException {
366        final AtomicFile inputFile = new AtomicFile(file);
367
368        DataInputStream in = null;
369        try {
370            in = new DataInputStream(new BufferedInputStream(inputFile.openRead()));
371
372            // verify file magic header intact
373            final int magic = in.readInt();
374            if (magic != FILE_MAGIC) {
375                throw new ProtocolException("unexpected magic: " + magic);
376            }
377
378            final int version = in.readInt();
379            switch (version) {
380                case VERSION_UID_INIT: {
381                    // uid := size *(UID NetworkStatsHistory)
382
383                    // drop this data version, since we don't have a good
384                    // mapping into NetworkIdentitySet.
385                    break;
386                }
387                case VERSION_UID_WITH_IDENT: {
388                    // uid := size *(NetworkIdentitySet size *(UID NetworkStatsHistory))
389
390                    // drop this data version, since this version only existed
391                    // for a short time.
392                    break;
393                }
394                case VERSION_UID_WITH_TAG:
395                case VERSION_UID_WITH_SET: {
396                    // uid := size *(NetworkIdentitySet size *(uid set tag NetworkStatsHistory))
397                    final int identSize = in.readInt();
398                    for (int i = 0; i < identSize; i++) {
399                        final NetworkIdentitySet ident = new NetworkIdentitySet(in);
400
401                        final int size = in.readInt();
402                        for (int j = 0; j < size; j++) {
403                            final int uid = in.readInt();
404                            final int set = (version >= VERSION_UID_WITH_SET) ? in.readInt()
405                                    : SET_DEFAULT;
406                            final int tag = in.readInt();
407
408                            final Key key = new Key(ident, uid, set, tag);
409                            final NetworkStatsHistory history = new NetworkStatsHistory(in);
410
411                            if ((tag == TAG_NONE) != onlyTags) {
412                                recordHistory(key, history);
413                            }
414                        }
415                    }
416                    break;
417                }
418                default: {
419                    throw new ProtocolException("unexpected version: " + version);
420                }
421            }
422        } catch (FileNotFoundException e) {
423            // missing stats is okay, probably first boot
424        } finally {
425            IoUtils.closeQuietly(in);
426        }
427    }
428
429    /**
430     * Remove any {@link NetworkStatsHistory} attributed to the requested UID,
431     * moving any {@link NetworkStats#TAG_NONE} series to
432     * {@link TrafficStats#UID_REMOVED}.
433     */
434    public void removeUid(int uid) {
435        final ArrayList<Key> knownKeys = Lists.newArrayList();
436        knownKeys.addAll(mStats.keySet());
437
438        // migrate all UID stats into special "removed" bucket
439        for (Key key : knownKeys) {
440            if (key.uid == uid) {
441                // only migrate combined TAG_NONE history
442                if (key.tag == TAG_NONE) {
443                    final NetworkStatsHistory uidHistory = mStats.get(key);
444                    final NetworkStatsHistory removedHistory = findOrCreateHistory(
445                            key.ident, UID_REMOVED, SET_DEFAULT, TAG_NONE);
446                    removedHistory.recordEntireHistory(uidHistory);
447                }
448                mStats.remove(key);
449                mDirty = true;
450            }
451        }
452    }
453
454    private void noteRecordedHistory(long startMillis, long endMillis, long totalBytes) {
455        if (startMillis < mStartMillis) mStartMillis = startMillis;
456        if (endMillis > mEndMillis) mEndMillis = endMillis;
457        mTotalBytes += totalBytes;
458        mDirty = true;
459    }
460
461    private int estimateBuckets() {
462        return (int) (Math.min(mEndMillis - mStartMillis, DateUtils.WEEK_IN_MILLIS * 5)
463                / mBucketDuration);
464    }
465
466    public void dump(IndentingPrintWriter pw) {
467        final ArrayList<Key> keys = Lists.newArrayList();
468        keys.addAll(mStats.keySet());
469        Collections.sort(keys);
470
471        for (Key key : keys) {
472            pw.print("ident="); pw.print(key.ident.toString());
473            pw.print(" uid="); pw.print(key.uid);
474            pw.print(" set="); pw.print(NetworkStats.setToString(key.set));
475            pw.print(" tag="); pw.println(NetworkStats.tagToString(key.tag));
476
477            final NetworkStatsHistory history = mStats.get(key);
478            pw.increaseIndent();
479            history.dump(pw, true);
480            pw.decreaseIndent();
481        }
482    }
483
484    /**
485     * Test if given {@link NetworkTemplate} matches any {@link NetworkIdentity}
486     * in the given {@link NetworkIdentitySet}.
487     */
488    private static boolean templateMatches(NetworkTemplate template, NetworkIdentitySet identSet) {
489        for (NetworkIdentity ident : identSet) {
490            if (template.matches(ident)) {
491                return true;
492            }
493        }
494        return false;
495    }
496
497    private static class Key implements Comparable<Key> {
498        public final NetworkIdentitySet ident;
499        public final int uid;
500        public final int set;
501        public final int tag;
502
503        private final int hashCode;
504
505        public Key(NetworkIdentitySet ident, int uid, int set, int tag) {
506            this.ident = ident;
507            this.uid = uid;
508            this.set = set;
509            this.tag = tag;
510            hashCode = Objects.hashCode(ident, uid, set, tag);
511        }
512
513        @Override
514        public int hashCode() {
515            return hashCode;
516        }
517
518        @Override
519        public boolean equals(Object obj) {
520            if (obj instanceof Key) {
521                final Key key = (Key) obj;
522                return uid == key.uid && set == key.set && tag == key.tag
523                        && Objects.equal(ident, key.ident);
524            }
525            return false;
526        }
527
528        @Override
529        public int compareTo(Key another) {
530            return Integer.compare(uid, another.uid);
531        }
532    }
533}
534