blob: 6d79d3bd32ab52a99f9e926010178e7a9c51f1cc [file] [log] [blame]
/*
* 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.EventLog;
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 {
private SparseMappingTable mParent;
private int mSequence = 1;
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) {
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 = mParent.mSequence;
}
/**
* 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() {
// Something with this checking isn't working and is triggering
// more problems than it's helping to debug.
// Original bug: b/27045736
// New bug: b/27960286
if (false) {
// Assert that our sequence number matches mParent's. If it isn't that means
// we have been reset and our. If our sequence is UNITIALIZED_SEQUENCE, then
// it's possible that everything is working fine and we just haven't been
// written to since the last resetTable().
if (mSequence != mParent.mSequence) {
if (mSequence < mParent.mSequence) {
logOrThrow("Sequence mismatch. SparseMappingTable.reset()"
+ " called but not Table.resetTable() -- "
+ dumpInternalState());
return;
} else if (mSequence > mParent.mSequence) {
logOrThrow("Sequence mismatch. Table.resetTable()"
+ " called but not SparseMappingTable.reset() -- "
+ 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();
}
}
public SparseMappingTable() {
mLongs.add(new long[ARRAY_SIZE]);
}
/**
* 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);
}
// Verify that last array's length is consistent with writeToParcel
if (N > 0 && mLongs.get(N - 1).length != mNextIndex) {
EventLog.writeEvent(0x534e4554, "73252178", -1, "");
throw new IllegalStateException("Expected array of length " + mNextIndex + " but was "
+ mLongs.get(N - 1).length);
}
}
/**
* Return a string for debugging.
*/
public String dumpInternalState(boolean includeData) {
final StringBuilder sb = new StringBuilder();
sb.append("SparseMappingTable{");
sb.append("mSequence=");
sb.append(mSequence);
sb.append(" mNextIndex=");
sb.append(mNextIndex);
sb.append(" mLongs.size=");
final int N = mLongs.size();
sb.append(N);
sb.append("\n");
if (includeData) {
for (int i=0; i<N; i++) {
final long[] array = mLongs.get(i);
for (int j=0; j<array.length; j++) {
if (i == N-1 && j == mNextIndex) {
break;
}
sb.append(String.format(" %4d %d 0x%016x %-19d\n", i, j, array[j], array[j]));
}
}
}
sb.append("}");
return sb.toString();
}
/**
* 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&0x0ffffffffL);
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.e(TAG, message, th);
if (Build.TYPE.equals("eng")) {
throw new RuntimeException(message, th);
}
}
}