| /* |
| * Copyright (C) 2013 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.app.procstats; |
| |
| import android.os.Build; |
| import android.os.Parcel; |
| import android.util.Slog; |
| import libcore.util.EmptyArray; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| |
| import com.android.internal.util.GrowingArrayUtils; |
| |
| /** |
| * Class that contains a set of tables mapping byte ids to long values. |
| * |
| * This class is used to store the ProcessStats data. This data happens to be |
| * a set of very sparse tables, that is mostly append or overwrite, with infrequent |
| * resets of the data. |
| * |
| * Data is stored as a list of large long[] arrays containing the actual values. There are a |
| * set of Table objects that each contain a small array mapping the byte IDs to a position |
| * in the larger arrays. |
| * |
| * The data itself is either a single long value or a range of long values which are always |
| * stored continguously in one of the long arrays. When the caller allocates a slot with |
| * getOrAddKey, an int key is returned. That key can be re-retreived with getKey without |
| * allocating the value. The data can then be set or retrieved with that key. |
| */ |
| public class SparseMappingTable { |
| private static final String TAG = "SparseMappingTable"; |
| |
| // How big each array is. |
| public static final int ARRAY_SIZE = 4096; |
| |
| public static final int INVALID_KEY = 0xffffffff; |
| |
| // Where the "type"/"state" part of the data appears in an offset integer. |
| private static final int ID_SHIFT = 0; |
| private static final int ID_MASK = 0xff; |
| // Where the "which array" part of the data appears in an offset integer. |
| private static final int ARRAY_SHIFT = 8; |
| private static final int ARRAY_MASK = 0xff; |
| // Where the "index into array" part of the data appears in an offset integer. |
| private static final int INDEX_SHIFT = 16; |
| private static final int INDEX_MASK = 0xffff; |
| |
| private int mSequence; |
| private int mNextIndex; |
| private final ArrayList<long[]> mLongs = new ArrayList<long[]>(); |
| |
| /** |
| * A table of data as stored in a SparseMappingTable. |
| */ |
| public static class Table { |
| // When mSequence is this this our data better be empty |
| private static final int UNINITIALIZED_SEQUENCE = -1; |
| |
| private SparseMappingTable mParent; |
| private int mSequence = UNINITIALIZED_SEQUENCE; |
| private int[] mTable; |
| private int mSize; |
| |
| public Table(SparseMappingTable parent) { |
| mParent = parent; |
| mSequence = parent.mSequence; |
| } |
| |
| /** |
| * Pulls the data from 'copyFrom' and stores it in our own longs table. |
| * |
| * @param copyFrom The Table to copy from |
| * @param valueCount The number of values to copy for each key |
| */ |
| public void copyFrom(Table copyFrom, int valueCount) { |
| mTable = null; |
| mSize = 0; |
| |
| final int N = copyFrom.getKeyCount(); |
| for (int i=0; i<N; i++) { |
| final int theirKey = copyFrom.getKeyAt(i); |
| final long[] theirLongs = copyFrom.mParent.mLongs.get(getArrayFromKey(theirKey)); |
| |
| final byte id = SparseMappingTable.getIdFromKey(theirKey); |
| |
| final int myKey = this.getOrAddKey((byte)id, valueCount); |
| final long[] myLongs = mParent.mLongs.get(getArrayFromKey(myKey)); |
| |
| System.arraycopy(theirLongs, getIndexFromKey(theirKey), |
| myLongs, getIndexFromKey(myKey), valueCount); |
| } |
| } |
| |
| /** |
| * Allocates data in the buffer, and stores that key in the mapping for this |
| * table. |
| * |
| * @param id The id of the item (will be used in making the key) |
| * @param count The number of bytes to allocate. Must be less than |
| * SparseMappingTable.ARRAY_SIZE. |
| * |
| * @return The 'key' for this data value, which contains both the id itself |
| * and the location in the long arrays that the data is actually stored |
| * but should be considered opaque to the caller. |
| */ |
| public int getOrAddKey(byte id, int count) { |
| // This is the only place we add data to mParent.mLongs, so this is the time |
| // to update our sequence to match there. |
| if (mSequence == UNINITIALIZED_SEQUENCE) { |
| mSequence = mParent.mSequence; |
| } |
| |
| assertConsistency(); |
| |
| final int idx = binarySearch(id); |
| if (idx >= 0) { |
| // Found |
| return mTable[idx]; |
| } else { |
| // Not found. Need to allocate it. |
| |
| // Get an array with enough space to store 'count' values. |
| final ArrayList<long[]> list = mParent.mLongs; |
| int whichArray = list.size()-1; |
| long[] array = list.get(whichArray); |
| if (mParent.mNextIndex + count > array.length) { |
| // if it won't fit then make a new array. |
| array = new long[ARRAY_SIZE]; |
| list.add(array); |
| whichArray++; |
| mParent.mNextIndex = 0; |
| } |
| |
| // The key is a combination of whichArray, which index in that array, and |
| // the table value itself, which will be used for lookup |
| final int key = (whichArray << ARRAY_SHIFT) |
| | (mParent.mNextIndex << INDEX_SHIFT) |
| | (((int)id) << ID_SHIFT); |
| |
| mParent.mNextIndex += count; |
| |
| // Store the key in the sparse lookup table for this Table object. |
| mTable = GrowingArrayUtils.insert(mTable != null ? mTable : EmptyArray.INT, |
| mSize, ~idx, key); |
| mSize++; |
| |
| return key; |
| } |
| } |
| |
| /** |
| * Looks up a key in the table. |
| * |
| * @return The key from this table or INVALID_KEY if the id is not found. |
| */ |
| public int getKey(byte id) { |
| assertConsistency(); |
| |
| final int idx = binarySearch(id); |
| if (idx >= 0) { |
| return mTable[idx]; |
| } else { |
| return INVALID_KEY; |
| } |
| } |
| |
| /** |
| * Get the value for the given key and offset from that key. |
| * |
| * @param key A key as obtained from getKey or getOrAddKey. |
| * @param value The value to set. |
| */ |
| public long getValue(int key) { |
| return getValue(key, 0); |
| } |
| |
| /** |
| * Get the value for the given key and offset from that key. |
| * |
| * @param key A key as obtained from getKey or getOrAddKey. |
| * @param index The offset from that key. Must be less than the count |
| * provided to getOrAddKey when the space was allocated. |
| * @param value The value to set. |
| * |
| * @return the value, or 0 in case of an error |
| */ |
| public long getValue(int key, int index) { |
| assertConsistency(); |
| |
| try { |
| final long[] array = mParent.mLongs.get(getArrayFromKey(key)); |
| return array[getIndexFromKey(key) + index]; |
| } catch (IndexOutOfBoundsException ex) { |
| logOrThrow("key=0x" + Integer.toHexString(key) |
| + " index=" + index + " -- " + dumpInternalState(), ex); |
| return 0; |
| } |
| } |
| |
| /** |
| * Set the value for the given id at offset 0 from that id. |
| * If the id is not found, return 0 instead. |
| * |
| * @param id The id of the item. |
| */ |
| public long getValueForId(byte id) { |
| return getValueForId(id, 0); |
| } |
| |
| /** |
| * Set the value for the given id and index offset from that id. |
| * If the id is not found, return 0 instead. |
| * |
| * @param id The id of the item. |
| * @param index The offset from that key. Must be less than the count |
| * provided to getOrAddKey when the space was allocated. |
| */ |
| public long getValueForId(byte id, int index) { |
| assertConsistency(); |
| |
| final int idx = binarySearch(id); |
| if (idx >= 0) { |
| final int key = mTable[idx]; |
| try { |
| final long[] array = mParent.mLongs.get(getArrayFromKey(key)); |
| return array[getIndexFromKey(key) + index]; |
| } catch (IndexOutOfBoundsException ex) { |
| logOrThrow("id=0x" + Integer.toHexString(id) + " idx=" + idx |
| + " key=0x" + Integer.toHexString(key) + " index=" + index |
| + " -- " + dumpInternalState(), ex); |
| return 0; |
| } |
| } else { |
| return 0; |
| } |
| } |
| |
| /** |
| * Return the raw storage long[] for the given key. |
| */ |
| public long[] getArrayForKey(int key) { |
| assertConsistency(); |
| |
| return mParent.mLongs.get(getArrayFromKey(key)); |
| } |
| |
| /** |
| * Set the value for the given key and offset from that key. |
| * |
| * @param key A key as obtained from getKey or getOrAddKey. |
| * @param value The value to set. |
| */ |
| public void setValue(int key, long value) { |
| setValue(key, 0, value); |
| } |
| |
| /** |
| * Set the value for the given key and offset from that key. |
| * |
| * @param key A key as obtained from getKey or getOrAddKey. |
| * @param index The offset from that key. Must be less than the count |
| * provided to getOrAddKey when the space was allocated. |
| * @param value The value to set. |
| */ |
| public void setValue(int key, int index, long value) { |
| assertConsistency(); |
| |
| if (value < 0) { |
| logOrThrow("can't store negative values" |
| + " key=0x" + Integer.toHexString(key) |
| + " index=" + index + " value=" + value |
| + " -- " + dumpInternalState()); |
| return; |
| } |
| |
| try { |
| final long[] array = mParent.mLongs.get(getArrayFromKey(key)); |
| array[getIndexFromKey(key) + index] = value; |
| } catch (IndexOutOfBoundsException ex) { |
| logOrThrow("key=0x" + Integer.toHexString(key) |
| + " index=" + index + " value=" + value |
| + " -- " + dumpInternalState(), ex); |
| return; |
| } |
| } |
| |
| /** |
| * Clear out the table, and reset the sequence numbers so future writes |
| * without allocations will assert. |
| */ |
| public void resetTable() { |
| // Clear out our table. |
| mTable = null; |
| mSize = 0; |
| |
| // Reset our sequence number. This will make all read/write calls |
| // start to fail, and then when we re-allocate it will be re-synced |
| // to that of mParent. |
| mSequence = UNINITIALIZED_SEQUENCE; |
| } |
| |
| /** |
| * Write the keys stored in the table to the parcel. The parent must |
| * be separately written. Does not save the actual data. |
| */ |
| public void writeToParcel(Parcel out) { |
| out.writeInt(mSequence); |
| out.writeInt(mSize); |
| for (int i=0; i<mSize; i++) { |
| out.writeInt(mTable[i]); |
| } |
| } |
| |
| /** |
| * Read the keys from the parcel. The parent (with its long array) must |
| * have been previously initialized. |
| */ |
| public boolean readFromParcel(Parcel in) { |
| // Read the state |
| mSequence = in.readInt(); |
| mSize = in.readInt(); |
| if (mSize != 0) { |
| mTable = new int[mSize]; |
| for (int i=0; i<mSize; i++) { |
| mTable[i] = in.readInt(); |
| } |
| } else { |
| mTable = null; |
| } |
| |
| // Make sure we're all healthy |
| if (validateKeys(true)) { |
| return true; |
| } else { |
| // Clear it out |
| mSize = 0; |
| mTable = null; |
| return false; |
| } |
| } |
| |
| /** |
| * Return the number of keys that have been added to this Table. |
| */ |
| public int getKeyCount() { |
| return mSize; |
| } |
| |
| /** |
| * Get the key at the given index in our table. |
| */ |
| public int getKeyAt(int i) { |
| return mTable[i]; |
| } |
| |
| /** |
| * Throw an exception if one of a variety of internal consistency checks fails. |
| */ |
| private void assertConsistency() { |
| // Assert that our sequewnce number has been initialized. If it hasn't |
| // that means someone tried to read or write data without allocating it |
| // since we were created or reset. |
| if (mSequence == UNINITIALIZED_SEQUENCE) { |
| logOrThrow("mSequence == UNINITIALIZED_SEQUENCE in" |
| + " SparseMappingTable.Table. -- " |
| + dumpInternalState()); |
| return; |
| } |
| |
| // Assert that our sequence number matches mParent's. If it isn't that means |
| // we have been reset and our |
| if (mSequence != mParent.mSequence) { |
| if (mSequence < mParent.mSequence) { |
| logOrThrow("Sequence mismatch. SparseMappingTable.resetTable()" |
| + " called but not Table.resetTable() -- " |
| + dumpInternalState()); |
| return; |
| } else if (mSequence > mParent.mSequence) { |
| logOrThrow("Sequence mismatch. Table.resetTable()" |
| + " called but not SparseMappingTable.resetTable() -- " |
| + dumpInternalState()); |
| return; |
| } |
| } |
| } |
| |
| /** |
| * Finds the 'id' inside the array of length size (physical size of the array |
| * is not used). |
| * |
| * @return The index of the array, or the bitwise not (~index) of where it |
| * would go if you wanted to insert 'id' into the array. |
| */ |
| private int binarySearch(byte id) { |
| int lo = 0; |
| int hi = mSize - 1; |
| |
| while (lo <= hi) { |
| int mid = (lo + hi) >>> 1; |
| byte midId = (byte)((mTable[mid] >> ID_SHIFT) & ID_MASK); |
| |
| if (midId < id) { |
| lo = mid + 1; |
| } else if (midId > id) { |
| hi = mid - 1; |
| } else { |
| return mid; // id found |
| } |
| } |
| return ~lo; // id not present |
| } |
| |
| /** |
| * Check that all the keys are valid locations in the long arrays. |
| * |
| * If any aren't, log it and return false. Else return true. |
| */ |
| private boolean validateKeys(boolean log) { |
| ArrayList<long[]> longs = mParent.mLongs; |
| final int longsSize = longs.size(); |
| |
| final int N = mSize; |
| for (int i=0; i<N; i++) { |
| final int key = mTable[i]; |
| final int arrayIndex = getArrayFromKey(key); |
| final int index = getIndexFromKey(key); |
| if (arrayIndex >= longsSize || index >= longs.get(arrayIndex).length) { |
| if (log) { |
| Slog.w(TAG, "Invalid stats at index " + i + " -- " + dumpInternalState()); |
| } |
| return false; |
| } |
| } |
| |
| return true; |
| } |
| |
| public String dumpInternalState() { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("SparseMappingTable.Table{mSequence="); |
| sb.append(mSequence); |
| sb.append(" mParent.mSequence="); |
| sb.append(mParent.mSequence); |
| sb.append(" mParent.mLongs.size()="); |
| sb.append(mParent.mLongs.size()); |
| sb.append(" mSize="); |
| sb.append(mSize); |
| sb.append(" mTable="); |
| if (mTable == null) { |
| sb.append("null"); |
| } else { |
| final int N = mTable.length; |
| sb.append('['); |
| for (int i=0; i<N; i++) { |
| final int key = mTable[i]; |
| sb.append("0x"); |
| sb.append(Integer.toHexString((key >> ID_SHIFT) & ID_MASK)); |
| sb.append("/0x"); |
| sb.append(Integer.toHexString((key >> ARRAY_SHIFT) & ARRAY_MASK)); |
| sb.append("/0x"); |
| sb.append(Integer.toHexString((key >> INDEX_SHIFT) & INDEX_MASK)); |
| if (i != N-1) { |
| sb.append(", "); |
| } |
| } |
| sb.append(']'); |
| } |
| sb.append(" clazz="); |
| sb.append(getClass().getName()); |
| sb.append('}'); |
| |
| return sb.toString(); |
| } |
| } |
| |
| /** |
| * Wipe out all the data. |
| */ |
| public void reset() { |
| // Clear out mLongs, and prime it with a new array of data |
| mLongs.clear(); |
| mLongs.add(new long[ARRAY_SIZE]); |
| mNextIndex = 0; |
| |
| // Increment out sequence counter, because all of the tables will |
| // now be out of sync with the data. |
| mSequence++; |
| } |
| |
| /** |
| * Write the data arrays to the parcel. |
| */ |
| public void writeToParcel(Parcel out) { |
| out.writeInt(mSequence); |
| out.writeInt(mNextIndex); |
| final int N = mLongs.size(); |
| out.writeInt(N); |
| for (int i=0; i<N-1; i++) { |
| final long[] array = mLongs.get(i); |
| out.writeInt(array.length); |
| writeCompactedLongArray(out, array, array.length); |
| } |
| // save less for the last one. upon re-loading they'll just start a new array. |
| final long[] lastLongs = mLongs.get(N-1); |
| out.writeInt(mNextIndex); |
| writeCompactedLongArray(out, lastLongs, mNextIndex); |
| } |
| |
| /** |
| * Read the data arrays from the parcel. |
| */ |
| public void readFromParcel(Parcel in) { |
| mSequence = in.readInt(); |
| mNextIndex = in.readInt(); |
| |
| mLongs.clear(); |
| final int N = in.readInt(); |
| for (int i=0; i<N; i++) { |
| final int size = in.readInt(); |
| final long[] array = new long[size]; |
| readCompactedLongArray(in, array, size); |
| mLongs.add(array); |
| } |
| } |
| |
| /** |
| * Write the long array to the parcel in a compacted form. Does not allow negative |
| * values in the array. |
| */ |
| private static void writeCompactedLongArray(Parcel out, long[] array, int num) { |
| for (int i=0; i<num; i++) { |
| long val = array[i]; |
| if (val < 0) { |
| Slog.w(TAG, "Time val negative: " + val); |
| val = 0; |
| } |
| if (val <= Integer.MAX_VALUE) { |
| out.writeInt((int)val); |
| } else { |
| int top = ~((int)((val>>32)&0x7fffffff)); |
| int bottom = (int)(val&0xfffffff); |
| out.writeInt(top); |
| out.writeInt(bottom); |
| } |
| } |
| } |
| |
| /** |
| * Read the compacted array into the long[]. |
| */ |
| private static void readCompactedLongArray(Parcel in, long[] array, int num) { |
| final int alen = array.length; |
| if (num > alen) { |
| logOrThrow("bad array lengths: got " + num + " array is " + alen); |
| return; |
| } |
| int i; |
| for (i=0; i<num; i++) { |
| int val = in.readInt(); |
| if (val >= 0) { |
| array[i] = val; |
| } else { |
| int bottom = in.readInt(); |
| array[i] = (((long)~val)<<32) | bottom; |
| } |
| } |
| while (i < alen) { |
| array[i] = 0; |
| i++; |
| } |
| } |
| |
| /** |
| * Extract the id from a key. |
| */ |
| public static byte getIdFromKey(int key) { |
| return (byte)((key >> ID_SHIFT) & ID_MASK); |
| } |
| |
| /** |
| * Gets the index of the array in the list of arrays. |
| * |
| * Not to be confused with getIndexFromKey. |
| */ |
| public static int getArrayFromKey(int key) { |
| return (key >> ARRAY_SHIFT) & ARRAY_MASK; |
| } |
| |
| /** |
| * Gets the index of a value in a long[]. |
| * |
| * Not to be confused with getArrayFromKey. |
| */ |
| public static int getIndexFromKey(int key) { |
| return (key >> INDEX_SHIFT) & INDEX_MASK; |
| } |
| |
| /** |
| * Do a Slog.wtf or throw an exception (thereby crashing the system process if |
| * this is a debug build.) |
| */ |
| private static void logOrThrow(String message) { |
| logOrThrow(message, new RuntimeException("Stack trace")); |
| } |
| |
| /** |
| * Do a Slog.wtf or throw an exception (thereby crashing the system process if |
| * this is an eng build.) |
| */ |
| private static void logOrThrow(String message, Throwable th) { |
| Slog.wtf(TAG, message, th); |
| if (Build.TYPE.equals("eng")) { |
| throw new RuntimeException(message, th); |
| } |
| } |
| } |