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

Commit 972d353f authored by Lee Shombert's avatar Lee Shombert Committed by Automerger Merge Worker
Browse files

Merge "A space-efficient 2D matrix" into sc-dev am: 6b5bbd62

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/14827585

Change-Id: I04e2239dce2e63fbb0bb2568b2756e3b6f83b9b8
parents ed090028 6b5bbd62
Loading
Loading
Loading
Loading
+204 −68
Original line number Diff line number Diff line
@@ -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:
     * <ul>
     * <li> The matrix never shrinks.
     * <li> The matrix does not automatically shrink but there is a compress() method that
     *      will recover unused space.
     * <li> Equality is a very, very expesive operation.
     * </ul>
     */

    /**
     * mOrder is always a multiple of this value.  A  minimal matrix therefore holds 2^12
     * values and requires 1024 bytes.
     * values and requires 1024 bytes.  The value is visible for testing.
     */
    @VisibleForTesting(visibility = PRIVATE)
    static final int STEP = 64;

    /**
     * There are 8 bits in a byte.  The constant is defined here only to make it easy to
     * find in the code.
     */
    private static final int BYTE = 8;

    /**
     * Constants that index into the string array returned by matrixToString.  The primary
     * consumer is test code.
     */
    private static final int STEP = 64;
    static final int STRING_KEY_INDEX = 0;
    static final int STRING_MAP_INDEX = 1;
    static final int STRING_INUSE_INDEX = 2;

    /**
     * The order of the matrix storage, including any padding.  The matrix is always
@@ -103,7 +123,7 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
    /**
     * The boolean array.  This array is always {@code mOrder x mOrder} in size.
     */
    private boolean[] mValues;
    private byte[] mValues;

    /**
     * A convenience function called when the elements are added to or removed from the storage.
@@ -140,7 +160,7 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
        mInUse = new boolean[mOrder];
        mKeys = ArrayUtils.newUnpaddedIntArray(mOrder);
        mMap = ArrayUtils.newUnpaddedIntArray(mOrder);
        mValues = new boolean[mOrder * mOrder];
        mValues = new byte[mOrder * mOrder / 8];
        mSize = 0;
    }

@@ -207,7 +227,7 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
        }
        if (r >= 0 && c >= 0) {
            setValueAt(r, c, value);
            onChanged();
            // setValueAt() will call onChanged().
        } else {
            throw new RuntimeException("matrix overflow");
        }
@@ -232,8 +252,12 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
    public void removeAt(int index) {
        validateIndex(index);
        mInUse[mMap[index]] = false;
        // Remove the specified index and ensure that unused words in mKeys and mMap are
        // always zero, to simplify the equality function.
        System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
        mKeys[mSize - 1] = 0;
        System.arraycopy(mMap, index + 1, mMap, index, mSize - (index + 1));
        mMap[mSize - 1] = 0;
        mSize--;
        onChanged();
    }
@@ -271,6 +295,17 @@ public class WatchedSparseBooleanMatrix extends WatchableImpl implements Snappab
        return mKeys[index];
    }

    /**
     * An internal method to fetch the boolean value given the mValues row and column
     * indices.  These are not the indices used by the *At() methods.
     */
    private boolean valueAtInternal(int row, int col) {
        int element = row * mOrder + col;
        int offset = element / BYTE;
        int mask = 1 << (element % BYTE);
        return (mValues[offset] & mask) != 0;
    }

    /**
     * Given a row and column, each in the range <code>0...size()-1</code>, returns the
     * value from the <code>index</code>th 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);
    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;
    }

        boolean[] newValues = new boolean[newOrder * 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++) {
            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];
                if ((mValues[srcOffset] & srcMask) == 0) {
                    mValues[dstOffset] &= ~dstMask;
                } else {
                    mValues[dstOffset] |= dstMask;
                }
                srcOffset += mOrder / BYTE;
                dstOffset += mOrder / BYTE;
            }
        }
    }

        mInUse = newInuse;
        mValues = newValues;
        mOrder = newOrder;
    /**
     * 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]) {
        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);
        }
+141 −39
Original line number Diff line number Diff line
@@ -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,22 +867,14 @@ 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);
        }
    }

    // Return a value based on the row and column.  The algorithm tries to avoid simple
    // patterns like checkerboard.
    private final boolean cellValue(int row, int col) {
        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) {
        private boolean contains(int[] s, int length, int k) {
            for (int i = 0; i < length; i++) {
                if (s[i] == k) {
                    return true;
@@ -892,19 +882,29 @@ public class WatcherTest {
            }
            return false;
        }

    private void matrixTest(WatchedSparseBooleanMatrix matrix, int size, IndexGenerator indexer) {
        indexer.reset();
        int[] indexes = new int[size];
        public int[] indexes(int size) {
            reset();
            int[] r = new int[size];
            for (int i = 0; i < size; i++) {
            int key = indexer.index();
                int key = next();
                // Ensure the list of indices are unique.
            while (contains(indexes, i, key)) {
                key = indexer.index();
                while (contains(r, i, key)) {
                    key = next();
                }
                r[i] = key;
            }
            return r;
        }
            indexes[i] = key;
    }
        // Set values in the matrix.

    // Return a value based on the row and column.  The algorithm tries to avoid simple
    // patterns like checkerboard.
    private final boolean cellValue(int row, int col) {
        return (((row * 4 + col) % 3)& 1) == 1;
    }

    // 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,22 +913,40 @@ 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];
                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);
                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);
                    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++) {
            int key = indexes[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