NetworkStatsHistory.java revision 558a23200697d306b75750cf4612cf0717e73537
1/*
2 * Copyright (C) 2011 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 android.net;
18
19import static android.net.NetworkStats.IFACE_ALL;
20import static android.net.NetworkStats.SET_DEFAULT;
21import static android.net.NetworkStats.TAG_NONE;
22import static android.net.NetworkStats.UID_ALL;
23import static android.net.NetworkStatsHistory.DataStreamUtils.readFullLongArray;
24import static android.net.NetworkStatsHistory.DataStreamUtils.readVarLongArray;
25import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray;
26import static android.net.NetworkStatsHistory.Entry.UNKNOWN;
27import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray;
28import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray;
29
30import android.os.Parcel;
31import android.os.Parcelable;
32
33import java.io.CharArrayWriter;
34import java.io.DataInputStream;
35import java.io.DataOutputStream;
36import java.io.IOException;
37import java.io.PrintWriter;
38import java.net.ProtocolException;
39import java.util.Arrays;
40import java.util.Random;
41
42/**
43 * Collection of historical network statistics, recorded into equally-sized
44 * "buckets" in time. Internally it stores data in {@code long} series for more
45 * efficient persistence.
46 * <p>
47 * Each bucket is defined by a {@link #bucketStart} timestamp, and lasts for
48 * {@link #bucketDuration}. Internally assumes that {@link #bucketStart} is
49 * sorted at all times.
50 *
51 * @hide
52 */
53public class NetworkStatsHistory implements Parcelable {
54    private static final int VERSION_INIT = 1;
55    private static final int VERSION_ADD_PACKETS = 2;
56    private static final int VERSION_ADD_ACTIVE = 3;
57
58    public static final int FIELD_ACTIVE_TIME = 0x01;
59    public static final int FIELD_RX_BYTES = 0x02;
60    public static final int FIELD_RX_PACKETS = 0x04;
61    public static final int FIELD_TX_BYTES = 0x08;
62    public static final int FIELD_TX_PACKETS = 0x10;
63    public static final int FIELD_OPERATIONS = 0x20;
64
65    public static final int FIELD_ALL = 0xFFFFFFFF;
66
67    private long bucketDuration;
68    private int bucketCount;
69    private long[] bucketStart;
70    private long[] activeTime;
71    private long[] rxBytes;
72    private long[] rxPackets;
73    private long[] txBytes;
74    private long[] txPackets;
75    private long[] operations;
76
77    public static class Entry {
78        public static final long UNKNOWN = -1;
79
80        public long bucketDuration;
81        public long bucketStart;
82        public long activeTime;
83        public long rxBytes;
84        public long rxPackets;
85        public long txBytes;
86        public long txPackets;
87        public long operations;
88    }
89
90    public NetworkStatsHistory(long bucketDuration) {
91        this(bucketDuration, 10, FIELD_ALL);
92    }
93
94    public NetworkStatsHistory(long bucketDuration, int initialSize) {
95        this(bucketDuration, initialSize, FIELD_ALL);
96    }
97
98    public NetworkStatsHistory(long bucketDuration, int initialSize, int fields) {
99        this.bucketDuration = bucketDuration;
100        bucketStart = new long[initialSize];
101        if ((fields & FIELD_ACTIVE_TIME) != 0) activeTime = new long[initialSize];
102        if ((fields & FIELD_RX_BYTES) != 0) rxBytes = new long[initialSize];
103        if ((fields & FIELD_RX_PACKETS) != 0) rxPackets = new long[initialSize];
104        if ((fields & FIELD_TX_BYTES) != 0) txBytes = new long[initialSize];
105        if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize];
106        if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize];
107        bucketCount = 0;
108    }
109
110    public NetworkStatsHistory(Parcel in) {
111        bucketDuration = in.readLong();
112        bucketStart = readLongArray(in);
113        activeTime = readLongArray(in);
114        rxBytes = readLongArray(in);
115        rxPackets = readLongArray(in);
116        txBytes = readLongArray(in);
117        txPackets = readLongArray(in);
118        operations = readLongArray(in);
119        bucketCount = bucketStart.length;
120    }
121
122    /** {@inheritDoc} */
123    public void writeToParcel(Parcel out, int flags) {
124        out.writeLong(bucketDuration);
125        writeLongArray(out, bucketStart, bucketCount);
126        writeLongArray(out, activeTime, bucketCount);
127        writeLongArray(out, rxBytes, bucketCount);
128        writeLongArray(out, rxPackets, bucketCount);
129        writeLongArray(out, txBytes, bucketCount);
130        writeLongArray(out, txPackets, bucketCount);
131        writeLongArray(out, operations, bucketCount);
132    }
133
134    public NetworkStatsHistory(DataInputStream in) throws IOException {
135        final int version = in.readInt();
136        switch (version) {
137            case VERSION_INIT: {
138                bucketDuration = in.readLong();
139                bucketStart = readFullLongArray(in);
140                rxBytes = readFullLongArray(in);
141                rxPackets = new long[bucketStart.length];
142                txBytes = readFullLongArray(in);
143                txPackets = new long[bucketStart.length];
144                operations = new long[bucketStart.length];
145                bucketCount = bucketStart.length;
146                break;
147            }
148            case VERSION_ADD_PACKETS:
149            case VERSION_ADD_ACTIVE: {
150                bucketDuration = in.readLong();
151                bucketStart = readVarLongArray(in);
152                activeTime = (version >= VERSION_ADD_ACTIVE) ? readVarLongArray(in)
153                        : new long[bucketStart.length];
154                rxBytes = readVarLongArray(in);
155                rxPackets = readVarLongArray(in);
156                txBytes = readVarLongArray(in);
157                txPackets = readVarLongArray(in);
158                operations = readVarLongArray(in);
159                bucketCount = bucketStart.length;
160                break;
161            }
162            default: {
163                throw new ProtocolException("unexpected version: " + version);
164            }
165        }
166    }
167
168    public void writeToStream(DataOutputStream out) throws IOException {
169        out.writeInt(VERSION_ADD_ACTIVE);
170        out.writeLong(bucketDuration);
171        writeVarLongArray(out, bucketStart, bucketCount);
172        writeVarLongArray(out, activeTime, bucketCount);
173        writeVarLongArray(out, rxBytes, bucketCount);
174        writeVarLongArray(out, rxPackets, bucketCount);
175        writeVarLongArray(out, txBytes, bucketCount);
176        writeVarLongArray(out, txPackets, bucketCount);
177        writeVarLongArray(out, operations, bucketCount);
178    }
179
180    /** {@inheritDoc} */
181    public int describeContents() {
182        return 0;
183    }
184
185    public int size() {
186        return bucketCount;
187    }
188
189    public long getBucketDuration() {
190        return bucketDuration;
191    }
192
193    public long getStart() {
194        if (bucketCount > 0) {
195            return bucketStart[0];
196        } else {
197            return Long.MAX_VALUE;
198        }
199    }
200
201    public long getEnd() {
202        if (bucketCount > 0) {
203            return bucketStart[bucketCount - 1] + bucketDuration;
204        } else {
205            return Long.MIN_VALUE;
206        }
207    }
208
209    /**
210     * Return specific stats entry.
211     */
212    public Entry getValues(int i, Entry recycle) {
213        final Entry entry = recycle != null ? recycle : new Entry();
214        entry.bucketStart = bucketStart[i];
215        entry.bucketDuration = bucketDuration;
216        entry.activeTime = getLong(activeTime, i, UNKNOWN);
217        entry.rxBytes = getLong(rxBytes, i, UNKNOWN);
218        entry.rxPackets = getLong(rxPackets, i, UNKNOWN);
219        entry.txBytes = getLong(txBytes, i, UNKNOWN);
220        entry.txPackets = getLong(txPackets, i, UNKNOWN);
221        entry.operations = getLong(operations, i, UNKNOWN);
222        return entry;
223    }
224
225    /**
226     * Record that data traffic occurred in the given time range. Will
227     * distribute across internal buckets, creating new buckets as needed.
228     */
229    @Deprecated
230    public void recordData(long start, long end, long rxBytes, long txBytes) {
231        recordData(start, end, new NetworkStats.Entry(
232                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, 0L, txBytes, 0L, 0L));
233    }
234
235    /**
236     * Record that data traffic occurred in the given time range. Will
237     * distribute across internal buckets, creating new buckets as needed.
238     */
239    public void recordData(long start, long end, NetworkStats.Entry entry) {
240        if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0 || entry.txPackets < 0
241                || entry.operations < 0) {
242            throw new IllegalArgumentException("tried recording negative data");
243        }
244
245        // create any buckets needed by this range
246        ensureBuckets(start, end);
247
248        // distribute data usage into buckets
249        long duration = end - start;
250        for (int i = bucketCount - 1; i >= 0; i--) {
251            final long curStart = bucketStart[i];
252            final long curEnd = curStart + bucketDuration;
253
254            // bucket is older than record; we're finished
255            if (curEnd < start) break;
256            // bucket is newer than record; keep looking
257            if (curStart > end) continue;
258
259            final long overlap = Math.min(curEnd, end) - Math.max(curStart, start);
260            if (overlap <= 0) continue;
261
262            // integer math each time is faster than floating point
263            final long fracRxBytes = entry.rxBytes * overlap / duration;
264            final long fracRxPackets = entry.rxPackets * overlap / duration;
265            final long fracTxBytes = entry.txBytes * overlap / duration;
266            final long fracTxPackets = entry.txPackets * overlap / duration;
267            final long fracOperations = entry.operations * overlap / duration;
268
269            addLong(activeTime, i, overlap);
270            addLong(rxBytes, i, fracRxBytes); entry.rxBytes -= fracRxBytes;
271            addLong(rxPackets, i, fracRxPackets); entry.rxPackets -= fracRxPackets;
272            addLong(txBytes, i, fracTxBytes); entry.txBytes -= fracTxBytes;
273            addLong(txPackets, i, fracTxPackets); entry.txPackets -= fracTxPackets;
274            addLong(operations, i, fracOperations); entry.operations -= fracOperations;
275
276            duration -= overlap;
277        }
278    }
279
280    /**
281     * Record an entire {@link NetworkStatsHistory} into this history. Usually
282     * for combining together stats for external reporting.
283     */
284    public void recordEntireHistory(NetworkStatsHistory input) {
285        final NetworkStats.Entry entry = new NetworkStats.Entry(
286                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
287        for (int i = 0; i < input.bucketCount; i++) {
288            final long start = input.bucketStart[i];
289            final long end = start + input.bucketDuration;
290
291            entry.rxBytes = getLong(input.rxBytes, i, 0L);
292            entry.rxPackets = getLong(input.rxPackets, i, 0L);
293            entry.txBytes = getLong(input.txBytes, i, 0L);
294            entry.txPackets = getLong(input.txPackets, i, 0L);
295            entry.operations = getLong(input.operations, i, 0L);
296
297            recordData(start, end, entry);
298        }
299    }
300
301    /**
302     * Ensure that buckets exist for given time range, creating as needed.
303     */
304    private void ensureBuckets(long start, long end) {
305        // normalize incoming range to bucket boundaries
306        start -= start % bucketDuration;
307        end += (bucketDuration - (end % bucketDuration)) % bucketDuration;
308
309        for (long now = start; now < end; now += bucketDuration) {
310            // try finding existing bucket
311            final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now);
312            if (index < 0) {
313                // bucket missing, create and insert
314                insertBucket(~index, now);
315            }
316        }
317    }
318
319    /**
320     * Insert new bucket at requested index and starting time.
321     */
322    private void insertBucket(int index, long start) {
323        // create more buckets when needed
324        if (bucketCount >= bucketStart.length) {
325            final int newLength = Math.max(bucketStart.length, 10) * 3 / 2;
326            bucketStart = Arrays.copyOf(bucketStart, newLength);
327            if (activeTime != null) activeTime = Arrays.copyOf(activeTime, newLength);
328            if (rxBytes != null) rxBytes = Arrays.copyOf(rxBytes, newLength);
329            if (rxPackets != null) rxPackets = Arrays.copyOf(rxPackets, newLength);
330            if (txBytes != null) txBytes = Arrays.copyOf(txBytes, newLength);
331            if (txPackets != null) txPackets = Arrays.copyOf(txPackets, newLength);
332            if (operations != null) operations = Arrays.copyOf(operations, newLength);
333        }
334
335        // create gap when inserting bucket in middle
336        if (index < bucketCount) {
337            final int dstPos = index + 1;
338            final int length = bucketCount - index;
339
340            System.arraycopy(bucketStart, index, bucketStart, dstPos, length);
341            if (activeTime != null) System.arraycopy(activeTime, index, activeTime, dstPos, length);
342            if (rxBytes != null) System.arraycopy(rxBytes, index, rxBytes, dstPos, length);
343            if (rxPackets != null) System.arraycopy(rxPackets, index, rxPackets, dstPos, length);
344            if (txBytes != null) System.arraycopy(txBytes, index, txBytes, dstPos, length);
345            if (txPackets != null) System.arraycopy(txPackets, index, txPackets, dstPos, length);
346            if (operations != null) System.arraycopy(operations, index, operations, dstPos, length);
347        }
348
349        bucketStart[index] = start;
350        setLong(activeTime, index, 0L);
351        setLong(rxBytes, index, 0L);
352        setLong(rxPackets, index, 0L);
353        setLong(txBytes, index, 0L);
354        setLong(txPackets, index, 0L);
355        setLong(operations, index, 0L);
356        bucketCount++;
357    }
358
359    /**
360     * Remove buckets older than requested cutoff.
361     */
362    public void removeBucketsBefore(long cutoff) {
363        int i;
364        for (i = 0; i < bucketCount; i++) {
365            final long curStart = bucketStart[i];
366            final long curEnd = curStart + bucketDuration;
367
368            // cutoff happens before or during this bucket; everything before
369            // this bucket should be removed.
370            if (curEnd > cutoff) break;
371        }
372
373        if (i > 0) {
374            final int length = bucketStart.length;
375            bucketStart = Arrays.copyOfRange(bucketStart, i, length);
376            if (activeTime != null) activeTime = Arrays.copyOfRange(activeTime, i, length);
377            if (rxBytes != null) rxBytes = Arrays.copyOfRange(rxBytes, i, length);
378            if (rxPackets != null) rxPackets = Arrays.copyOfRange(rxPackets, i, length);
379            if (txBytes != null) txBytes = Arrays.copyOfRange(txBytes, i, length);
380            if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length);
381            if (operations != null) operations = Arrays.copyOfRange(operations, i, length);
382            bucketCount -= i;
383        }
384    }
385
386    /**
387     * Return interpolated data usage across the requested range. Interpolates
388     * across buckets, so values may be rounded slightly.
389     */
390    public Entry getValues(long start, long end, Entry recycle) {
391        return getValues(start, end, Long.MAX_VALUE, recycle);
392    }
393
394    /**
395     * Return interpolated data usage across the requested range. Interpolates
396     * across buckets, so values may be rounded slightly.
397     */
398    public Entry getValues(long start, long end, long now, Entry recycle) {
399        final Entry entry = recycle != null ? recycle : new Entry();
400        entry.bucketDuration = end - start;
401        entry.bucketStart = start;
402        entry.activeTime = activeTime != null ? 0 : UNKNOWN;
403        entry.rxBytes = rxBytes != null ? 0 : UNKNOWN;
404        entry.rxPackets = rxPackets != null ? 0 : UNKNOWN;
405        entry.txBytes = txBytes != null ? 0 : UNKNOWN;
406        entry.txPackets = txPackets != null ? 0 : UNKNOWN;
407        entry.operations = operations != null ? 0 : UNKNOWN;
408
409        for (int i = bucketCount - 1; i >= 0; i--) {
410            final long curStart = bucketStart[i];
411            final long curEnd = curStart + bucketDuration;
412
413            // bucket is older than record; we're finished
414            if (curEnd < start) break;
415            // bucket is newer than record; keep looking
416            if (curStart > end) continue;
417
418            // include full value for active buckets, otherwise only fractional
419            final boolean activeBucket = curStart < now && curEnd > now;
420            final long overlap = activeBucket ? bucketDuration
421                    : Math.min(curEnd, end) - Math.max(curStart, start);
422            if (overlap <= 0) continue;
423
424            // integer math each time is faster than floating point
425            if (activeTime != null) entry.activeTime += activeTime[i] * overlap / bucketDuration;
426            if (rxBytes != null) entry.rxBytes += rxBytes[i] * overlap / bucketDuration;
427            if (rxPackets != null) entry.rxPackets += rxPackets[i] * overlap / bucketDuration;
428            if (txBytes != null) entry.txBytes += txBytes[i] * overlap / bucketDuration;
429            if (txPackets != null) entry.txPackets += txPackets[i] * overlap / bucketDuration;
430            if (operations != null) entry.operations += operations[i] * overlap / bucketDuration;
431        }
432
433        return entry;
434    }
435
436    /**
437     * @deprecated only for temporary testing
438     */
439    @Deprecated
440    public void generateRandom(long start, long end, long rxBytes, long rxPackets, long txBytes,
441            long txPackets, long operations) {
442        ensureBuckets(start, end);
443
444        final NetworkStats.Entry entry = new NetworkStats.Entry(
445                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
446        final Random r = new Random();
447        while (rxBytes > 1024 || rxPackets > 128 || txBytes > 1024 || txPackets > 128
448                || operations > 32) {
449            final long curStart = randomLong(r, start, end);
450            final long curEnd = randomLong(r, curStart, end);
451
452            entry.rxBytes = randomLong(r, 0, rxBytes);
453            entry.rxPackets = randomLong(r, 0, rxPackets);
454            entry.txBytes = randomLong(r, 0, txBytes);
455            entry.txPackets = randomLong(r, 0, txPackets);
456            entry.operations = randomLong(r, 0, operations);
457
458            rxBytes -= entry.rxBytes;
459            rxPackets -= entry.rxPackets;
460            txBytes -= entry.txBytes;
461            txPackets -= entry.txPackets;
462            operations -= entry.operations;
463
464            recordData(curStart, curEnd, entry);
465        }
466    }
467
468    private static long randomLong(Random r, long start, long end) {
469        return (long) (start + (r.nextFloat() * (end - start)));
470    }
471
472    public void dump(String prefix, PrintWriter pw, boolean fullHistory) {
473        pw.print(prefix);
474        pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration);
475
476        final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32);
477        if (start > 0) {
478            pw.print(prefix);
479            pw.print("  (omitting "); pw.print(start); pw.println(" buckets)");
480        }
481
482        for (int i = start; i < bucketCount; i++) {
483            pw.print(prefix);
484            pw.print("  bucketStart="); pw.print(bucketStart[i]);
485            if (activeTime != null) pw.print(" activeTime="); pw.print(activeTime[i]);
486            if (rxBytes != null) pw.print(" rxBytes="); pw.print(rxBytes[i]);
487            if (rxPackets != null) pw.print(" rxPackets="); pw.print(rxPackets[i]);
488            if (txBytes != null) pw.print(" txBytes="); pw.print(txBytes[i]);
489            if (txPackets != null) pw.print(" txPackets="); pw.print(txPackets[i]);
490            if (operations != null) pw.print(" operations="); pw.print(operations[i]);
491            pw.println();
492        }
493    }
494
495    @Override
496    public String toString() {
497        final CharArrayWriter writer = new CharArrayWriter();
498        dump("", new PrintWriter(writer), false);
499        return writer.toString();
500    }
501
502    public static final Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
503        public NetworkStatsHistory createFromParcel(Parcel in) {
504            return new NetworkStatsHistory(in);
505        }
506
507        public NetworkStatsHistory[] newArray(int size) {
508            return new NetworkStatsHistory[size];
509        }
510    };
511
512    private static long getLong(long[] array, int i, long value) {
513        return array != null ? array[i] : value;
514    }
515
516    private static void setLong(long[] array, int i, long value) {
517        if (array != null) array[i] = value;
518    }
519
520    private static void addLong(long[] array, int i, long value) {
521        if (array != null) array[i] += value;
522    }
523
524    /**
525     * Utility methods for interacting with {@link DataInputStream} and
526     * {@link DataOutputStream}, mostly dealing with writing partial arrays.
527     */
528    public static class DataStreamUtils {
529        @Deprecated
530        public static long[] readFullLongArray(DataInputStream in) throws IOException {
531            final int size = in.readInt();
532            final long[] values = new long[size];
533            for (int i = 0; i < values.length; i++) {
534                values[i] = in.readLong();
535            }
536            return values;
537        }
538
539        /**
540         * Read variable-length {@link Long} using protobuf-style approach.
541         */
542        public static long readVarLong(DataInputStream in) throws IOException {
543            int shift = 0;
544            long result = 0;
545            while (shift < 64) {
546                byte b = in.readByte();
547                result |= (long) (b & 0x7F) << shift;
548                if ((b & 0x80) == 0)
549                    return result;
550                shift += 7;
551            }
552            throw new ProtocolException("malformed long");
553        }
554
555        /**
556         * Write variable-length {@link Long} using protobuf-style approach.
557         */
558        public static void writeVarLong(DataOutputStream out, long value) throws IOException {
559            while (true) {
560                if ((value & ~0x7FL) == 0) {
561                    out.writeByte((int) value);
562                    return;
563                } else {
564                    out.writeByte(((int) value & 0x7F) | 0x80);
565                    value >>>= 7;
566                }
567            }
568        }
569
570        public static long[] readVarLongArray(DataInputStream in) throws IOException {
571            final int size = in.readInt();
572            if (size == -1) return null;
573            final long[] values = new long[size];
574            for (int i = 0; i < values.length; i++) {
575                values[i] = readVarLong(in);
576            }
577            return values;
578        }
579
580        public static void writeVarLongArray(DataOutputStream out, long[] values, int size)
581                throws IOException {
582            if (values == null) {
583                out.writeInt(-1);
584                return;
585            }
586            if (size > values.length) {
587                throw new IllegalArgumentException("size larger than length");
588            }
589            out.writeInt(size);
590            for (int i = 0; i < size; i++) {
591                writeVarLong(out, values[i]);
592            }
593        }
594    }
595
596    /**
597     * Utility methods for interacting with {@link Parcel} structures, mostly
598     * dealing with writing partial arrays.
599     */
600    public static class ParcelUtils {
601        public static long[] readLongArray(Parcel in) {
602            final int size = in.readInt();
603            if (size == -1) return null;
604            final long[] values = new long[size];
605            for (int i = 0; i < values.length; i++) {
606                values[i] = in.readLong();
607            }
608            return values;
609        }
610
611        public static void writeLongArray(Parcel out, long[] values, int size) {
612            if (values == null) {
613                out.writeInt(-1);
614                return;
615            }
616            if (size > values.length) {
617                throw new IllegalArgumentException("size larger than length");
618            }
619            out.writeInt(size);
620            for (int i = 0; i < size; i++) {
621                out.writeLong(values[i]);
622            }
623        }
624    }
625
626}
627