diff --git a/services/core/java/com/android/server/utils/WatchedSparseBooleanMatrix.java b/services/core/java/com/android/server/utils/WatchedSparseBooleanMatrix.java index 42a2f81bf9a70..bf54bd527f574 100644 --- a/services/core/java/com/android/server/utils/WatchedSparseBooleanMatrix.java +++ b/services/core/java/com/android/server/utils/WatchedSparseBooleanMatrix.java @@ -16,9 +16,12 @@ package com.android.server.utils; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; + import android.annotation.Nullable; import android.annotation.Size; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; @@ -39,13 +42,14 @@ import java.util.Arrays; public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappable { /** - * The matrix is implemented through four arrays. The matrix of booleans is stored in - * a one-dimensional {@code mValues} array. {@code mValues} is always of size - * {@code mOrder * mOrder}. Elements of {@code mValues} are addressed with - * arithmetic: the offset of the element {@code {row, col}} is at - * {@code row * mOrder + col}. The term "storage index" applies to {@code mValues}. - * A storage index designates a row (column) in the underlying storage. This is not - * the same as the row seen by client code. + * The matrix is implemented through four arrays. First, the matrix of booleans is + * stored in a two-dimensional {@code mValues} array of bit-packed booleans. + * {@code mValues} is always of size {@code mOrder * mOrder / 8}. The factor of 8 is + * present because there are 8 bits in a byte. Elements of {@code mValues} are + * addressed with arithmetic: the element {@code {row, col}} is bit {@code col % 8} in + * byte * {@code (row * mOrder + col) / 8}. The term "storage index" applies to + * {@code mValues}. A storage index designates a row (column) in the underlying + * storage. This is not the same as the row seen by client code. * * Client code addresses the matrix through indices. These are integers that need not * be contiguous. Client indices are mapped to storage indices through two linear @@ -61,16 +65,32 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab * * Some notes: *
0...size()-1, returns the
* value from the indexth key-value mapping that this WatchedSparseBooleanMatrix
@@ -280,8 +315,22 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
validateIndex(rowIndex, colIndex);
int r = mMap[rowIndex];
int c = mMap[colIndex];
- int element = r * mOrder + c;
- return mValues[element];
+ return valueAtInternal(r, c);
+ }
+
+ /**
+ * An internal method to set the boolean value given the mValues row and column
+ * indices. These are not the indices used by the *At() methods.
+ */
+ private void setValueAtInternal(int row, int col, boolean value) {
+ int element = row * mOrder + col;
+ int offset = element / BYTE;
+ byte mask = (byte) (1 << (element % BYTE));
+ if (value) {
+ mValues[offset] |= mask;
+ } else {
+ mValues[offset] &= ~mask;
+ }
}
/**
@@ -291,8 +340,7 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
validateIndex(rowIndex, colIndex);
int r = mMap[rowIndex];
int c = mMap[colIndex];
- int element = r * mOrder + c;
- mValues[element] = value;
+ setValueAtInternal(r, c, value);
onChanged();
}
@@ -327,12 +375,17 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mMap = GrowingArrayUtils.insert(mMap, mSize, i, newIndex);
mSize++;
+
// Initialize the row and column corresponding to the new index.
+ int valueRow = mOrder / BYTE;
+ int offset = newIndex / BYTE;
+ byte mask = (byte) (~(1 << (newIndex % BYTE)));
+ Arrays.fill(mValues, newIndex * valueRow, (newIndex + 1) * valueRow, (byte) 0);
for (int n = 0; n < mSize; n++) {
- mValues[n * mOrder + newIndex] = false;
- mValues[newIndex * mOrder + n] = false;
+ mValues[n * valueRow + offset] &= mask;
}
- onChanged();
+ // Do not report onChanged() from this private method. onChanged() is the
+ // responsibility of public methods that call this one.
}
return i;
}
@@ -355,6 +408,33 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
validateIndex(col);
}
+ /**
+ * Expand the 2D array. This also extends the free list.
+ */
+ private void growMatrix() {
+ resizeValues(mOrder + STEP);
+ }
+
+ /**
+ * Resize the values array to the new dimension.
+ */
+ private void resizeValues(int newOrder) {
+
+ boolean[] newInuse = Arrays.copyOf(mInUse, newOrder);
+ int minOrder = Math.min(mOrder, newOrder);
+
+ byte[] newValues = new byte[newOrder * newOrder / BYTE];
+ for (int i = 0; i < minOrder; i++) {
+ int row = mOrder * i / BYTE;
+ int newRow = newOrder * i / BYTE;
+ System.arraycopy(mValues, row, newValues, newRow, minOrder / BYTE);
+ }
+
+ mInUse = newInuse;
+ mValues = newValues;
+ mOrder = newOrder;
+ }
+
/**
* Find an unused storage index, mark it in-use, and return it.
*/
@@ -369,27 +449,82 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
}
/**
- * Expand the 2D array. This also extends the free list.
+ * Return the index of the key that uses the highest row index in use. This returns
+ * -1 if the matrix is empty. Note that the return is an index suitable for the *At()
+ * methods. It is not the index in the mInUse array.
*/
- private void growMatrix() {
- int newOrder = mOrder + STEP;
-
- boolean[] newInuse = Arrays.copyOf(mInUse, newOrder);
-
- boolean[] newValues = new boolean[newOrder * newOrder];
- for (int i = 0; i < mOrder; i++) {
- int row = mOrder * i;
- int newRow = newOrder * i;
- for (int j = 0; j < mOrder; j++) {
- int index = row + j;
- int newIndex = newRow + j;
- newValues[newIndex] = mValues[index];
+ private int lastInuse() {
+ for (int i = mOrder - 1; i >= 0; i--) {
+ if (mInUse[i]) {
+ for (int j = 0; j < mSize; j++) {
+ if (mMap[j] == i) {
+ return j;
+ }
+ }
+ throw new IndexOutOfBoundsException();
}
}
+ return -1;
+ }
- mInUse = newInuse;
- mValues = newValues;
- mOrder = newOrder;
+ /**
+ * Compress the matrix by packing keys into consecutive indices. If the compression
+ * is sufficient, the mValues array can be shrunk.
+ */
+ private void pack() {
+ if (mSize == 0 || mSize == mOrder) {
+ return;
+ }
+ // dst and src are identify raw (row, col) in mValues. srcIndex is the index (as
+ // in the result of keyAt()) of the key being relocated.
+ for (int dst = nextFree(); dst < mSize; dst = nextFree()) {
+ int srcIndex = lastInuse();
+ int src = mMap[srcIndex];
+ mInUse[src] = false;
+ mMap[srcIndex] = dst;
+ System.arraycopy(mValues, src * mOrder / BYTE,
+ mValues, dst * mOrder / BYTE,
+ mOrder / BYTE);
+ int srcOffset = (src / BYTE);
+ byte srcMask = (byte) (1 << (src % BYTE));
+ int dstOffset = (dst / BYTE);
+ byte dstMask = (byte) (1 << (dst % BYTE));
+ for (int i = 0; i < mOrder; i++) {
+ if ((mValues[srcOffset] & srcMask) == 0) {
+ mValues[dstOffset] &= ~dstMask;
+ } else {
+ mValues[dstOffset] |= dstMask;
+ }
+ srcOffset += mOrder / BYTE;
+ dstOffset += mOrder / BYTE;
+ }
+ }
+ }
+
+ /**
+ * Shrink the matrix, if possible.
+ */
+ public void compact() {
+ pack();
+ int unused = (mOrder - mSize) / STEP;
+ if (unused > 0) {
+ resizeValues(mOrder - (unused * STEP));
+ }
+ }
+
+ /**
+ * Return a copy of the keys that are in use by the matrix.
+ */
+ public int[] keys() {
+ return Arrays.copyOf(mKeys, mSize);
+ }
+
+ /**
+ * Return the size of the 2D matrix. This is always greater than or equal to size().
+ * This does not reflect the sizes of the meta-information arrays (such as mKeys).
+ */
+ public int capacity() {
+ return mOrder;
}
/**
@@ -398,15 +533,12 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
@Override
public int hashCode() {
int hashCode = mSize;
+ hashCode = 31 * hashCode + Arrays.hashCode(mKeys);
+ hashCode = 31 * hashCode + Arrays.hashCode(mMap);
for (int i = 0; i < mSize; i++) {
- hashCode = 31 * hashCode + mKeys[i];
- hashCode = 31 * hashCode + mMap[i];
- }
- for (int i = 0; i < mSize; i++) {
- int row = mMap[i] * mOrder;
+ int row = mMap[i];
for (int j = 0; j < mSize; j++) {
- int element = mMap[j] + row;
- hashCode = 31 * hashCode + (mValues[element] ? 1 : 0);
+ hashCode = 31 * hashCode + (valueAtInternal(row, mMap[j]) ? 1 : 0);
}
}
return hashCode;
@@ -429,20 +561,16 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
if (mSize != other.mSize) {
return false;
}
-
- for (int i = 0; i < mSize; i++) {
- if (mKeys[i] != other.mKeys[i]) {
- return false;
- }
- if (mMap[i] != other.mMap[i]) {
- return false;
- }
+ if (!Arrays.equals(mKeys, other.mKeys)) {
+ // mKeys is zero padded at the end and is sorted, so the arrays can always be
+ // directly compared.
+ return false;
}
for (int i = 0; i < mSize; i++) {
- int row = mMap[i] * mOrder;
+ int row = mMap[i];
for (int j = 0; j < mSize; j++) {
- int element = mMap[j] + row;
- if (mValues[element] != other.mValues[element]) {
+ int col = mMap[j];
+ if (valueAtInternal(row, col) != other.valueAtInternal(row, col)) {
return false;
}
}
@@ -451,9 +579,12 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
}
/**
- * Return the matrix meta information. This is always three strings long.
+ * Return the matrix meta information. This is always three strings long. The
+ * strings are indexed by the constants STRING_KEY_INDEX, STRING_MAP_INDEX, and
+ * STRING_INUSE_INDEX.
*/
- private @Size(3) String[] matrixToStringMeta() {
+ @VisibleForTesting(visibility = PRIVATE)
+ @Size(3) String[] matrixToStringMeta() {
String[] result = new String[3];
StringBuilder k = new StringBuilder();
@@ -463,7 +594,7 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
k.append(" ");
}
}
- result[0] = k.substring(0);
+ result[STRING_KEY_INDEX] = k.substring(0);
StringBuilder m = new StringBuilder();
for (int i = 0; i < mSize; i++) {
@@ -472,42 +603,47 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
m.append(" ");
}
}
- result[1] = m.substring(0);
+ result[STRING_MAP_INDEX] = m.substring(0);
StringBuilder u = new StringBuilder();
for (int i = 0; i < mOrder; i++) {
u.append(mInUse[i] ? "1" : "0");
}
- result[2] = u.substring(0);
+ result[STRING_INUSE_INDEX] = u.substring(0);
return result;
}
/**
* Return the matrix as an array of strings. There is one string per row. Each
- * string has a '1' or a '0' in the proper column.
+ * string has a '1' or a '0' in the proper column. This is the raw data indexed by
+ * row/column disregarding the key map.
*/
- private String[] matrixToStringRaw() {
+ @VisibleForTesting(visibility = PRIVATE)
+ String[] matrixToStringRaw() {
String[] result = new String[mOrder];
for (int i = 0; i < mOrder; i++) {
- int row = i * mOrder;
StringBuilder line = new StringBuilder(mOrder);
for (int j = 0; j < mOrder; j++) {
- int element = row + j;
- line.append(mValues[element] ? "1" : "0");
+ line.append(valueAtInternal(i, j) ? "1" : "0");
}
result[i] = line.substring(0);
}
return result;
}
- private String[] matrixToStringCooked() {
+ /**
+ * Return the matrix as an array of strings. There is one string per row. Each
+ * string has a '1' or a '0' in the proper column. This is the cooked data indexed by
+ * keys, in key order.
+ */
+ @VisibleForTesting(visibility = PRIVATE)
+ String[] matrixToStringCooked() {
String[] result = new String[mSize];
for (int i = 0; i < mSize; i++) {
- int row = mMap[i] * mOrder;
+ int row = mMap[i];
StringBuilder line = new StringBuilder(mSize);
for (int j = 0; j < mSize; j++) {
- int element = row + mMap[j];
- line.append(mValues[element] ? "1" : "0");
+ line.append(valueAtInternal(row, mMap[j]) ? "1" : "0");
}
result[i] = line.substring(0);
}
diff --git a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java
index 5db9492c35c5f..f361f4a8bb5c0 100644
--- a/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java
+++ b/services/tests/servicestests/src/com/android/server/utils/WatcherTest.java
@@ -22,7 +22,6 @@ import static org.junit.Assert.fail;
import android.util.ArrayMap;
import android.util.ArraySet;
-import android.util.Log;
import android.util.LongSparseArray;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
@@ -35,7 +34,6 @@ import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Random;
/**
@@ -869,12 +867,34 @@ public class WatcherTest {
mSeed = seed;
mRandom = new Random(mSeed);
}
- public int index() {
+ public int next() {
return mRandom.nextInt(50000);
}
public void reset() {
mRandom.setSeed(mSeed);
}
+ // This is an inefficient way to know if a value appears in an array.
+ private boolean contains(int[] s, int length, int k) {
+ for (int i = 0; i < length; i++) {
+ if (s[i] == k) {
+ return true;
+ }
+ }
+ return false;
+ }
+ public int[] indexes(int size) {
+ reset();
+ int[] r = new int[size];
+ for (int i = 0; i < size; i++) {
+ int key = next();
+ // Ensure the list of indices are unique.
+ while (contains(r, i, key)) {
+ key = next();
+ }
+ r[i] = key;
+ }
+ return r;
+ }
}
// Return a value based on the row and column. The algorithm tries to avoid simple
@@ -883,28 +903,8 @@ public class WatcherTest {
return (((row * 4 + col) % 3)& 1) == 1;
}
- // This is an inefficient way to know if a value appears in an array.
- private final boolean contains(int[] s, int length, int k) {
- for (int i = 0; i < length; i++) {
- if (s[i] == k) {
- return true;
- }
- }
- return false;
- }
-
- private void matrixTest(WatchedSparseBooleanMatrix matrix, int size, IndexGenerator indexer) {
- indexer.reset();
- int[] indexes = new int[size];
- for (int i = 0; i < size; i++) {
- int key = indexer.index();
- // Ensure the list of indices are unique.
- while (contains(indexes, i, key)) {
- key = indexer.index();
- }
- indexes[i] = key;
- }
- // Set values in the matrix.
+ // Fill a matrix
+ private void fill(WatchedSparseBooleanMatrix matrix, int size, int[] indexes) {
for (int i = 0; i < size; i++) {
int row = indexes[i];
for (int j = 0; j < size; j++) {
@@ -913,21 +913,39 @@ public class WatcherTest {
matrix.put(row, col, want);
}
}
+ }
- assertEquals(matrix.size(), size);
-
- // Read back and verify
+ // Verify the content of a matrix. This asserts on mismatch. Selected indices may
+ // have been deleted.
+ private void verify(WatchedSparseBooleanMatrix matrix, int[] indexes, boolean[] absent) {
for (int i = 0; i < matrix.size(); i++) {
int row = indexes[i];
for (int j = 0; j < matrix.size(); j++) {
int col = indexes[j];
- boolean want = cellValue(i, j);
- boolean actual = matrix.get(row, col);
- String msg = String.format("matrix(%d:%d, %d:%d) == %s, expected %s",
- i, row, j, col, actual, want);
- assertEquals(msg, actual, want);
+ if (absent != null && (absent[i] || absent[j])) {
+ boolean want = false;
+ String msg = String.format("matrix(%d:%d, %d:%d) (deleted)", i, row, j, col);
+ assertEquals(msg, matrix.get(row, col), false);
+ assertEquals(msg, matrix.get(row, col, false), false);
+ assertEquals(msg, matrix.get(row, col, true), true);
+ } else {
+ boolean want = cellValue(i, j);
+ String msg = String.format("matrix(%d:%d, %d:%d)", i, row, j, col);
+ assertEquals(msg, matrix.get(row, col), want);
+ assertEquals(msg, matrix.get(row, col, false), want);
+ assertEquals(msg, matrix.get(row, col, true), want);
+ }
}
}
+ }
+
+ private void matrixGrow(WatchedSparseBooleanMatrix matrix, int size, IndexGenerator indexer) {
+ int[] indexes = indexer.indexes(size);
+
+ // Set values in the matrix, then read back and verify.
+ fill(matrix, size, indexes);
+ assertEquals(matrix.size(), size);
+ verify(matrix, indexes, null);
// Test the keyAt/indexOfKey methods
for (int i = 0; i < matrix.size(); i++) {
@@ -936,17 +954,101 @@ public class WatcherTest {
}
}
+ private void matrixDelete(WatchedSparseBooleanMatrix matrix, int size, IndexGenerator indexer) {
+ int[] indexes = indexer.indexes(size);
+ fill(matrix, size, indexes);
+
+ // Delete a bunch of rows. Verify that reading back results in false and that
+ // contains() is false. Recreate the rows and verify that all cells (other than
+ // the one just created) are false.
+ boolean[] absent = new boolean[size];
+ for (int i = 0; i < size; i += 13) {
+ matrix.deleteKey(indexes[i]);
+ absent[i] = true;
+ }
+ verify(matrix, indexes, absent);
+ }
+
+ private void matrixShrink(WatchedSparseBooleanMatrix matrix, int size, IndexGenerator indexer) {
+ int[] indexes = indexer.indexes(size);
+ fill(matrix, size, indexes);
+
+ int initialCapacity = matrix.capacity();
+
+ // Delete every other row, remembering which rows were deleted. The goal is to
+ // make room for compaction.
+ boolean[] absent = new boolean[size];
+ for (int i = 0; i < size; i += 2) {
+ matrix.deleteKey(indexes[i]);
+ absent[i] = true;
+ }
+
+ matrix.compact();
+ int finalCapacity = matrix.capacity();
+ assertTrue("Matrix shrink", initialCapacity > finalCapacity);
+ assertTrue("Matrix shrink", finalCapacity - matrix.size() < matrix.STEP);
+ }
+
@Test
public void testWatchedSparseBooleanMatrix() {
final String name = "WatchedSparseBooleanMatrix";
- // The first part of this method tests the core matrix functionality. The second
- // part tests the watchable behavior. The third part tests the snappable
- // behavior.
+ // Test the core matrix functionality. The three tess are meant to test various
+ // combinations of auto-grow.
IndexGenerator indexer = new IndexGenerator(3);
- matrixTest(new WatchedSparseBooleanMatrix(), 10, indexer);
- matrixTest(new WatchedSparseBooleanMatrix(1000), 500, indexer);
- matrixTest(new WatchedSparseBooleanMatrix(1000), 2000, indexer);
+ matrixGrow(new WatchedSparseBooleanMatrix(), 10, indexer);
+ matrixGrow(new WatchedSparseBooleanMatrix(1000), 500, indexer);
+ matrixGrow(new WatchedSparseBooleanMatrix(1000), 2000, indexer);
+ matrixDelete(new WatchedSparseBooleanMatrix(), 500, indexer);
+ matrixShrink(new WatchedSparseBooleanMatrix(), 500, indexer);
+
+ // Test Watchable behavior.
+ WatchedSparseBooleanMatrix matrix = new WatchedSparseBooleanMatrix();
+ WatchableTester tester = new WatchableTester(matrix, name);
+ tester.verify(0, "Initial array - no registration");
+ matrix.put(INDEX_A, INDEX_A, true);
+ tester.verify(0, "Updates with no registration");
+ tester.register();
+ tester.verify(0, "Updates with no registration");
+ matrix.put(INDEX_A, INDEX_B, true);
+ tester.verify(1, "Single cell assignment");
+ matrix.put(INDEX_A, INDEX_B, true);
+ tester.verify(2, "Single cell assignment - same value");
+ matrix.put(INDEX_C, INDEX_B, true);
+ tester.verify(3, "Single cell assignment");
+ matrix.deleteKey(INDEX_B);
+ tester.verify(4, "Delete key");
+ assertEquals(matrix.get(INDEX_B, INDEX_C), false);
+ assertEquals(matrix.get(INDEX_B, INDEX_C, false), false);
+ assertEquals(matrix.get(INDEX_B, INDEX_C, true), true);
+
+ matrix.clear();
+ tester.verify(5, "Clear");
+ assertEquals(matrix.size(), 0);
+ fill(matrix, 10, indexer.indexes(10));
+ int[] keys = matrix.keys();
+ assertEquals(keys.length, matrix.size());
+ for (int i = 0; i < matrix.size(); i++) {
+ assertEquals(matrix.keyAt(i), keys[i]);
+ }
+
+ WatchedSparseBooleanMatrix a = new WatchedSparseBooleanMatrix();
+ matrixGrow(a, 10, indexer);
+ assertEquals(a.size(), 10);
+ WatchedSparseBooleanMatrix b = new WatchedSparseBooleanMatrix();
+ matrixGrow(b, 10, indexer);
+ assertEquals(b.size(), 10);
+ assertEquals(a.equals(b), true);
+ int rowIndex = b.keyAt(3);
+ int colIndex = b.keyAt(4);
+ b.put(rowIndex, colIndex, !b.get(rowIndex, colIndex));
+ assertEquals(a.equals(b), false);
+
+ // Test Snappable behavior.
+ WatchedSparseBooleanMatrix s = a.snapshot();
+ assertEquals(a.equals(s), true);
+ a.put(rowIndex, colIndex, !a.get(rowIndex, colIndex));
+ assertEquals(a.equals(s), false);
}
@Test