blob: aa0d6c9bc32bdae9f9202e53ebfde18f57f9113b [file] [log] [blame]
/*
* Copyright (C) 2015 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 android.support.constraint.solver;
import android.support.constraint.solver.widgets.ConstraintAnchor;
import java.util.Arrays;
import java.util.HashMap;
/**
* Represents and solve a system of linear equations.
*/
public class LinearSystem {
private static final boolean DEBUG = false;
/*
* Default size for the object pools
*/
private static final int POOL_SIZE = 1000;
/*
* Variable counter
*/
private int mVariablesID = 0;
/*
* Store a map between name->SolverVariable and SolverVariable->Float for the resolution.
*/
private HashMap<String, SolverVariable> mVariables = null;
/*
* The goal that is used when minimizing the system.
*/
private ArrayRow mGoal;
private int TABLE_SIZE = 32; // default table size for the allocation
private int mMaxColumns = TABLE_SIZE;
private ArrayRow[] mRows = null;
// Used in optimize()
private boolean[] mAlreadyTestedCandidates = new boolean[TABLE_SIZE];
int mNumColumns = 1;
int mNumRows = 0;
int mMaxRows = TABLE_SIZE;
final private Cache mCache;
private SolverVariable[] mPoolVariables = new SolverVariable[POOL_SIZE];
private int mPoolVariablesCount = 0;
public LinearSystem() {
mRows = new ArrayRow[TABLE_SIZE];
releaseRows();
mCache = new Cache();
}
/*--------------------------------------------------------------------------------------------*/
// Memory management
/*--------------------------------------------------------------------------------------------*/
/**
* Reallocate memory to accommodate increased amount of variables
*/
void increaseTableSize() {
TABLE_SIZE *= 2;
mRows = Arrays.copyOf(mRows, TABLE_SIZE);
mCache.mIndexedVariables = Arrays.copyOf(mCache.mIndexedVariables, TABLE_SIZE);
mAlreadyTestedCandidates = new boolean[TABLE_SIZE];
mMaxColumns = TABLE_SIZE;
mMaxRows = TABLE_SIZE;
releaseGoal();
mGoal = null;
}
/**
* Release ArrayRows back to their pool
*/
private void releaseRows() {
for (int i = 0; i < mRows.length; i++) {
ArrayRow row = (ArrayRow) mRows[i];
if (row != null) {
mCache.arrayRowPool.release(row);
}
mRows[i] = null;
}
}
/**
* Release the ArrayRow used for the goal back to the pool
*/
private void releaseGoal() {
if (mGoal != null) {
mCache.arrayRowPool.release((ArrayRow) mGoal);
}
}
/**
* Reset the LinearSystem object so that it can be reused.
*/
public void reset() {
for (int i = 0; i < mCache.mIndexedVariables.length; i++) {
SolverVariable variable = mCache.mIndexedVariables[i];
if (variable != null) {
variable.reset();
}
}
mCache.solverVariablePool.releaseAll(mPoolVariables, mPoolVariablesCount);
mPoolVariablesCount = 0;
Arrays.fill(mCache.mIndexedVariables, null);
if (mVariables != null) {
mVariables.clear();
}
mVariablesID = 0;
releaseGoal();
mGoal = null;
mNumColumns = 1;
for (int i = 0; i < mNumRows; i++) {
mRows[i].used = false;
}
releaseRows();
mNumRows = 0;
}
/*--------------------------------------------------------------------------------------------*/
// Creation of rows / variables / errors
/*--------------------------------------------------------------------------------------------*/
/**
* Add the equation to the system
* @param e the equation we want to add.
*/
public void addConstraint(LinearEquation e) {
ArrayRow row = EquationCreation.createRowFromEquation(this, e);
addConstraint(row);
}
public SolverVariable createObjectVariable(Object anchor) {
if (anchor == null) {
return null;
}
if (mNumColumns + 1 >= mMaxColumns) {
increaseTableSize();
}
SolverVariable variable = null;
if (anchor instanceof ConstraintAnchor) {
variable = ((ConstraintAnchor) anchor).getSolverVariable();
if (variable == null) {
((ConstraintAnchor) anchor).resetSolverVariable(mCache);
variable = ((ConstraintAnchor) anchor).getSolverVariable();
}
if (variable.id == -1
|| variable.id > mVariablesID
|| mCache.mIndexedVariables[variable.id] == null) {
if (variable.id != -1) {
variable.reset();
}
mVariablesID++;
mNumColumns++;
variable.id = mVariablesID;
variable.mType = SolverVariable.Type.UNRESTRICTED;
mCache.mIndexedVariables[mVariablesID] = variable;
}
}
return variable;
}
public ArrayRow createRow() {
ArrayRow row = mCache.arrayRowPool.acquire();
if (row == null) {
row = new ArrayRow(mCache);
} else {
row.reset();
}
return row;
}
public SolverVariable createSlackVariable() {
if (mNumColumns + 1 >= mMaxColumns) {
increaseTableSize();
}
SolverVariable variable = acquireSolverVariable(SolverVariable.Type.SLACK);
mVariablesID++;
mNumColumns++;
variable.id = mVariablesID;
mCache.mIndexedVariables[mVariablesID] = variable;
return variable;
}
void addError(ArrayRow row) {
SolverVariable error1 = createErrorVariable();
SolverVariable error2 = createErrorVariable();
row.addError(error1, error2);
}
void addSingleError(ArrayRow row, int sign) {
SolverVariable error = createErrorVariable();
row.addSingleError(error, sign);
}
private SolverVariable createVariable(String name, SolverVariable.Type type) {
if (mNumColumns + 1 >= mMaxColumns) {
increaseTableSize();
}
SolverVariable variable = acquireSolverVariable(type);
variable.setName(name);
mVariablesID++;
mNumColumns++;
variable.id = mVariablesID;
if (mVariables == null) {
mVariables = new HashMap<>();
}
mVariables.put(name, variable);
mCache.mIndexedVariables[mVariablesID] = variable;
return variable;
}
private SolverVariable createErrorVariable() {
if (mNumColumns + 1 >= mMaxColumns) {
increaseTableSize();
}
SolverVariable variable = acquireSolverVariable(SolverVariable.Type.ERROR);
mVariablesID++;
mNumColumns++;
variable.id = mVariablesID;
mCache.mIndexedVariables[mVariablesID] = variable;
return variable;
}
/**
* Returns a SolverVariable instance of the given type
* @param type type of the SolverVariable
* @return instance of SolverVariable
*/
private final SolverVariable acquireSolverVariable(SolverVariable.Type type) {
SolverVariable variable = mCache.solverVariablePool.acquire();
if (variable == null) {
variable = new SolverVariable(mCache, type);
}
variable.reset();
variable.setType(type);
mPoolVariables[mPoolVariablesCount++] = variable;
return variable;
}
/*--------------------------------------------------------------------------------------------*/
// Accessors of rows / variables / errors
/*--------------------------------------------------------------------------------------------*/
/**
* Simple accessor for the current goal. Used when minimizing the system's goal.
* @return the current goal.
*/
public ArrayRow getGoal() { return mGoal; }
public ArrayRow getRow(int n) {
return mRows[n];
}
public float getValueFor(String name) {
SolverVariable v = getVariable(name, SolverVariable.Type.UNRESTRICTED);
if (v == null) {
return 0;
}
return v.computedValue;
}
public int getObjectVariableValue(Object anchor) {
SolverVariable variable = ((ConstraintAnchor) anchor).getSolverVariable();
if (variable != null) {
return (int) (variable.computedValue + 0.5f);
}
return 0;
}
/**
* Returns a SolverVariable instance given a name and a type.
*
* @param name name of the variable
* @param type {@link SolverVariable.Type type} of the variable
* @return a SolverVariable instance
*/
public SolverVariable getVariable(String name, SolverVariable.Type type) {
if (mVariables == null) {
mVariables = new HashMap<>();
}
SolverVariable variable = mVariables.get(name);
if (variable == null) {
variable = createVariable(name, type);
}
return variable;
}
/*--------------------------------------------------------------------------------------------*/
// System resolution
/*--------------------------------------------------------------------------------------------*/
/**
* Rebuild the goal from the errors and slack variables
*/
public void rebuildGoalFromErrors() {
if (mGoal != null) {
mGoal.reset();
} else {
mGoal = createRow();
}
for (int i = 1; i < mNumColumns; i++) {
SolverVariable variable = mCache.mIndexedVariables[i];
if (variable.mType == SolverVariable.Type.ERROR
/* || variable.getType() == SolverVariable.Type.SLACK */) {
mGoal.variables.put(variable, 1.f);
}
}
if (DEBUG) {
System.out.println("system after rebuilding: ");
displayReadableRows();
System.out.println("rebuilt goal from errors: " + mGoal);
}
}
/**
* Minimize the current goal of the system.
*/
public void minimize() throws Exception {
rebuildGoalFromErrors();
minimizeGoal(mGoal);
}
/**
* Minimize the given goal with the current system.
* @param goal the goal to minimize.
*/
public void minimizeGoal(ArrayRow goal) throws Exception {
// Update the equation with the variables already defined in the system
if (ArrayRow.USE_LINKED_VARIABLES) {
goal.variables.updateFromSystem(goal, mRows);
} else {
for (int i = 0; i < mNumRows; i++) {
goal.updateRowWithEquation(mRows[i]);
}
}
boolean validGoal = goal.hasAtLeastOnePositiveVariable();
if (!validGoal) {
computeValues();
return;
}
if (DEBUG) {
System.out.println("Minimize goal " + goal);
displayReadableRows();
}
try {
// First, let's make sure that the system is in Basic Feasible Solved Form (BFS), i.e.
// all the constants of the restricted variables should be positive.
int tries = enforceBFS(goal);
if (DEBUG) {
System.out.println("System in BFS ( " + tries + ") " + goal.toReadableString());
}
// The system at this point is supposed to be in Basic Feasible Solved Form (BFS),
// so the only thing we have to do here is pivot on any candidates variables in the goal,
// i.e. any variables that are negative (as they would decrease the goal's result).
tries = optimize(goal);
if (DEBUG) {
System.out.println("Goal minimized ( " + tries + ") " + goal.toReadableString());
}
computeValues();
} catch (Exception e) {
computeValues();
throw e;
}
}
SolverVariable[] tempVars = new SolverVariable[256];
/**
* Update the equation with the variables already defined in the system
* @param row row to update
*/
private void updateRowFromVariables(ArrayRow row) {
if (ArrayRow.USE_LINKED_VARIABLES) {
if (mNumRows > 0) {
row.variables.updateFromSystem(row, mRows);
if (row.variables.currentSize == 0) {
row.isSimpleDefinition = true;
}
}
} else {
int numVariables = row.variables.currentSize;
if (numVariables > tempVars.length) {
tempVars = new SolverVariable[tempVars.length * 2];
}
int added = 0;
for (int i = 0; i < numVariables; i++) {
SolverVariable variable = row.variables.getVariable(i);
if (variable != null && variable.definitionId != -1) {
tempVars[added] = variable;
added++;
}
}
for (int i = 0; i < added; i++) {
int idx = tempVars[i].definitionId;
row.updateRowWithEquation(mRows[idx]);
}
}
}
private ArrayRow[] tempClientsCopy = new ArrayRow[32];
/**
* Add the equation to the system
* @param row the equation we want to add expressed as a system row.
*/
public void addConstraint(ArrayRow row) {
if (row == null) {
return;
}
if (mNumRows + 1 >= mMaxRows || mNumColumns + 1 >= mMaxColumns) {
increaseTableSize();
}
if (DEBUG) {
System.out.println("addConstraint: " + row.toReadableString());
}
// Update the equation with the variables already defined in the system
updateRowFromVariables(row);
// First, ensure that if we have a constant it's positive
row.ensurePositiveConstant();
if (DEBUG) {
System.out.println("addConstraint, updated row : " + row.toReadableString());
}
// Then pick a good variable to use for the row
row.pickRowVariable();
if (!row.hasKeyVariable()) {
// Can happen if row resolves to nil
if (DEBUG) {
System.out.println("No variable found to pivot on " + row.toReadableString());
displayReadableRows();
}
// We haven't found a variable to pivot on. Normally, we should introduce a new stay
// variable to solve the system, then solve, and possibly remove the stay variable.
// But the equations we insert have (for this exact purpose) balanced +/- variables,
// so should not be necessary. Let's simply exit.
return;
}
if (mRows[mNumRows] != null) {
mCache.arrayRowPool.release(mRows[mNumRows]);
}
row.updateClientEquations();
mRows[mNumRows] = row;
row.variable.definitionId = mNumRows;
mNumRows++;
final int count = row.variable.mClientEquationsCount;
if (count > 0) {
while (tempClientsCopy.length < count) {
tempClientsCopy = new ArrayRow[tempClientsCopy.length * 2];
}
ArrayRow[] clients = tempClientsCopy;
if (SolverVariable.USE_LIST) {
for (int i = 0; i < count; i++) {
clients[i] = row.variable.mClientEquations[i];
}
SolverVariable.Link current = (SolverVariable.Link) (Object) row.variable.mClientEquations;
int n = 0;
while (current.next != null) {
clients[n++] = current.next.row;
current = current.next;
}
} else {
System.arraycopy(row.variable.mClientEquations, 0, clients, 0, count);
}
for (int i = 0; i < count; i++) {
ArrayRow client = clients[i];
if (client == row) {
continue;
}
if (ArrayRow.USE_LINKED_VARIABLES) {
client.variables.updateFromRow(client, row);
} else {
client.updateRowWithEquation(row);
}
client.updateClientEquations();
}
}
if (DEBUG) {
System.out.println("Row added, here is the system:");
displayReadableRows();
}
}
/**
* Optimize the system given a goal to minimize. The system should be in BFS form.
* @param goal goal to optimize.
* @return number of iterations.
*/
private int optimize(ArrayRow goal) {
boolean done = false;
int tries = 0;
for (int i = 0; i < mNumColumns; i++) {
mAlreadyTestedCandidates[i] = false;
}
int tested = 0;
while (!done) {
tries++;
if (DEBUG) {
System.out.println("iteration on system " + tries);
}
SolverVariable pivotCandidate = goal.variables.getPivotCandidate();
if (pivotCandidate != null) {
if (mAlreadyTestedCandidates[pivotCandidate.id]) {
pivotCandidate = null;
} else {
mAlreadyTestedCandidates[pivotCandidate.id] = true;
tested++;
if (tested >= mNumColumns) {
done = true;
}
}
}
if (pivotCandidate != null) {
// there's a negative variable in the goal that we can pivot on.
// We now need to select which equation of the system we should do
// the pivot on.
// Let's try to find the equation in the system that we can pivot on.
// The rules are simple:
// - only look at restricted variables equations (i.e. Cs)
// - only look at equations containing the column we are trying to pivot on (duh)
// - select preferably an equation with strong strength over weak strength
float minWeak = Float.MAX_VALUE;
float minStrong = Float.MAX_VALUE;
int pivotRowIndex;
int pivotRowIndexWeak = -1;
int pivotRowIndexStrong = -1;
for (int i = 0; i < mNumRows; i++) {
ArrayRow current = mRows[i];
SolverVariable variable = current.variable;
if (variable.mType == SolverVariable.Type.UNRESTRICTED) {
// skip unrestricted variables equations.
continue;
}
if (current.hasVariable(pivotCandidate)) {
// the current row does contains the variable
// we want to pivot on
float C = current.constantValue;
float a_j = current.variables.get(pivotCandidate);
if (a_j < 0) {
float value = (C * -1) / a_j;
if (pivotCandidate.mStrength ==
SolverVariable.Strength.STRONG) {
if (value < minStrong) {
minStrong = value;
pivotRowIndexStrong = i;
}
} else {
if (value < minWeak) {
minWeak = value;
pivotRowIndexWeak = i;
}
}
}
}
}
if (pivotRowIndexStrong > -1) {
pivotRowIndex = pivotRowIndexStrong;
} else {
pivotRowIndex = pivotRowIndexWeak;
}
// At this point, we ought to have an equation to pivot on,
// either weak or strong.
if (pivotRowIndex > -1) {
// We found an equation to pivot on
if (DEBUG) {
System.out.println("We pivot on " + pivotRowIndex);
}
ArrayRow pivotEquation = mRows[pivotRowIndex];
pivotEquation.variable.definitionId = -1;
pivotEquation.pivot(pivotCandidate);
pivotEquation.variable.definitionId = pivotRowIndex;
// let's update the system with the new pivoted equation
for (int i = 0; i < mNumRows; i++) {
mRows[i].updateRowWithEquation(pivotEquation);
}
// let's update the goal equation as well
goal.updateRowWithEquation(pivotEquation);
if (DEBUG) {
System.out.println("new goal after pivot: " + goal);
}
// now that we pivoted, we're going to continue looping on the next goal
// columns, until we exhaust all the possibilities of improving the system
} else {
// We couldn't find an equation to pivot, we should exit the loop.
done = true;
}
} else {
// There is no candidate goals columns we should try to pivot on,
// so let's exit the loop.
done = true;
}
}
return tries;
}
/**
* Make sure that the system is in Basic Feasible Solved form (BFS).
* @param goal
* @return number of iterations
*/
private int enforceBFS(ArrayRow goal) throws Exception {
int tries = 0;
boolean done;
// At this point, we might not be in Basic Feasible Solved form (BFS),
// i.e. one of the restricted equation has a negative constant.
// Let's check if that's the case or not.
boolean infeasibleSystem = false;
for (int i = 0; i < mNumRows; i++) {
SolverVariable variable = mRows[i].variable;
if (variable.mType == SolverVariable.Type.UNRESTRICTED) {
continue; // C can be either positive or negative.
}
if (mRows[i].constantValue < 0) {
infeasibleSystem = true;
break;
}
}
// The system happens to not be in BFS form, we need to go back to it to properly solve it.
if (infeasibleSystem) {
if (DEBUG) {
System.out.println("the current system is infeasible, let's try to fix this.");
}
// Going back to BFS form can be done by selecting any equations in Cs containing
// a negative constant, then selecting a potential pivot variable that would remove
// this negative constant. Once we have
done = false;
tries = 0;
while (!done) {
tries++;
if (DEBUG) {
System.out.println("iteration on infeasible system " + tries);
}
float minWeak = Float.MAX_VALUE;
float minStrong = Float.MAX_VALUE;
int pivotRowIndexWeak = -1;
int pivotRowIndexStrong = -1;
int pivotRowIndex;
int pivotColumnIndexStrong = -1;
int pivotColumnIndexWeak = -1;
int pivotColumnIndex;
for (int i = 0; i < mNumRows; i++) {
ArrayRow current = mRows[i];
SolverVariable variable = current.variable;
if (variable.mType == SolverVariable.Type.UNRESTRICTED) {
// skip unrestricted variables equations, as C
// can be either positive or negative.
continue;
}
if (current.constantValue < 0) {
// let's examine this row, see if we can find a good pivot
for (int j = 1; j < mNumColumns; j++) {
SolverVariable candidate = mCache.mIndexedVariables[j];
float a_j = current.variables.get(candidate);
if (a_j <= 0) {
continue;
}
float d_j = goal.variables.get(candidate);
float value = d_j / a_j;
if (variable.mStrength == SolverVariable.Strength.STRONG) {
if (value < minStrong) {
minStrong = value;
pivotRowIndexStrong = i;
pivotColumnIndexStrong = j;
}
} else {
if (value < minWeak) {
minWeak = value;
pivotRowIndexWeak = i;
pivotColumnIndexWeak = j;
}
}
}
}
}
if (pivotRowIndexStrong != -1) {
pivotRowIndex = pivotRowIndexStrong;
pivotColumnIndex = pivotColumnIndexStrong;
} else {
pivotRowIndex = pivotRowIndexWeak;
pivotColumnIndex = pivotColumnIndexWeak;
}
if (pivotRowIndex != -1) {
// We have a pivot!
ArrayRow pivotEquation = mRows[pivotRowIndex];
pivotEquation.variable.definitionId = -1;
pivotEquation.pivot(mCache.mIndexedVariables[pivotColumnIndex]);
pivotEquation.variable.definitionId = pivotRowIndex;
// let's update the system with the new pivoted equation
for (int i = 0; i < mNumRows; i++) {
mRows[i].updateRowWithEquation(pivotEquation);
}
// let's update the goal equation as well
// TODO: might not be necessary here
goal.updateRowWithEquation(pivotEquation);
if (DEBUG) {
System.out.println("new goal after pivot: " + goal);
}
} else {
done = true;
}
}
}
if (DEBUG) {
System.out.println("the current system should now be feasible [" + infeasibleSystem + "] after " + tries + " iterations");
displayReadableRows();
}
// Let's make sure the system is correct
infeasibleSystem = false;
for (int i = 0; i < mNumRows; i++) {
SolverVariable variable = mRows[i].variable;
if (variable.mType == SolverVariable.Type.UNRESTRICTED) {
continue; // C can be either positive or negative.
}
if (mRows[i].constantValue < 0) {
infeasibleSystem = true;
break;
}
}
if (infeasibleSystem) {
// throw new Exception();
}
return tries;
}
private void computeValues() {
for (int i = 0; i < mNumRows; i++) {
ArrayRow row = mRows[i];
row.variable.computedValue = row.constantValue;
}
}
/**
* Replaces a given variable of target with its definition coming from the system
* (if there is one)
* @param target target row
* @param variable variable to replace
*/
void replaceVariable(ArrayRow target, SolverVariable variable) {
int idx = variable.definitionId;
if (idx != -1) {
target.updateRowWithEquation(mRows[idx]);
}
}
/*--------------------------------------------------------------------------------------------*/
// Display utility functions
/*--------------------------------------------------------------------------------------------*/
public void displayRows() {
displaySolverVariables();
String s = "";
for (int i = 0; i < mNumRows; i++) {
s += mRows[i];
s += "\n";
}
if (mGoal != null) {
s += mGoal + "\n";
}
System.out.println(s);
}
public void displayReadableRows() {
displaySolverVariables();
String s = "";
for (int i = 0; i < mNumRows; i++) {
s += mRows[i].toReadableString();
s += "\n";
}
if (mGoal != null) {
s += mGoal.toReadableString() + "\n";
}
System.out.println(s);
}
public void displayVariablesReadableRows() {
displaySolverVariables();
String s = "";
for (int i = 0; i < mNumRows; i++) {
if (mRows[i].variable.mType == SolverVariable.Type.UNRESTRICTED) {
s += mRows[i].toReadableString();
s += "\n";
}
}
if (mGoal != null) {
s += mGoal.toReadableString() + "\n";
}
System.out.println(s);
}
public int getMemoryUsed() {
int actualRowSize = 0;
for (int i = 0; i < mNumRows; i++) {
if (mRows[i] != null) {
actualRowSize += mRows[i].sizeInBytes();
}
}
return actualRowSize;
}
public int getNumEquations() { return mNumRows; }
public int getNumVariables() { return mVariablesID; }
/**
* Display current system informations
*/
public void displaySystemInformations() {
int count = 0;
int rowSize = 0;
for (int i = 0; i < TABLE_SIZE; i++) {
if (mRows[i] != null) {
rowSize += mRows[i].sizeInBytes();
}
}
int actualRowSize = 0;
for (int i = 0; i < mNumRows; i++) {
if (mRows[i] != null) {
actualRowSize += mRows[i].sizeInBytes();
}
}
System.out.println("Linear System -> Table size: " + TABLE_SIZE
+ " (" + getDisplaySize(TABLE_SIZE * TABLE_SIZE)
+ ") -- row sizes: " + getDisplaySize(rowSize)
+ ", actual size: " + getDisplaySize(actualRowSize)
+ " rows: " + mNumRows + "/" + mMaxRows
+ " cols: " + mNumColumns + "/" + mMaxColumns
+ " " + count + " occupied cells, " + getDisplaySize(count)
+ " " + LinkedVariables.sCreation + " created Link variables"
);
}
private void displaySolverVariables() {
String s = "Display Rows (" + mNumRows + "x" + mNumColumns + ") :\n\t | C | ";
for (int i = 1; i <= mNumColumns; i++) {
SolverVariable v = mCache.mIndexedVariables[i];
s += v;
s += " | ";
}
s += "\n";
System.out.println(s);
}
private String getDisplaySize(int n) {
int mb = (n * 4) / 1024 / 1024;
if (mb > 0) {
return "" + mb + " Mb";
}
int kb = (n * 4) / 1024;
if (kb > 0) {
return "" + kb + " Kb";
}
return "" + (n * 4) + " bytes";
}
public Cache getCache() {
return mCache;
}
}