Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 163e6443 authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Correct proc file reader, optimizations.

Moved away from BufferedReader, which only reads the first 8KB of
some proc files because it aggresively fills its buffer.  Optimized
proc parsing, now double the speed.  Tests to cover.

Log when NetworkStats counters roll backwards when subtracting, and
optimizations around findIndex().  When system removes UID, also
remove from last stats snapshot to avoid xt counters from rolling
backwards.

Bug: 5472949, 5458380
Change-Id: I07c08fe5233156fac2b84450f6291868bf9bfaf2
parent 7a5a50c1
Loading
Loading
Loading
Loading
+71 −50
Original line number Diff line number Diff line
@@ -16,10 +16,11 @@

package android.net;

import static com.android.internal.util.Preconditions.checkNotNull;

import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseBooleanArray;

import com.android.internal.util.Objects;
@@ -54,6 +55,8 @@ public class NetworkStats implements Parcelable {
    /** {@link #tag} value for total data across all tags. */
    public static final int TAG_NONE = 0;

    // TODO: move fields to "mVariable" notation

    /**
     * {@link SystemClock#elapsedRealtime()} timestamp when this data was
     * generated.
@@ -295,8 +298,33 @@ public class NetworkStats implements Parcelable {
     */
    public int findIndex(String iface, int uid, int set, int tag) {
        for (int i = 0; i < size; i++) {
            if (Objects.equal(iface, this.iface[i]) && uid == this.uid[i] && set == this.set[i]
                    && tag == this.tag[i]) {
            if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
                    && Objects.equal(iface, this.iface[i])) {
                return i;
            }
        }
        return -1;
    }

    /**
     * Find first stats index that matches the requested parameters, starting
     * search around the hinted index as an optimization.
     */
    // @VisibleForTesting
    public int findIndexHinted(String iface, int uid, int set, int tag, int hintIndex) {
        for (int offset = 0; offset < size; offset++) {
            final int halfOffset = offset / 2;

            // search outwards from hint index, alternating forward and backward
            final int i;
            if (offset % 2 == 0) {
                i = (hintIndex + halfOffset) % size;
            } else {
                i = (size + hintIndex - halfOffset - 1) % size;
            }

            if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
                    && Objects.equal(iface, this.iface[i])) {
                return i;
            }
        }
@@ -423,40 +451,10 @@ public class NetworkStats implements Parcelable {
     * Subtract the given {@link NetworkStats}, effectively leaving the delta
     * between two snapshots in time. Assumes that statistics rows collect over
     * time, and that none of them have disappeared.
     *
     * @throws IllegalArgumentException when given {@link NetworkStats} is
     *             non-monotonic.
     */
    public NetworkStats subtract(NetworkStats value) {
        return subtract(value, true, false);
    }

    /**
     * Subtract the given {@link NetworkStats}, effectively leaving the delta
     * between two snapshots in time. Assumes that statistics rows collect over
     * time, and that none of them have disappeared.
     * <p>
     * Instead of throwing when counters are non-monotonic, this variant clamps
     * results to never be negative.
     */
    public NetworkStats subtractClamped(NetworkStats value) {
        return subtract(value, false, true);
    }

    /**
     * Subtract the given {@link NetworkStats}, effectively leaving the delta
     * between two snapshots in time. Assumes that statistics rows collect over
     * time, and that none of them have disappeared.
     *
     * @param enforceMonotonic Validate that incoming value is strictly
     *            monotonic compared to this object.
     * @param clampNegative Instead of throwing like {@code enforceMonotonic},
     *            clamp resulting counters at 0 to prevent negative values.
     */
    private NetworkStats subtract(
            NetworkStats value, boolean enforceMonotonic, boolean clampNegative) {
    public NetworkStats subtract(NetworkStats value) throws NonMonotonicException {
        final long deltaRealtime = this.elapsedRealtime - value.elapsedRealtime;
        if (enforceMonotonic && deltaRealtime < 0) {
        if (deltaRealtime < 0) {
            throw new IllegalArgumentException("found non-monotonic realtime");
        }

@@ -470,7 +468,7 @@ public class NetworkStats implements Parcelable {
            entry.tag = tag[i];

            // find remote row that matches, and subtract
            final int j = value.findIndex(entry.iface, entry.uid, entry.set, entry.tag);
            final int j = value.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag, i);
            if (j == -1) {
                // newly appearing row, return entire value
                entry.rxBytes = rxBytes[i];
@@ -485,20 +483,10 @@ public class NetworkStats implements Parcelable {
                entry.txBytes = txBytes[i] - value.txBytes[j];
                entry.txPackets = txPackets[i] - value.txPackets[j];
                entry.operations = operations[i] - value.operations[j];
                if (enforceMonotonic
                        && (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0
                                || entry.txPackets < 0 || entry.operations < 0)) {
                    Log.v(TAG, "lhs=" + this);
                    Log.v(TAG, "rhs=" + value);
                    throw new IllegalArgumentException(
                            "found non-monotonic values at lhs[" + i + "] - rhs[" + j + "]");
                }
                if (clampNegative) {
                    entry.rxBytes = Math.max(0, entry.rxBytes);
                    entry.rxPackets = Math.max(0, entry.rxPackets);
                    entry.txBytes = Math.max(0, entry.txBytes);
                    entry.txPackets = Math.max(0, entry.txPackets);
                    entry.operations = Math.max(0, entry.operations);

                if (entry.rxBytes < 0 || entry.rxPackets < 0 || entry.txBytes < 0
                        || entry.txPackets < 0 || entry.operations < 0) {
                    throw new NonMonotonicException(this, i, value, j);
                }
            }

@@ -564,6 +552,24 @@ public class NetworkStats implements Parcelable {
        return stats;
    }

    /**
     * Return all rows except those attributed to the requested UID; doesn't
     * mutate the original structure.
     */
    public NetworkStats withoutUid(int uid) {
        final NetworkStats stats = new NetworkStats(elapsedRealtime, 10);

        Entry entry = new Entry();
        for (int i = 0; i < size; i++) {
            entry = getValues(i, entry);
            if (entry.uid != uid) {
                stats.addValues(entry);
            }
        }

        return stats;
    }

    public void dump(String prefix, PrintWriter pw) {
        pw.print(prefix);
        pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime);
@@ -625,4 +631,19 @@ public class NetworkStats implements Parcelable {
            return new NetworkStats[size];
        }
    };

    public static class NonMonotonicException extends Exception {
        public final NetworkStats left;
        public final NetworkStats right;
        public final int leftIndex;
        public final int rightIndex;

        public NonMonotonicException(
                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex) {
            this.left = checkNotNull(left, "missing left");
            this.right = checkNotNull(right, "missing right");
            this.leftIndex = leftIndex;
            this.rightIndex = rightIndex;
        }
    }
}
+10 −6
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import android.app.DownloadManager;
import android.app.backup.BackupManager;
import android.content.Context;
import android.media.MediaPlayer;
import android.net.NetworkStats.NonMonotonicException;
import android.os.RemoteException;
import android.os.ServiceManager;

@@ -192,12 +193,15 @@ public class TrafficStats {
                throw new IllegalStateException("not profiling data");
            }

            try {
                // subtract starting values and return delta
                final NetworkStats profilingStop = getDataLayerSnapshotForUid(context);
            final NetworkStats profilingDelta = profilingStop.subtractClamped(
                    sActiveProfilingStart);
                final NetworkStats profilingDelta = profilingStop.subtract(sActiveProfilingStart);
                sActiveProfilingStart = null;
                return profilingDelta;
            } catch (NonMonotonicException e) {
                throw new RuntimeException(e);
            }
        }
    }

+29 −37
Original line number Diff line number Diff line
@@ -25,12 +25,14 @@ import android.net.NetworkStats;
import android.os.SystemClock;
import android.util.Slog;

import com.android.internal.util.ProcFileReader;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import com.google.android.collect.Sets;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
@@ -107,6 +109,7 @@ public class NetworkStatsFactory {
        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
        final NetworkStats.Entry entry = new NetworkStats.Entry();

        // TODO: transition to ProcFileReader
        // TODO: read directly from proc once headers are added
        final ArrayList<String> keys = Lists.newArrayList(KEY_IFACE, KEY_ACTIVE, KEY_SNAP_RX_BYTES,
                KEY_SNAP_RX_PACKETS, KEY_SNAP_TX_BYTES, KEY_SNAP_TX_PACKETS, KEY_RX_BYTES,
@@ -257,71 +260,58 @@ public class NetworkStatsFactory {
        final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 24);
        final NetworkStats.Entry entry = new NetworkStats.Entry();

        // TODO: remove knownLines check once 5087722 verified
        final HashSet<String> knownLines = Sets.newHashSet();
        // TODO: remove lastIdx check once 5270106 verified
        int lastIdx;
        int idx = 1;
        int lastIdx = 1;

        final ArrayList<String> keys = Lists.newArrayList();
        final ArrayList<String> values = Lists.newArrayList();
        final HashMap<String, String> parsed = Maps.newHashMap();

        BufferedReader reader = null;
        String line = null;
        ProcFileReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(mStatsXtUid));

            // parse first line as header
            line = reader.readLine();
            splitLine(line, keys);
            lastIdx = 1;

            // parse remaining lines
            while ((line = reader.readLine()) != null) {
                splitLine(line, values);
                parseLine(keys, values, parsed);
            // open and consume header line
            reader = new ProcFileReader(new FileInputStream(mStatsXtUid));
            reader.finishLine();

                if (!knownLines.add(line)) {
                    throw new IllegalStateException("duplicate proc entry: " + line);
                }

                final int idx = getParsedInt(parsed, KEY_IDX);
            while (reader.hasMoreData()) {
                idx = reader.nextInt();
                if (idx != lastIdx + 1) {
                    throw new IllegalStateException(
                            "inconsistent idx=" + idx + " after lastIdx=" + lastIdx);
                }
                lastIdx = idx;

                entry.iface = parsed.get(KEY_IFACE);
                entry.uid = getParsedInt(parsed, KEY_UID);
                entry.set = getParsedInt(parsed, KEY_COUNTER_SET);
                entry.tag = kernelToTag(parsed.get(KEY_TAG_HEX));
                entry.rxBytes = getParsedLong(parsed, KEY_RX_BYTES);
                entry.rxPackets = getParsedLong(parsed, KEY_RX_PACKETS);
                entry.txBytes = getParsedLong(parsed, KEY_TX_BYTES);
                entry.txPackets = getParsedLong(parsed, KEY_TX_PACKETS);
                entry.iface = reader.nextString();
                entry.tag = kernelToTag(reader.nextString());
                entry.uid = reader.nextInt();
                entry.set = reader.nextInt();
                entry.rxBytes = reader.nextLong();
                entry.rxPackets = reader.nextLong();
                entry.txBytes = reader.nextLong();
                entry.txPackets = reader.nextLong();

                if (limitUid == UID_ALL || limitUid == entry.uid) {
                    stats.addValues(entry);
                }

                reader.finishLine();
            }
        } catch (NullPointerException e) {
            throw new IllegalStateException("problem parsing line: " + line, e);
            throw new IllegalStateException("problem parsing idx " + idx, e);
        } catch (NumberFormatException e) {
            throw new IllegalStateException("problem parsing line: " + line, e);
            throw new IllegalStateException("problem parsing idx " + idx, e);
        } catch (IOException e) {
            throw new IllegalStateException("problem parsing line: " + line, e);
            throw new IllegalStateException("problem parsing idx " + idx, e);
        } finally {
            IoUtils.closeQuietly(reader);
        }

        return stats;
    }

    @Deprecated
    private static int getParsedInt(HashMap<String, String> parsed, String key) {
        final String value = parsed.get(key);
        return value != null ? Integer.parseInt(value) : 0;
    }

    @Deprecated
    private static long getParsedLong(HashMap<String, String> parsed, String key) {
        final String value = parsed.get(key);
        return value != null ? Long.parseLong(value) : 0;
@@ -330,6 +320,7 @@ public class NetworkStatsFactory {
    /**
     * Split given line into {@link ArrayList}.
     */
    @Deprecated
    private static void splitLine(String line, ArrayList<String> outSplit) {
        outSplit.clear();

@@ -343,6 +334,7 @@ public class NetworkStatsFactory {
     * Zip the two given {@link ArrayList} as key and value pairs into
     * {@link HashMap}.
     */
    @Deprecated
    private static void parseLine(
            ArrayList<String> keys, ArrayList<String> values, HashMap<String, String> outParsed) {
        outParsed.clear();
+199 −0
Original line number Diff line number Diff line
/*
 * 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 com.android.internal.util;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charsets;

/**
 * Reader that specializes in parsing {@code /proc/} files quickly. Walks
 * through the stream using a single space {@code ' '} as token separator, and
 * requires each line boundary to be explicitly acknowledged using
 * {@link #finishLine()}. Assumes {@link Charsets#US_ASCII} encoding.
 * <p>
 * Currently doesn't support formats based on {@code \0}, tabs, or repeated
 * delimiters.
 */
public class ProcFileReader implements Closeable {
    private final InputStream mStream;
    private final byte[] mBuffer;

    /** Write pointer in {@link #mBuffer}. */
    private int mTail;
    /** Flag when last read token finished current line. */
    private boolean mLineFinished;

    public ProcFileReader(InputStream stream) throws IOException {
        this(stream, 4096);
    }

    public ProcFileReader(InputStream stream, int bufferSize) throws IOException {
        mStream = stream;
        mBuffer = new byte[bufferSize];

        // read enough to answer hasMoreData
        fillBuf();
    }

    /**
     * Read more data from {@link #mStream} into internal buffer.
     */
    private int fillBuf() throws IOException {
        final int length = mBuffer.length - mTail;
        if (length == 0) {
            throw new IOException("attempting to fill already-full buffer");
        }

        final int read = mStream.read(mBuffer, mTail, length);
        if (read != -1) {
            mTail += read;
        }
        return read;
    }

    /**
     * Consume number of bytes from beginning of internal buffer. If consuming
     * all remaining bytes, will attempt to {@link #fillBuf()}.
     */
    private void consumeBuf(int count) throws IOException {
        // TODO: consider moving to read pointer, but for now traceview says
        // these copies aren't a bottleneck.
        System.arraycopy(mBuffer, count, mBuffer, 0, mTail - count);
        mTail -= count;
        if (mTail == 0) {
            fillBuf();
        }
    }

    /**
     * Find buffer index of next token delimiter, usually space or newline. Will
     * fill buffer as needed.
     */
    private int nextTokenIndex() throws IOException {
        if (mLineFinished) {
            throw new IOException("no tokens remaining on current line");
        }

        int i = 0;
        do {
            // scan forward for token boundary
            for (; i < mTail; i++) {
                final byte b = mBuffer[i];
                if (b == '\n') {
                    mLineFinished = true;
                    return i;
                }
                if (b == ' ') {
                    return i;
                }
            }
        } while (fillBuf() > 0);

        throw new IOException("end of stream while looking for token boundary");
    }

    /**
     * Check if stream has more data to be parsed.
     */
    public boolean hasMoreData() {
        return mTail > 0;
    }

    /**
     * Finish current line, skipping any remaining data.
     */
    public void finishLine() throws IOException {
        // last token already finished line; reset silently
        if (mLineFinished) {
            mLineFinished = false;
            return;
        }

        int i = 0;
        do {
            // scan forward for line boundary and consume
            for (; i < mTail; i++) {
                if (mBuffer[i] == '\n') {
                    consumeBuf(i + 1);
                    return;
                }
            }
        } while (fillBuf() > 0);

        throw new IOException("end of stream while looking for line boundary");
    }

    /**
     * Parse and return next token as {@link String}.
     */
    public String nextString() throws IOException {
        final int tokenIndex = nextTokenIndex();
        final String s = new String(mBuffer, 0, tokenIndex, Charsets.US_ASCII);
        consumeBuf(tokenIndex + 1);
        return s;
    }

    /**
     * Parse and return next token as base-10 encoded {@code long}.
     */
    public long nextLong() throws IOException {
        final int tokenIndex = nextTokenIndex();
        final boolean negative = mBuffer[0] == '-';

        // TODO: refactor into something like IntegralToString
        long result = 0;
        for (int i = negative ? 1 : 0; i < tokenIndex; i++) {
            final int digit = mBuffer[i] - '0';
            if (digit < 0 || digit > 9) {
                throw invalidLong(tokenIndex);
            }

            // always parse as negative number and apply sign later; this
            // correctly handles MIN_VALUE which is "larger" than MAX_VALUE.
            final long next = result * 10 - digit;
            if (next > result) {
                throw invalidLong(tokenIndex);
            }
            result = next;
        }

        consumeBuf(tokenIndex + 1);
        return negative ? result : -result;
    }

    private NumberFormatException invalidLong(int tokenIndex) {
        return new NumberFormatException(
                "invalid long: " + new String(mBuffer, 0, tokenIndex, Charsets.US_ASCII));
    }

    /**
     * Parse and return next token as base-10 encoded {@code int}.
     */
    public int nextInt() throws IOException {
        final long value = nextLong();
        if (value > Integer.MAX_VALUE || value < Integer.MIN_VALUE) {
            throw new NumberFormatException("parsed value larger than integer");
        }
        return (int) value;
    }

    public void close() throws IOException {
        mStream.close();
    }
}
+3 −3
Original line number Diff line number Diff line
@@ -89,7 +89,7 @@ public class BandwidthTest extends InstrumentationTestCase {
     * Ensure that downloading on wifi reports reasonable stats.
     */
    @LargeTest
    public void testWifiDownload() {
    public void testWifiDownload() throws Exception {
        assertTrue(setDeviceWifiAndAirplaneMode(mSsid));
        NetworkStats pre_test_stats = fetchDataFromProc(mUid);
        String ts = Long.toString(System.currentTimeMillis());
@@ -123,7 +123,7 @@ public class BandwidthTest extends InstrumentationTestCase {
     * Ensure that downloading on wifi reports reasonable stats.
     */
    @LargeTest
    public void testWifiUpload() {
    public void testWifiUpload() throws Exception {
        assertTrue(setDeviceWifiAndAirplaneMode(mSsid));
        // Download a file from the server.
        String ts = Long.toString(System.currentTimeMillis());
@@ -160,7 +160,7 @@ public class BandwidthTest extends InstrumentationTestCase {
     * accounting still goes to the app making the call and that the numbers still make sense.
     */
    @LargeTest
    public void testWifiDownloadWithDownloadManager() {
    public void testWifiDownloadWithDownloadManager() throws Exception {
        assertTrue(setDeviceWifiAndAirplaneMode(mSsid));
        // If we are using the download manager, then the data that is written to /proc/uid_stat/
        // is accounted against download manager's uid, since it uses pre-ICS API.
Loading