/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.net; import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; import static android.net.NetworkStats.DEFAULT_NETWORK_NO; import static android.net.NetworkStats.IFACE_ALL; import static android.net.NetworkStats.METERED_NO; import static android.net.NetworkStats.ROAMING_NO; import static android.net.NetworkStats.SET_DEFAULT; import static android.net.NetworkStats.TAG_NONE; import static android.net.NetworkStats.UID_ALL; import static android.net.NetworkStatsHistory.DataStreamUtils.readFullLongArray; import static android.net.NetworkStatsHistory.DataStreamUtils.readVarLongArray; import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray; import static android.net.NetworkStatsHistory.Entry.UNKNOWN; import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray; import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray; import static android.text.format.DateUtils.SECOND_IN_MILLIS; import static com.android.net.module.util.NetworkStatsUtils.multiplySafeByRational; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.os.Build; import android.os.Parcel; import android.os.Parcelable; import android.service.NetworkStatsHistoryBucketProto; import android.service.NetworkStatsHistoryProto; import android.util.IndentingPrintWriter; import android.util.proto.ProtoOutputStream; import com.android.net.module.util.CollectionUtils; import com.android.net.module.util.NetworkStatsUtils; import libcore.util.EmptyArray; import java.io.CharArrayWriter; import java.io.DataInput; import java.io.DataOutput; import java.io.IOException; import java.io.PrintWriter; import java.net.ProtocolException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.TreeMap; /** * Collection of historical network statistics, recorded into equally-sized * "buckets" in time. Internally it stores data in {@code long} series for more * efficient persistence. *

* Each bucket is defined by a {@link #bucketStart} timestamp, and lasts for * {@link #bucketDuration}. Internally assumes that {@link #bucketStart} is * sorted at all times. * * @hide */ @SystemApi(client = MODULE_LIBRARIES) public final class NetworkStatsHistory implements Parcelable { private static final int VERSION_INIT = 1; private static final int VERSION_ADD_PACKETS = 2; private static final int VERSION_ADD_ACTIVE = 3; /** @hide */ public static final int FIELD_ACTIVE_TIME = 0x01; /** @hide */ public static final int FIELD_RX_BYTES = 0x02; /** @hide */ public static final int FIELD_RX_PACKETS = 0x04; /** @hide */ public static final int FIELD_TX_BYTES = 0x08; /** @hide */ public static final int FIELD_TX_PACKETS = 0x10; /** @hide */ public static final int FIELD_OPERATIONS = 0x20; /** @hide */ public static final int FIELD_ALL = 0xFFFFFFFF; private long bucketDuration; private int bucketCount; private long[] bucketStart; private long[] activeTime; private long[] rxBytes; private long[] rxPackets; private long[] txBytes; private long[] txPackets; private long[] operations; private long totalBytes; /** @hide */ public NetworkStatsHistory(long bucketDuration, long[] bucketStart, long[] activeTime, long[] rxBytes, long[] rxPackets, long[] txBytes, long[] txPackets, long[] operations, int bucketCount, long totalBytes) { this.bucketDuration = bucketDuration; this.bucketStart = bucketStart; this.activeTime = activeTime; this.rxBytes = rxBytes; this.rxPackets = rxPackets; this.txBytes = txBytes; this.txPackets = txPackets; this.operations = operations; this.bucketCount = bucketCount; this.totalBytes = totalBytes; } /** * An instance to represent a single record in a {@link NetworkStatsHistory} object. */ public static final class Entry { /** @hide */ public static final long UNKNOWN = -1; /** @hide */ // TODO: Migrate all callers to get duration from the history object and remove this field. @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public long bucketDuration; /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public long bucketStart; /** @hide */ public long activeTime; /** @hide */ @UnsupportedAppUsage public long rxBytes; /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public long rxPackets; /** @hide */ @UnsupportedAppUsage public long txBytes; /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public long txPackets; /** @hide */ public long operations; /** @hide */ Entry() {} /** * Construct a {@link Entry} instance to represent a single record in a * {@link NetworkStatsHistory} object. * * @param bucketStart Start of period for this {@link Entry}, in milliseconds since the * Unix epoch, see {@link java.lang.System#currentTimeMillis}. * @param activeTime Active time for this {@link Entry}, in milliseconds. * @param rxBytes Number of bytes received for this {@link Entry}. Statistics should * represent the contents of IP packets, including IP headers. * @param rxPackets Number of packets received for this {@link Entry}. Statistics should * represent the contents of IP packets, including IP headers. * @param txBytes Number of bytes transmitted for this {@link Entry}. Statistics should * represent the contents of IP packets, including IP headers. * @param txPackets Number of bytes transmitted for this {@link Entry}. Statistics should * represent the contents of IP packets, including IP headers. * @param operations count of network operations performed for this {@link Entry}. This can * be used to derive bytes-per-operation. */ public Entry(long bucketStart, long activeTime, long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) { this.bucketStart = bucketStart; this.activeTime = activeTime; this.rxBytes = rxBytes; this.rxPackets = rxPackets; this.txBytes = txBytes; this.txPackets = txPackets; this.operations = operations; } /** * Get start timestamp of the bucket's time interval, in milliseconds since the Unix epoch. */ public long getBucketStart() { return bucketStart; } /** * Get active time of the bucket's time interval, in milliseconds. */ public long getActiveTime() { return activeTime; } /** Get number of bytes received for this {@link Entry}. */ public long getRxBytes() { return rxBytes; } /** Get number of packets received for this {@link Entry}. */ public long getRxPackets() { return rxPackets; } /** Get number of bytes transmitted for this {@link Entry}. */ public long getTxBytes() { return txBytes; } /** Get number of packets transmitted for this {@link Entry}. */ public long getTxPackets() { return txPackets; } /** Get count of network operations performed for this {@link Entry}. */ public long getOperations() { return operations; } @Override public boolean equals(Object o) { if (this == o) return true; if (o.getClass() != getClass()) return false; Entry entry = (Entry) o; return bucketStart == entry.bucketStart && activeTime == entry.activeTime && rxBytes == entry.rxBytes && rxPackets == entry.rxPackets && txBytes == entry.txBytes && txPackets == entry.txPackets && operations == entry.operations; } @Override public int hashCode() { return (int) (bucketStart * 2 + activeTime * 3 + rxBytes * 5 + rxPackets * 7 + txBytes * 11 + txPackets * 13 + operations * 17); } @Override public String toString() { return "Entry{" + "bucketStart=" + bucketStart + ", activeTime=" + activeTime + ", rxBytes=" + rxBytes + ", rxPackets=" + rxPackets + ", txBytes=" + txBytes + ", txPackets=" + txPackets + ", operations=" + operations + "}"; } /** * Add the given {@link Entry} with this instance and return a new {@link Entry} * instance as the result. * * @hide */ @NonNull public Entry plus(@NonNull Entry another, long bucketDuration) { if (this.bucketStart != another.bucketStart) { throw new IllegalArgumentException("bucketStart " + this.bucketStart + " is not equal to " + another.bucketStart); } return new Entry(this.bucketStart, // Active time should not go over bucket duration. Math.min(this.activeTime + another.activeTime, bucketDuration), this.rxBytes + another.rxBytes, this.rxPackets + another.rxPackets, this.txBytes + another.txBytes, this.txPackets + another.txPackets, this.operations + another.operations); } } /** @hide */ @UnsupportedAppUsage public NetworkStatsHistory(long bucketDuration) { this(bucketDuration, 10, FIELD_ALL); } /** @hide */ public NetworkStatsHistory(long bucketDuration, int initialSize) { this(bucketDuration, initialSize, FIELD_ALL); } /** @hide */ public NetworkStatsHistory(long bucketDuration, int initialSize, int fields) { this.bucketDuration = bucketDuration; bucketStart = new long[initialSize]; if ((fields & FIELD_ACTIVE_TIME) != 0) activeTime = new long[initialSize]; if ((fields & FIELD_RX_BYTES) != 0) rxBytes = new long[initialSize]; if ((fields & FIELD_RX_PACKETS) != 0) rxPackets = new long[initialSize]; if ((fields & FIELD_TX_BYTES) != 0) txBytes = new long[initialSize]; if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize]; if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize]; bucketCount = 0; totalBytes = 0; } /** @hide */ public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) { this(bucketDuration, existing.estimateResizeBuckets(bucketDuration)); recordEntireHistory(existing); } /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public NetworkStatsHistory(Parcel in) { bucketDuration = in.readLong(); bucketStart = readLongArray(in); activeTime = readLongArray(in); rxBytes = readLongArray(in); rxPackets = readLongArray(in); txBytes = readLongArray(in); txPackets = readLongArray(in); operations = readLongArray(in); bucketCount = bucketStart.length; totalBytes = in.readLong(); } @Override public void writeToParcel(@NonNull Parcel out, int flags) { out.writeLong(bucketDuration); writeLongArray(out, bucketStart, bucketCount); writeLongArray(out, activeTime, bucketCount); writeLongArray(out, rxBytes, bucketCount); writeLongArray(out, rxPackets, bucketCount); writeLongArray(out, txBytes, bucketCount); writeLongArray(out, txPackets, bucketCount); writeLongArray(out, operations, bucketCount); out.writeLong(totalBytes); } /** @hide */ public NetworkStatsHistory(DataInput in) throws IOException { final int version = in.readInt(); switch (version) { case VERSION_INIT: { bucketDuration = in.readLong(); bucketStart = readFullLongArray(in); rxBytes = readFullLongArray(in); rxPackets = new long[bucketStart.length]; txBytes = readFullLongArray(in); txPackets = new long[bucketStart.length]; operations = new long[bucketStart.length]; bucketCount = bucketStart.length; totalBytes = CollectionUtils.total(rxBytes) + CollectionUtils.total(txBytes); break; } case VERSION_ADD_PACKETS: case VERSION_ADD_ACTIVE: { bucketDuration = in.readLong(); bucketStart = readVarLongArray(in); activeTime = (version >= VERSION_ADD_ACTIVE) ? readVarLongArray(in) : new long[bucketStart.length]; rxBytes = readVarLongArray(in); rxPackets = readVarLongArray(in); txBytes = readVarLongArray(in); txPackets = readVarLongArray(in); operations = readVarLongArray(in); bucketCount = bucketStart.length; totalBytes = CollectionUtils.total(rxBytes) + CollectionUtils.total(txBytes); break; } default: { throw new ProtocolException("unexpected version: " + version); } } if (bucketStart.length != bucketCount || rxBytes.length != bucketCount || rxPackets.length != bucketCount || txBytes.length != bucketCount || txPackets.length != bucketCount || operations.length != bucketCount) { throw new ProtocolException("Mismatched history lengths"); } } /** @hide */ public void writeToStream(DataOutput out) throws IOException { out.writeInt(VERSION_ADD_ACTIVE); out.writeLong(bucketDuration); writeVarLongArray(out, bucketStart, bucketCount); writeVarLongArray(out, activeTime, bucketCount); writeVarLongArray(out, rxBytes, bucketCount); writeVarLongArray(out, rxPackets, bucketCount); writeVarLongArray(out, txBytes, bucketCount); writeVarLongArray(out, txPackets, bucketCount); writeVarLongArray(out, operations, bucketCount); } @Override public int describeContents() { return 0; } /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public int size() { return bucketCount; } /** @hide */ public long getBucketDuration() { return bucketDuration; } /** @hide */ @UnsupportedAppUsage public long getStart() { if (bucketCount > 0) { return bucketStart[0]; } else { return Long.MAX_VALUE; } } /** @hide */ @UnsupportedAppUsage public long getEnd() { if (bucketCount > 0) { return bucketStart[bucketCount - 1] + bucketDuration; } else { return Long.MIN_VALUE; } } /** * Return total bytes represented by this history. * @hide */ public long getTotalBytes() { return totalBytes; } /** * Return index of bucket that contains or is immediately before the * requested time. * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public int getIndexBefore(long time) { int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time); if (index < 0) { index = (~index) - 1; } else { index -= 1; } return NetworkStatsUtils.constrain(index, 0, bucketCount - 1); } /** * Return index of bucket that contains or is immediately after the * requested time. * @hide */ public int getIndexAfter(long time) { int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time); if (index < 0) { index = ~index; } else { index += 1; } return NetworkStatsUtils.constrain(index, 0, bucketCount - 1); } /** * Return specific stats entry. * @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public Entry getValues(int i, Entry recycle) { final Entry entry = recycle != null ? recycle : new Entry(); entry.bucketStart = bucketStart[i]; entry.bucketDuration = bucketDuration; entry.activeTime = getLong(activeTime, i, UNKNOWN); entry.rxBytes = getLong(rxBytes, i, UNKNOWN); entry.rxPackets = getLong(rxPackets, i, UNKNOWN); entry.txBytes = getLong(txBytes, i, UNKNOWN); entry.txPackets = getLong(txPackets, i, UNKNOWN); entry.operations = getLong(operations, i, UNKNOWN); return entry; } /** * Get List of {@link Entry} of the {@link NetworkStatsHistory} instance. * * @return */ @NonNull public List getEntries() { // TODO: Return a wrapper that uses this list instead, to prevent the returned result // from being changed. final ArrayList ret = new ArrayList<>(size()); for (int i = 0; i < size(); i++) { ret.add(getValues(i, null /* recycle */)); } return ret; } /** @hide */ public void setValues(int i, Entry entry) { // Unwind old values if (rxBytes != null) totalBytes -= rxBytes[i]; if (txBytes != null) totalBytes -= txBytes[i]; bucketStart[i] = entry.bucketStart; setLong(activeTime, i, entry.activeTime); setLong(rxBytes, i, entry.rxBytes); setLong(rxPackets, i, entry.rxPackets); setLong(txBytes, i, entry.txBytes); setLong(txPackets, i, entry.txPackets); setLong(operations, i, entry.operations); // Apply new values if (rxBytes != null) totalBytes += rxBytes[i]; if (txBytes != null) totalBytes += txBytes[i]; } /** * Record that data traffic occurred in the given time range. Will * distribute across internal buckets, creating new buckets as needed. * @hide */ @Deprecated public void recordData(long start, long end, long rxBytes, long txBytes) { recordData(start, end, new NetworkStats.Entry( IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, rxBytes, 0L, txBytes, 0L, 0L)); } /** * Record that data traffic occurred in the given time range. Will * distribute across internal buckets, creating new buckets as needed. * @hide */ public void recordData(long start, long end, NetworkStats.Entry entry) { long rxBytes = entry.rxBytes; long rxPackets = entry.rxPackets; long txBytes = entry.txBytes; long txPackets = entry.txPackets; long operations = entry.operations; if (entry.isNegative()) { throw new IllegalArgumentException("tried recording negative data"); } if (entry.isEmpty()) { return; } // create any buckets needed by this range ensureBuckets(start, end); // Return fast if there is still no entry. This would typically happen when the start, // end or duration are not valid values, e.g. start > end, negative duration value, etc. if (bucketCount == 0) return; // distribute data usage into buckets long duration = end - start; final int startIndex = getIndexAfter(end); for (int i = startIndex; i >= 0; i--) { final long curStart = bucketStart[i]; final long curEnd = curStart + bucketDuration; // bucket is older than record; we're finished if (curEnd < start) break; // bucket is newer than record; keep looking if (curStart > end) continue; final long overlap = Math.min(curEnd, end) - Math.max(curStart, start); if (overlap <= 0) continue; // integer math each time is faster than floating point final long fracRxBytes = multiplySafeByRational(rxBytes, overlap, duration); final long fracRxPackets = multiplySafeByRational(rxPackets, overlap, duration); final long fracTxBytes = multiplySafeByRational(txBytes, overlap, duration); final long fracTxPackets = multiplySafeByRational(txPackets, overlap, duration); final long fracOperations = multiplySafeByRational(operations, overlap, duration); addLong(activeTime, i, overlap); addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes; addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets; addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes; addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets; addLong(this.operations, i, fracOperations); operations -= fracOperations; duration -= overlap; } totalBytes += entry.rxBytes + entry.txBytes; } /** * Record an entire {@link NetworkStatsHistory} into this history. Usually * for combining together stats for external reporting. * @hide */ @UnsupportedAppUsage public void recordEntireHistory(NetworkStatsHistory input) { recordHistory(input, Long.MIN_VALUE, Long.MAX_VALUE); } /** * Record given {@link NetworkStatsHistory} into this history, copying only * buckets that atomically occur in the inclusive time range. Doesn't * interpolate across partial buckets. * @hide */ public void recordHistory(NetworkStatsHistory input, long start, long end) { final NetworkStats.Entry entry = new NetworkStats.Entry( IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); for (int i = 0; i < input.bucketCount; i++) { final long bucketStart = input.bucketStart[i]; final long bucketEnd = bucketStart + input.bucketDuration; // skip when bucket is outside requested range if (bucketStart < start || bucketEnd > end) continue; entry.rxBytes = getLong(input.rxBytes, i, 0L); entry.rxPackets = getLong(input.rxPackets, i, 0L); entry.txBytes = getLong(input.txBytes, i, 0L); entry.txPackets = getLong(input.txPackets, i, 0L); entry.operations = getLong(input.operations, i, 0L); recordData(bucketStart, bucketEnd, entry); } } /** * Ensure that buckets exist for given time range, creating as needed. */ private void ensureBuckets(long start, long end) { // normalize incoming range to bucket boundaries start -= start % bucketDuration; end += (bucketDuration - (end % bucketDuration)) % bucketDuration; for (long now = start; now < end; now += bucketDuration) { // try finding existing bucket final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now); if (index < 0) { // bucket missing, create and insert insertBucket(~index, now); } } } /** * Insert new bucket at requested index and starting time. */ private void insertBucket(int index, long start) { // create more buckets when needed if (bucketCount >= bucketStart.length) { final int newLength = Math.max(bucketStart.length, 10) * 3 / 2; bucketStart = Arrays.copyOf(bucketStart, newLength); if (activeTime != null) activeTime = Arrays.copyOf(activeTime, newLength); if (rxBytes != null) rxBytes = Arrays.copyOf(rxBytes, newLength); if (rxPackets != null) rxPackets = Arrays.copyOf(rxPackets, newLength); if (txBytes != null) txBytes = Arrays.copyOf(txBytes, newLength); if (txPackets != null) txPackets = Arrays.copyOf(txPackets, newLength); if (operations != null) operations = Arrays.copyOf(operations, newLength); } // create gap when inserting bucket in middle if (index < bucketCount) { final int dstPos = index + 1; final int length = bucketCount - index; System.arraycopy(bucketStart, index, bucketStart, dstPos, length); if (activeTime != null) System.arraycopy(activeTime, index, activeTime, dstPos, length); if (rxBytes != null) System.arraycopy(rxBytes, index, rxBytes, dstPos, length); if (rxPackets != null) System.arraycopy(rxPackets, index, rxPackets, dstPos, length); if (txBytes != null) System.arraycopy(txBytes, index, txBytes, dstPos, length); if (txPackets != null) System.arraycopy(txPackets, index, txPackets, dstPos, length); if (operations != null) System.arraycopy(operations, index, operations, dstPos, length); } bucketStart[index] = start; setLong(activeTime, index, 0L); setLong(rxBytes, index, 0L); setLong(rxPackets, index, 0L); setLong(txBytes, index, 0L); setLong(txPackets, index, 0L); setLong(operations, index, 0L); bucketCount++; } /** * Clear all data stored in this object. * @hide */ public void clear() { bucketStart = EmptyArray.LONG; if (activeTime != null) activeTime = EmptyArray.LONG; if (rxBytes != null) rxBytes = EmptyArray.LONG; if (rxPackets != null) rxPackets = EmptyArray.LONG; if (txBytes != null) txBytes = EmptyArray.LONG; if (txPackets != null) txPackets = EmptyArray.LONG; if (operations != null) operations = EmptyArray.LONG; bucketCount = 0; totalBytes = 0; } /** * Remove buckets that start older than requested cutoff. * * This method will remove any bucket that contains any data older than the requested * cutoff, even if that same bucket includes some data from after the cutoff. * * @hide */ public void removeBucketsStartingBefore(final long cutoff) { // TODO: Consider use getIndexBefore. int i; for (i = 0; i < bucketCount; i++) { final long curStart = bucketStart[i]; // This bucket starts after or at the cutoff, so it should be kept. if (curStart >= cutoff) break; } if (i > 0) { final int length = bucketStart.length; bucketStart = Arrays.copyOfRange(bucketStart, i, length); if (activeTime != null) activeTime = Arrays.copyOfRange(activeTime, i, length); if (rxBytes != null) rxBytes = Arrays.copyOfRange(rxBytes, i, length); if (rxPackets != null) rxPackets = Arrays.copyOfRange(rxPackets, i, length); if (txBytes != null) txBytes = Arrays.copyOfRange(txBytes, i, length); if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length); if (operations != null) operations = Arrays.copyOfRange(operations, i, length); bucketCount -= i; totalBytes = 0; if (rxBytes != null) totalBytes += CollectionUtils.total(rxBytes); if (txBytes != null) totalBytes += CollectionUtils.total(txBytes); } } /** * Return interpolated data usage across the requested range. Interpolates * across buckets, so values may be rounded slightly. * *

If the active bucket is not completed yet, it returns the proportional value of it * based on its duration and the {@code end} param. * * @param start - start of the range, timestamp in milliseconds since the epoch. * @param end - end of the range, timestamp in milliseconds since the epoch. * @param recycle - entry instance for performance, could be null. * @hide */ @UnsupportedAppUsage public Entry getValues(long start, long end, Entry recycle) { return getValues(start, end, Long.MAX_VALUE, recycle); } /** * Return interpolated data usage across the requested range. Interpolates * across buckets, so values may be rounded slightly. * * @param start - start of the range, timestamp in milliseconds since the epoch. * @param end - end of the range, timestamp in milliseconds since the epoch. * @param now - current timestamp in milliseconds since the epoch (wall clock). * @param recycle - entry instance for performance, could be null. * @hide */ @UnsupportedAppUsage public Entry getValues(long start, long end, long now, Entry recycle) { final Entry entry = recycle != null ? recycle : new Entry(); entry.bucketDuration = end - start; entry.bucketStart = start; entry.activeTime = activeTime != null ? 0 : UNKNOWN; entry.rxBytes = rxBytes != null ? 0 : UNKNOWN; entry.rxPackets = rxPackets != null ? 0 : UNKNOWN; entry.txBytes = txBytes != null ? 0 : UNKNOWN; entry.txPackets = txPackets != null ? 0 : UNKNOWN; entry.operations = operations != null ? 0 : UNKNOWN; // Return fast if there is no entry. if (bucketCount == 0) return entry; final int startIndex = getIndexAfter(end); for (int i = startIndex; i >= 0; i--) { final long curStart = bucketStart[i]; long curEnd = curStart + bucketDuration; // bucket is older than request; we're finished if (curEnd <= start) break; // bucket is newer than request; keep looking if (curStart >= end) continue; // the active bucket is shorter then a normal completed bucket if (curEnd > now) curEnd = now; // usually this is simply bucketDuration final long bucketSpan = curEnd - curStart; // prevent division by zero if (bucketSpan <= 0) continue; final long overlapEnd = curEnd < end ? curEnd : end; final long overlapStart = curStart > start ? curStart : start; final long overlap = overlapEnd - overlapStart; if (overlap <= 0) continue; // integer math each time is faster than floating point if (activeTime != null) { entry.activeTime += multiplySafeByRational(activeTime[i], overlap, bucketSpan); } if (rxBytes != null) { entry.rxBytes += multiplySafeByRational(rxBytes[i], overlap, bucketSpan); } if (rxPackets != null) { entry.rxPackets += multiplySafeByRational(rxPackets[i], overlap, bucketSpan); } if (txBytes != null) { entry.txBytes += multiplySafeByRational(txBytes[i], overlap, bucketSpan); } if (txPackets != null) { entry.txPackets += multiplySafeByRational(txPackets[i], overlap, bucketSpan); } if (operations != null) { entry.operations += multiplySafeByRational(operations[i], overlap, bucketSpan); } } return entry; } /** * @deprecated only for temporary testing * @hide */ @Deprecated public void generateRandom(long start, long end, long bytes) { final Random r = new Random(); final float fractionRx = r.nextFloat(); final long rxBytes = (long) (bytes * fractionRx); final long txBytes = (long) (bytes * (1 - fractionRx)); final long rxPackets = rxBytes / 1024; final long txPackets = txBytes / 1024; final long operations = rxBytes / 2048; generateRandom(start, end, rxBytes, rxPackets, txBytes, txPackets, operations, r); } /** * @deprecated only for temporary testing * @hide */ @Deprecated public void generateRandom(long start, long end, long rxBytes, long rxPackets, long txBytes, long txPackets, long operations, Random r) { ensureBuckets(start, end); final NetworkStats.Entry entry = new NetworkStats.Entry( IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO, 0L, 0L, 0L, 0L, 0L); while (rxBytes > 1024 || rxPackets > 128 || txBytes > 1024 || txPackets > 128 || operations > 32) { final long curStart = randomLong(r, start, end); final long curEnd = curStart + randomLong(r, 0, (end - curStart) / 2); entry.rxBytes = randomLong(r, 0, rxBytes); entry.rxPackets = randomLong(r, 0, rxPackets); entry.txBytes = randomLong(r, 0, txBytes); entry.txPackets = randomLong(r, 0, txPackets); entry.operations = randomLong(r, 0, operations); rxBytes -= entry.rxBytes; rxPackets -= entry.rxPackets; txBytes -= entry.txBytes; txPackets -= entry.txPackets; operations -= entry.operations; recordData(curStart, curEnd, entry); } } /** @hide */ public static long randomLong(Random r, long start, long end) { return (long) (start + (r.nextFloat() * (end - start))); } /** * Quickly determine if this history intersects with given window. * @hide */ public boolean intersects(long start, long end) { final long dataStart = getStart(); final long dataEnd = getEnd(); if (start >= dataStart && start <= dataEnd) return true; if (end >= dataStart && end <= dataEnd) return true; if (dataStart >= start && dataStart <= end) return true; if (dataEnd >= start && dataEnd <= end) return true; return false; } /** @hide */ public void dump(IndentingPrintWriter pw, boolean fullHistory) { pw.print("NetworkStatsHistory: bucketDuration="); pw.println(bucketDuration / SECOND_IN_MILLIS); pw.increaseIndent(); final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32); if (start > 0) { pw.print("(omitting "); pw.print(start); pw.println(" buckets)"); } for (int i = start; i < bucketCount; i++) { pw.print("st="); pw.print(bucketStart[i] / SECOND_IN_MILLIS); if (rxBytes != null) { pw.print(" rb="); pw.print(rxBytes[i]); } if (rxPackets != null) { pw.print(" rp="); pw.print(rxPackets[i]); } if (txBytes != null) { pw.print(" tb="); pw.print(txBytes[i]); } if (txPackets != null) { pw.print(" tp="); pw.print(txPackets[i]); } if (operations != null) { pw.print(" op="); pw.print(operations[i]); } pw.println(); } pw.decreaseIndent(); } /** @hide */ public void dumpCheckin(PrintWriter pw) { pw.print("d,"); pw.print(bucketDuration / SECOND_IN_MILLIS); pw.println(); for (int i = 0; i < bucketCount; i++) { pw.print("b,"); pw.print(bucketStart[i] / SECOND_IN_MILLIS); pw.print(','); if (rxBytes != null) { pw.print(rxBytes[i]); } else { pw.print("*"); } pw.print(','); if (rxPackets != null) { pw.print(rxPackets[i]); } else { pw.print("*"); } pw.print(','); if (txBytes != null) { pw.print(txBytes[i]); } else { pw.print("*"); } pw.print(','); if (txPackets != null) { pw.print(txPackets[i]); } else { pw.print("*"); } pw.print(','); if (operations != null) { pw.print(operations[i]); } else { pw.print("*"); } pw.println(); } } /** @hide */ public void dumpDebug(ProtoOutputStream proto, long tag) { final long start = proto.start(tag); proto.write(NetworkStatsHistoryProto.BUCKET_DURATION_MS, bucketDuration); for (int i = 0; i < bucketCount; i++) { final long startBucket = proto.start(NetworkStatsHistoryProto.BUCKETS); proto.write(NetworkStatsHistoryBucketProto.BUCKET_START_MS, bucketStart[i]); dumpDebug(proto, NetworkStatsHistoryBucketProto.RX_BYTES, rxBytes, i); dumpDebug(proto, NetworkStatsHistoryBucketProto.RX_PACKETS, rxPackets, i); dumpDebug(proto, NetworkStatsHistoryBucketProto.TX_BYTES, txBytes, i); dumpDebug(proto, NetworkStatsHistoryBucketProto.TX_PACKETS, txPackets, i); dumpDebug(proto, NetworkStatsHistoryBucketProto.OPERATIONS, operations, i); proto.end(startBucket); } proto.end(start); } private static void dumpDebug(ProtoOutputStream proto, long tag, long[] array, int index) { if (array != null) { proto.write(tag, array[index]); } } @Override public String toString() { final CharArrayWriter writer = new CharArrayWriter(); dump(new IndentingPrintWriter(writer, " "), false); return writer.toString(); } /** * Same as "equals", but not actually called equals as this would affect public API behavior. * @hide */ @Nullable public boolean isSameAs(NetworkStatsHistory other) { return bucketCount == other.bucketCount && Arrays.equals(bucketStart, other.bucketStart) // Don't check activeTime since it can change on import due to the importer using // recordHistory. It's also not exposed by the APIs or present in dumpsys or // toString(). && Arrays.equals(rxBytes, other.rxBytes) && Arrays.equals(rxPackets, other.rxPackets) && Arrays.equals(txBytes, other.txBytes) && Arrays.equals(txPackets, other.txPackets) && Arrays.equals(operations, other.operations) && totalBytes == other.totalBytes; } @UnsupportedAppUsage public static final @android.annotation.NonNull Creator CREATOR = new Creator() { @Override public NetworkStatsHistory createFromParcel(Parcel in) { return new NetworkStatsHistory(in); } @Override public NetworkStatsHistory[] newArray(int size) { return new NetworkStatsHistory[size]; } }; private static long getLong(long[] array, int i, long value) { return array != null ? array[i] : value; } private static void setLong(long[] array, int i, long value) { if (array != null) array[i] = value; } private static void addLong(long[] array, int i, long value) { if (array != null) array[i] += value; } /** @hide */ public int estimateResizeBuckets(long newBucketDuration) { return (int) (size() * getBucketDuration() / newBucketDuration); } /** * Utility methods for interacting with {@link DataInputStream} and * {@link DataOutputStream}, mostly dealing with writing partial arrays. * @hide */ public static class DataStreamUtils { @Deprecated public static long[] readFullLongArray(DataInput in) throws IOException { final int size = in.readInt(); if (size < 0) throw new ProtocolException("negative array size"); final long[] values = new long[size]; for (int i = 0; i < values.length; i++) { values[i] = in.readLong(); } return values; } /** * Read variable-length {@link Long} using protobuf-style approach. */ public static long readVarLong(DataInput in) throws IOException { int shift = 0; long result = 0; while (shift < 64) { byte b = in.readByte(); result |= (long) (b & 0x7F) << shift; if ((b & 0x80) == 0) return result; shift += 7; } throw new ProtocolException("malformed long"); } /** * Write variable-length {@link Long} using protobuf-style approach. */ public static void writeVarLong(DataOutput out, long value) throws IOException { while (true) { if ((value & ~0x7FL) == 0) { out.writeByte((int) value); return; } else { out.writeByte(((int) value & 0x7F) | 0x80); value >>>= 7; } } } public static long[] readVarLongArray(DataInput in) throws IOException { final int size = in.readInt(); if (size == -1) return null; if (size < 0) throw new ProtocolException("negative array size"); final long[] values = new long[size]; for (int i = 0; i < values.length; i++) { values[i] = readVarLong(in); } return values; } public static void writeVarLongArray(DataOutput out, long[] values, int size) throws IOException { if (values == null) { out.writeInt(-1); return; } if (size > values.length) { throw new IllegalArgumentException("size larger than length"); } out.writeInt(size); for (int i = 0; i < size; i++) { writeVarLong(out, values[i]); } } } /** * Utility methods for interacting with {@link Parcel} structures, mostly * dealing with writing partial arrays. * @hide */ public static class ParcelUtils { public static long[] readLongArray(Parcel in) { final int size = in.readInt(); if (size == -1) return null; final long[] values = new long[size]; for (int i = 0; i < values.length; i++) { values[i] = in.readLong(); } return values; } public static void writeLongArray(Parcel out, long[] values, int size) { if (values == null) { out.writeInt(-1); return; } if (size > values.length) { throw new IllegalArgumentException("size larger than length"); } out.writeInt(size); for (int i = 0; i < size; i++) { out.writeLong(values[i]); } } } /** * Builder class for {@link NetworkStatsHistory}. */ public static final class Builder { private final TreeMap mEntries; private final long mBucketDuration; /** * Creates a new Builder with given bucket duration and initial capacity to construct * {@link NetworkStatsHistory} objects. * * @param bucketDuration Duration of the buckets of the object, in milliseconds. * @param initialCapacity Estimated number of records. */ public Builder(long bucketDuration, int initialCapacity) { mBucketDuration = bucketDuration; // Create a collection that is always sorted and can deduplicate items by the timestamp. mEntries = new TreeMap<>(); } /** * Add an {@link Entry} into the {@link NetworkStatsHistory} instance. If the timestamp * already exists, the given {@link Entry} will be combined into existing entry. * * @param entry The target {@link Entry} object. * @return The builder object. */ @NonNull public Builder addEntry(@NonNull Entry entry) { final Entry existing = mEntries.get(entry.bucketStart); if (existing != null) { mEntries.put(entry.bucketStart, existing.plus(entry, mBucketDuration)); } else { mEntries.put(entry.bucketStart, entry); } return this; } private static long sum(@NonNull long[] array) { long sum = 0L; for (long entry : array) { sum += entry; } return sum; } /** * Builds the instance of the {@link NetworkStatsHistory}. * * @return the built instance of {@link NetworkStatsHistory}. */ @NonNull public NetworkStatsHistory build() { int size = mEntries.size(); final long[] bucketStart = new long[size]; final long[] activeTime = new long[size]; final long[] rxBytes = new long[size]; final long[] rxPackets = new long[size]; final long[] txBytes = new long[size]; final long[] txPackets = new long[size]; final long[] operations = new long[size]; int i = 0; for (Entry entry : mEntries.values()) { bucketStart[i] = entry.bucketStart; activeTime[i] = entry.activeTime; rxBytes[i] = entry.rxBytes; rxPackets[i] = entry.rxPackets; txBytes[i] = entry.txBytes; txPackets[i] = entry.txPackets; operations[i] = entry.operations; i++; } return new NetworkStatsHistory(mBucketDuration, bucketStart, activeTime, rxBytes, rxPackets, txBytes, txPackets, operations, size, sum(rxBytes) + sum(txBytes)); } } }