NetworkStatsHistory.java revision 350083e36b9db6062e165954403ef921ff3dfdad
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 android.os.Parcel;
20import android.os.Parcelable;
21
22import java.io.CharArrayWriter;
23import java.io.DataInputStream;
24import java.io.DataOutputStream;
25import java.io.IOException;
26import java.io.PrintWriter;
27import java.net.ProtocolException;
28import java.util.Arrays;
29import java.util.Random;
30
31/**
32 * Collection of historical network statistics, recorded into equally-sized
33 * "buckets" in time. Internally it stores data in {@code long} series for more
34 * efficient persistence.
35 * <p>
36 * Each bucket is defined by a {@link #bucketStart} timestamp, and lasts for
37 * {@link #bucketDuration}. Internally assumes that {@link #bucketStart} is
38 * sorted at all times.
39 *
40 * @hide
41 */
42public class NetworkStatsHistory implements Parcelable {
43    private static final int VERSION_INIT = 1;
44
45    // TODO: teach about varint encoding to use less disk space
46
47    public final long bucketDuration;
48
49    public int bucketCount;
50    public long[] bucketStart;
51    public long[] rx;
52    public long[] tx;
53
54    public NetworkStatsHistory(long bucketDuration) {
55        this(bucketDuration, 10);
56    }
57
58    public NetworkStatsHistory(long bucketDuration, int initialSize) {
59        this.bucketDuration = bucketDuration;
60        bucketStart = new long[initialSize];
61        rx = new long[initialSize];
62        tx = new long[initialSize];
63        bucketCount = 0;
64    }
65
66    public NetworkStatsHistory(Parcel in) {
67        bucketDuration = in.readLong();
68        bucketStart = readLongArray(in);
69        rx = in.createLongArray();
70        tx = in.createLongArray();
71        bucketCount = bucketStart.length;
72    }
73
74    /** {@inheritDoc} */
75    public void writeToParcel(Parcel out, int flags) {
76        out.writeLong(bucketDuration);
77        writeLongArray(out, bucketStart, bucketCount);
78        writeLongArray(out, rx, bucketCount);
79        writeLongArray(out, tx, bucketCount);
80    }
81
82    public NetworkStatsHistory(DataInputStream in) throws IOException {
83        final int version = in.readInt();
84        switch (version) {
85            case VERSION_INIT: {
86                bucketDuration = in.readLong();
87                bucketStart = readLongArray(in);
88                rx = readLongArray(in);
89                tx = readLongArray(in);
90                bucketCount = bucketStart.length;
91                break;
92            }
93            default: {
94                throw new ProtocolException("unexpected version: " + version);
95            }
96        }
97    }
98
99    public void writeToStream(DataOutputStream out) throws IOException {
100        out.writeInt(VERSION_INIT);
101        out.writeLong(bucketDuration);
102        writeLongArray(out, bucketStart, bucketCount);
103        writeLongArray(out, rx, bucketCount);
104        writeLongArray(out, tx, bucketCount);
105    }
106
107    /** {@inheritDoc} */
108    public int describeContents() {
109        return 0;
110    }
111
112    /**
113     * Record that data traffic occurred in the given time range. Will
114     * distribute across internal buckets, creating new buckets as needed.
115     */
116    public void recordData(long start, long end, long rx, long tx) {
117        if (rx < 0 || tx < 0) {
118            throw new IllegalArgumentException(
119                    "tried recording negative data: rx=" + rx + ", tx=" + tx);
120        }
121
122        // create any buckets needed by this range
123        ensureBuckets(start, end);
124
125        // distribute data usage into buckets
126        final long duration = end - start;
127        for (int i = bucketCount - 1; i >= 0; i--) {
128            final long curStart = bucketStart[i];
129            final long curEnd = curStart + bucketDuration;
130
131            // bucket is older than record; we're finished
132            if (curEnd < start) break;
133            // bucket is newer than record; keep looking
134            if (curStart > end) continue;
135
136            final long overlap = Math.min(curEnd, end) - Math.max(curStart, start);
137            if (overlap > 0) {
138                this.rx[i] += rx * overlap / duration;
139                this.tx[i] += tx * overlap / duration;
140            }
141        }
142    }
143
144    /**
145     * Record an entire {@link NetworkStatsHistory} into this history. Usually
146     * for combining together stats for external reporting.
147     */
148    public void recordEntireHistory(NetworkStatsHistory input) {
149        for (int i = 0; i < input.bucketCount; i++) {
150            final long start = input.bucketStart[i];
151            final long end = start + input.bucketDuration;
152            recordData(start, end, input.rx[i], input.tx[i]);
153        }
154    }
155
156    /**
157     * Ensure that buckets exist for given time range, creating as needed.
158     */
159    private void ensureBuckets(long start, long end) {
160        // normalize incoming range to bucket boundaries
161        start -= start % bucketDuration;
162        end += (bucketDuration - (end % bucketDuration)) % bucketDuration;
163
164        for (long now = start; now < end; now += bucketDuration) {
165            // try finding existing bucket
166            final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now);
167            if (index < 0) {
168                // bucket missing, create and insert
169                insertBucket(~index, now);
170            }
171        }
172    }
173
174    /**
175     * Insert new bucket at requested index and starting time.
176     */
177    private void insertBucket(int index, long start) {
178        // create more buckets when needed
179        if (bucketCount >= bucketStart.length) {
180            final int newLength = Math.max(bucketStart.length, 10) * 3 / 2;
181            bucketStart = Arrays.copyOf(bucketStart, newLength);
182            rx = Arrays.copyOf(rx, newLength);
183            tx = Arrays.copyOf(tx, newLength);
184        }
185
186        // create gap when inserting bucket in middle
187        if (index < bucketCount) {
188            final int dstPos = index + 1;
189            final int length = bucketCount - index;
190
191            System.arraycopy(bucketStart, index, bucketStart, dstPos, length);
192            System.arraycopy(rx, index, rx, dstPos, length);
193            System.arraycopy(tx, index, tx, dstPos, length);
194        }
195
196        bucketStart[index] = start;
197        rx[index] = 0;
198        tx[index] = 0;
199        bucketCount++;
200    }
201
202    /**
203     * Remove buckets older than requested cutoff.
204     */
205    public void removeBucketsBefore(long cutoff) {
206        int i;
207        for (i = 0; i < bucketCount; i++) {
208            final long curStart = bucketStart[i];
209            final long curEnd = curStart + bucketDuration;
210
211            // cutoff happens before or during this bucket; everything before
212            // this bucket should be removed.
213            if (curEnd > cutoff) break;
214        }
215
216        if (i > 0) {
217            final int length = bucketStart.length;
218            bucketStart = Arrays.copyOfRange(bucketStart, i, length);
219            rx = Arrays.copyOfRange(rx, i, length);
220            tx = Arrays.copyOfRange(tx, i, length);
221            bucketCount -= i;
222        }
223    }
224
225    /**
226     * Return interpolated data usage across the requested range. Interpolates
227     * across buckets, so values may be rounded slightly.
228     */
229    public long[] getTotalData(long start, long end, long[] outTotal) {
230        long rx = 0;
231        long tx = 0;
232
233        for (int i = bucketCount - 1; i >= 0; i--) {
234            final long curStart = bucketStart[i];
235            final long curEnd = curStart + bucketDuration;
236
237            // bucket is older than record; we're finished
238            if (curEnd < start) break;
239            // bucket is newer than record; keep looking
240            if (curStart > end) continue;
241
242            final long overlap = Math.min(curEnd, end) - Math.max(curStart, start);
243            if (overlap > 0) {
244                rx += this.rx[i] * overlap / bucketDuration;
245                tx += this.tx[i] * overlap / bucketDuration;
246            }
247        }
248
249        if (outTotal == null || outTotal.length != 2) {
250            outTotal = new long[2];
251        }
252        outTotal[0] = rx;
253        outTotal[1] = tx;
254        return outTotal;
255    }
256
257    /**
258     * @deprecated only for temporary testing
259     */
260    @Deprecated
261    public void generateRandom(long start, long end, long rx, long tx) {
262        ensureBuckets(start, end);
263
264        final Random r = new Random();
265        while (rx > 1024 && tx > 1024) {
266            final long curStart = randomLong(r, start, end);
267            final long curEnd = randomLong(r, curStart, end);
268            final long curRx = randomLong(r, 0, rx);
269            final long curTx = randomLong(r, 0, tx);
270
271            recordData(curStart, curEnd, curRx, curTx);
272
273            rx -= curRx;
274            tx -= curTx;
275        }
276    }
277
278    private static long randomLong(Random r, long start, long end) {
279        return (long) (start + (r.nextFloat() * (end - start)));
280    }
281
282    public void dump(String prefix, PrintWriter pw, boolean fullHistory) {
283        pw.print(prefix);
284        pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration);
285
286        final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32);
287        if (start > 0) {
288            pw.print(prefix);
289            pw.print("  (omitting "); pw.print(start); pw.println(" buckets)");
290        }
291
292        for (int i = start; i < bucketCount; i++) {
293            pw.print(prefix);
294            pw.print("  bucketStart="); pw.print(bucketStart[i]);
295            pw.print(" rx="); pw.print(rx[i]);
296            pw.print(" tx="); pw.println(tx[i]);
297        }
298    }
299
300    @Override
301    public String toString() {
302        final CharArrayWriter writer = new CharArrayWriter();
303        dump("", new PrintWriter(writer), false);
304        return writer.toString();
305    }
306
307    public static final Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
308        public NetworkStatsHistory createFromParcel(Parcel in) {
309            return new NetworkStatsHistory(in);
310        }
311
312        public NetworkStatsHistory[] newArray(int size) {
313            return new NetworkStatsHistory[size];
314        }
315    };
316
317    private static long[] readLongArray(DataInputStream in) throws IOException {
318        final int size = in.readInt();
319        final long[] values = new long[size];
320        for (int i = 0; i < values.length; i++) {
321            values[i] = in.readLong();
322        }
323        return values;
324    }
325
326    private static void writeLongArray(DataOutputStream out, long[] values, int size) throws IOException {
327        if (size > values.length) {
328            throw new IllegalArgumentException("size larger than length");
329        }
330        out.writeInt(size);
331        for (int i = 0; i < size; i++) {
332            out.writeLong(values[i]);
333        }
334    }
335
336    private static long[] readLongArray(Parcel in) {
337        final int size = in.readInt();
338        final long[] values = new long[size];
339        for (int i = 0; i < values.length; i++) {
340            values[i] = in.readLong();
341        }
342        return values;
343    }
344
345    private static void writeLongArray(Parcel out, long[] values, int size) {
346        if (size > values.length) {
347            throw new IllegalArgumentException("size larger than length");
348        }
349        out.writeInt(size);
350        for (int i = 0; i < size; i++) {
351            out.writeLong(values[i]);
352        }
353    }
354
355}
356