/*
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.imagepipeline.memory;
import javax.annotation.concurrent.GuardedBy;
import javax.annotation.concurrent.NotThreadSafe;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import android.annotation.SuppressLint;
import android.util.SparseArray;
import android.util.SparseIntArray;
import com.facebook.common.internal.Preconditions;
import com.facebook.common.internal.Sets;
import com.facebook.common.internal.Throwables;
import com.facebook.common.internal.VisibleForTesting;
import com.facebook.common.logging.FLog;
import com.facebook.common.memory.MemoryTrimType;
import com.facebook.common.memory.MemoryTrimmableRegistry;
/**
* A base pool class that manages a pool of values (of type V). <p>
* The pool is organized as a map. Each entry in the map is a free-list (modeled by a queue) of
* entries for a given size.
* Some pools have a fixed set of buckets (aka bucketized sizes), while others don't.
* <p>
* The pool supports two main operations:
* <ul>
* <li> {@link #get(int)} - returns a value of size that's the same or larger than specified, hopefully
* from the pool; otherwise, this value is allocated (via the alloc function)</li>
* <li> {@link #release(V)} - releases a value to the pool</li>
* </ul>
* In addition, the pool subscribes to the {@link MemoryTrimmableRegistry}, and responds to
* low-memory events (calls to trim). Some percent (perhaps all) of the values in the pool are then
* released (via the underlying free function), and no longer belong to the pool.
* <p>
* Sizes
* There are 3 different notions of sizes we consider here (not all of them may be relevant for
* each use case).
* <ul>
* <li>Logical size is simply the size of the value in terms appropriate for the value. For
* example, for byte arrays, the size is simply the length. For a bitmap, the size is just the
* number of pixels.</li>
* <li>Bucketed size typically represents one of discrete set of logical sizes - such that each
* bucketed size can accommodate a range of logical sizes. For example, for byte arrays, using
* sizes that are powers of 2 for bucketed sizes allows these byte arrays to support a number
* of logical sizes.</li>
* <li>Finally, Size-in-bytes is exactly that - the size of the value in bytes.</li>
* </ul>
* Logical Size and BucketedSize are both represented by the type parameter S, while size-in-bytes
* is represented by an int.
* <p>
* Each concrete subclass of the pool must implement the following methods
* <ul>
* <li>{@link #getBucketedSize(int)} - returns the bucketized size for the given request size</li>
* <li>{@link #getBucketedSizeForValue(Object)} - returns the bucketized size for a given
* value</li>
* <li>{@link #getSizeInBytes(int)} - gets the size in bytes for a given bucketized size</li>
* <li>{@link #alloc(int)} - allocates a value of given size</li>
* <li>{@link #free(Object)} - frees the value V</li>
* Subclasses may optionally implement
* <li>{@link #onParamsChanged()} - called whenever this class determines to re-read the pool
* params</li>
* <li>{@link #isReusable(Object)} - used to determine if a value can be reused or must be
* freed</li>
* </ul>
* <p>
* InUse values
* The pool keeps track of values currently in use (in addition to the free values in the buckets).
* This is maintained in an IdentityHashSet (using reference equality for the values). The in-use
* set helps with accounting/book-keeping; we also use this during {@link #release(Object)} to avoid
* messing with (freeing/reusing) values that are 'unknown' to the pool.
* <p>
* PoolParams
* Pools are "configured" with a set of parameters (the PoolParams) supplied via a provider.
* This set of parameters includes
* <ul>
* <li> {@link PoolParams#maxSizeSoftCap}
* The size of a pool includes its used and free space. The maxSize setting
* for a pool is a soft cap on the overall size of the pool. A key point is that {@link #get(int)}
* requests will not fail because the max size has been exceeded (unless the underlying
* {@link #alloc(int)} function fails). However, the pool's free portion will be trimmed
* as much as possible so that the pool's size may fall below the max size. Note that when the
* free portion has fallen to zero, the pool may still be larger than its maxSizeSoftCap.
* On a {@link #release(Object)} request, the value will be 'freed' instead of being added to
* the free portion of the pool, if the pool exceeds its maxSizeSoftCap.
* The invariant we want to maintain - see {@link #ensurePoolSizeInvariant()} - is that the pool
* must be below the max size soft cap OR the free lists must be empty. </li>
* <li> {@link PoolParams#maxSizeHardCap}
* The hard cap is a stronger limit on the pool size. When this limit is reached, we first
* attempt to trim the pool. If the pool size is still over the hard, the
* {@link #get(int)} call will fail with a {@link PoolSizeViolationException} </li>
* <li> {@link PoolParams#bucketSizes}
* The pool can be configured with a set of 'sizes' - a bucket is created for each such size.
* Additionally, each bucket can have a a max-length specified, which is the sum of the used and
* free items in that bucket. As with the MaxSize parameter above, the maxLength here is a soft
* cap, in that it will not cause an exception on get; it simply controls the release path.
* If the BucketSizes parameter is null, then the pool will dynamically create buckets on demand.
* </li>
* </ul>
*/
public abstract class BasePool<V> implements Pool<V> {
private final Class<?> TAG = this.getClass();
/**
* The memory manager to register with
*/
final MemoryTrimmableRegistry mMemoryTrimmableRegistry;
/**
* Provider for pool parameters
*/
final PoolParams mPoolParams;
/**
* The buckets - representing different 'sizes'
*/
@VisibleForTesting
final SparseArray<Bucket<V>> mBuckets;
/**
* An Identity hash-set to keep track of values by reference equality
*/
@VisibleForTesting
final Set<V> mInUseValues;
/**
* Determines if new buckets can be created
*/
private boolean mAllowNewBuckets;
/**
* tracks 'used space' - space allocated via the pool
*/
@VisibleForTesting
@GuardedBy("this")
final Counter mUsed;
/**
* tracks 'free space' in the pool
*/
@VisibleForTesting
@GuardedBy("this")
final Counter mFree;
/**
* tracks memory "reserved" for allocation. It is expected that this is done
* after a call to canAllocate() and will be decremented immediately after a
* call to alloc either on a success or failure.
*/
@GuardedBy("this")
private int mReservedBytes;
private final PoolStatsTracker mPoolStatsTracker;
/**
* Creates a new instance of the pool.
* @param poolParams pool parameters
* @param poolStatsTracker
*/
public BasePool(
MemoryTrimmableRegistry memoryTrimmableRegistry,
PoolParams poolParams,
PoolStatsTracker poolStatsTracker) {
mMemoryTrimmableRegistry = Preconditions.checkNotNull(memoryTrimmableRegistry);
mPoolParams = Preconditions.checkNotNull(poolParams);
mPoolStatsTracker = Preconditions.checkNotNull(poolStatsTracker);
// initialize the buckets
mBuckets = new SparseArray<Bucket<V>>();
initBuckets(new SparseIntArray(0));
mInUseValues = Sets.newIdentityHashSet();
mFree = new Counter();
mUsed = new Counter();
}
/**
* Finish pool initialization.
*/
protected void initialize() {
mMemoryTrimmableRegistry.registerMemoryTrimmable(this);
mPoolStatsTracker.setBasePool(this);
}
/**
* Gets a new 'value' from the pool, if available. Allocates a new value if necessary.
* If we need to perform an allocation,
* - If the pool size exceeds the max-size soft cap, then we attempt to trim the free portion
* of the pool.
* - If the pool size exceeds the max-size hard-cap (after trimming), then we throw an
* {@link PoolSizeViolationException}
* Bucket length constraints are not considered in this function
* @param size the logical size to allocate
* @return a new value
* @throws InvalidSizeException
*/
public V get(int size) {
ensurePoolSizeInvariant();
int bucketedSize = getBucketedSize(size);
Bucket<V> bucket = getBucket(bucketedSize);
int sizeInBytes = -1;
synchronized (this) {
if (bucket != null) {
// find an existing value that we can reuse
V value = bucket.get();
if (value != null) {
Preconditions.checkState(mInUseValues.add(value));
// It is possible that we got a 'larger' value than we asked for.
// lets recompute size in bytes here
bucketedSize = getBucketedSizeForValue(value);
sizeInBytes = getSizeInBytes(bucketedSize);
mUsed.increment(sizeInBytes);
mFree.decrement(sizeInBytes);
mPoolStatsTracker.onValueReuse(sizeInBytes);
logStats();
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(
TAG,
"get (reuse) (object, size) = (%x, %s)",
System.identityHashCode(value),
bucketedSize);
}
return value;
}
// fall through
}
// check to see if we can allocate a value of the given size without exceeding the hard cap
sizeInBytes = getSizeInBytes(bucketedSize);
if (!canAllocate(sizeInBytes)) {
throw new PoolSizeViolationException(
mPoolParams.maxSizeHardCap,
mUsed.mNumBytes,
mFree.mNumBytes,
sizeInBytes);
}
// the allocation can succeed. So reserve the bytes to prevent another
// call to get() to not succeed by mistake.
mReservedBytes += sizeInBytes;
}
V value = null;
try {
// allocate the value outside the synchronized block, because it can be pretty expensive
// we could have done the allocation inside the synchronized block,
// but that would have blocked out other operations on the pool
value = alloc(bucketedSize);
} catch (Throwable e) {
// Remove this from reserved byte count if this alloc failed,
// without altering the code flow
synchronized (this) {
Preconditions.checkArgument(mReservedBytes >= sizeInBytes);
mReservedBytes -= sizeInBytes;
}
Throwables.propagateIfPossible(e);
}
// NOTE: We checked for hard caps earlier, and then did the alloc above. Now we need to
// update state - but it is possible that a concurrent thread did a similar operation - with
// the result being that we're now over the hard cap.
// We are willing to live with that situation - especially since the trim call below should
// be able to trim back memory usage.
synchronized(this) {
Preconditions.checkState(mInUseValues.add(value));
Preconditions.checkArgument(mReservedBytes >= sizeInBytes);
mUsed.increment(sizeInBytes);
mReservedBytes -= sizeInBytes;
if (bucket != null) {
bucket.incrementInUseCount();
}
// If we're over the pool's max size, try to trim the pool appropriately
trimToSoftCap();
mPoolStatsTracker.onAlloc(sizeInBytes);
logStats();
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(
TAG,
"get (alloc) (object, size) = (%x, %s)",
System.identityHashCode(value),
bucketedSize);
}
}
return value;
}
/**
* Releases the given value to the pool.
* In a few cases, the value is 'freed' instead of being released to the pool. If
* - the pool currently exceeds its max size OR
* - if the value does not map to a bucket that's currently maintained by the pool, OR
* - if the bucket for the value exceeds its maxLength, OR
* - if the value is not recognized by the pool
* then, the value is 'freed'.
* @param value the value to release to the pool
*/
@Override
public void release(V value) {
Preconditions.checkNotNull(value);
final int bucketedSize = getBucketedSizeForValue(value);
final int sizeInBytes = getSizeInBytes(bucketedSize);
final Bucket<V> bucket = getBucket(bucketedSize);
synchronized (this) {
if (!mInUseValues.remove(value)) {
// This value was not 'known' to the pool (i.e.) allocated via the pool.
// Something is going wrong, so let's free the value and report soft error.
FLog.e(
TAG,
"release (free, value unrecognized) (object, size) = (%x, %s)",
System.identityHashCode(value),
bucketedSize);
free(value);
mPoolStatsTracker.onFree(sizeInBytes);
} else {
// free the value, if
// - pool exceeds maxSize
// - there is no bucket for this value
// - there is a bucket for this value, but it has exceeded its maxLength
// - the value is not reusable
// If no bucket was found for the value, simply free it
// We should free the value if no bucket is found, or if the bucket length cap is exceeded.
// However, if the pool max size softcap is exceeded, it may not always be best to free
// *this* value.
if (bucket == null ||
bucket.isMaxLengthExceeded() ||
isMaxSizeSoftCapExceeded() ||
!isReusable(value)) {
if (bucket != null) {
bucket.decrementInUseCount();
}
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(
TAG,
"release (free) (object, size) = (%x, %s)",
System.identityHashCode(value),
bucketedSize);
}
free(value);
mUsed.decrement(sizeInBytes);
mPoolStatsTracker.onFree(sizeInBytes);
} else {
bucket.release(value);
mFree.increment(sizeInBytes);
mUsed.decrement(sizeInBytes);
mPoolStatsTracker.onValueRelease(sizeInBytes);
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(
TAG,
"release (reuse) (object, size) = (%x, %s)",
System.identityHashCode(value),
bucketedSize);
}
}
}
logStats();
}
}
/**
* 'Take over' the specified value, and keep track of it in the in-use-values.
* Callers can use the {@link #takeOver(Object)} method to transfer ownership of a value to
* the pool - once the value has been taken over, it is now known to the pool, it is reflected
* in the pool's usage stats, and {@link #release(Object)} can then reuse/free it.
* {@link #takeOver(Object)} is intended to be called by the producer of the value, and gives us
* explicit signal about the ownership - while the {@link #release(Object)} may be called by
* any consumer of the value.
* If the value cannot be successfully taken over by the pool, this function returns false.
* Currently the only case is when taking over the value will cause the pool to exceed its
* max cap.
* @param value the value to take over
* @return true, if the value was successfully taken over
*/
public boolean takeOver(V value) {
Preconditions.checkNotNull(value);
ensurePoolSizeInvariant();
final int bucketedSize = getBucketedSizeForValue(value);
final int sizeInBytes = getSizeInBytes(bucketedSize);
final Bucket<V> bucket = getBucket(bucketedSize);
synchronized (this) {
// if adding this value to the pool would cause the hard cap to be exceeded, then
// return false right away
if (!canAllocate(sizeInBytes)) {
return false;
}
if (mInUseValues.add(value)) {
mUsed.increment(sizeInBytes);
if (bucket != null) {
bucket.incrementInUseCount();
}
trimToSoftCap();
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(
TAG,
"takeover (object, size) = (%x, %s)",
System.identityHashCode(value),
bucketedSize);
}
} else {
FLog.w(
TAG,
"takeover (ignore) (object, size) = (%x, %s)",
System.identityHashCode(value),
bucketedSize);
}
logStats();
}
return true;
}
/**
* Trims the pool in response to low-memory states (invoked from MemoryManager)
* For now, we'll do the simplest thing, and simply clear out the entire pool. We may consider
* more sophisticated approaches later.
* In other words, we ignore the memoryTrimType parameter
* @param memoryTrimType the kind of trimming we want to perform
*/
public void trim(MemoryTrimType memoryTrimType) {
trimToNothing();
}
/**
* Allocates a new 'value' with the given size
* @param bucketedSize the logical size to allocate
* @return a new value
*/
protected abstract V alloc(int bucketedSize);
/**
* Frees the 'value'
* @param value the value to free
*/
@VisibleForTesting
protected abstract void free(V value);
/**
* Gets the bucketed size (typically something the same or larger than the requested size)
* @param requestSize the logical request size
* @return the 'bucketed' size
* @throws InvalidSizeException, if the size of the value doesn't match the pool's constraints
*/
protected abstract int getBucketedSize(int requestSize);
/**
* Gets the bucketed size of the value
* @param value the value
* @return bucketed size of the value
* @throws InvalidSizeException, if the size of the value doesn't match the pool's constraints
* @throws InvalidValueException, if the value is invalid
*/
protected abstract int getBucketedSizeForValue(V value);
/**
* Gets the size in bytes for the given bucketed size
* @param bucketedSize the bucketed size
* @return size in bytes
*/
protected abstract int getSizeInBytes(int bucketedSize);
/**
* The pool parameters may have changed. Subclasses can override this to update any state they
* were maintaining
*/
protected void onParamsChanged() {
}
/**
* Determines if the supplied value is 'reusable'.
* This is called during {@link #release(Object)}, and determines if the value can be added
* to the freelists of the pool (for future reuse), or must be released right away.
* Subclasses can override this to provide custom implementations
* @param value the value to test for reusability
* @return true if the value is reusable
*/
protected boolean isReusable(V value) {
Preconditions.checkNotNull(value);
return true;
}
/**
* Ensure pool size invariants.
* The pool must either be below the soft-cap OR it must have no free values left
*/
private synchronized void ensurePoolSizeInvariant() {
Preconditions.checkState(!isMaxSizeSoftCapExceeded() || mFree.mNumBytes == 0);
}
/**
* Initialize the list of buckets. Get the bucket sizes (and bucket lengths) from the bucket
* sizes provider
* @param inUseCounts map of current buckets and their in use counts
*/
private synchronized void initBuckets(SparseIntArray inUseCounts) {
Preconditions.checkNotNull(inUseCounts);
// clear out all the buckets
mBuckets.clear();
// create the new buckets
final SparseIntArray bucketSizes = mPoolParams.bucketSizes;
if (bucketSizes != null) {
for (int i = 0; i < bucketSizes.size(); ++i) {
final int bucketSize = bucketSizes.keyAt(i);
final int maxLength = bucketSizes.valueAt(i);
int bucketInUseCount = inUseCounts.get(bucketSize, 0);
mBuckets.put(
bucketSize,
new Bucket<V>(
getSizeInBytes(bucketSize),
maxLength,
bucketInUseCount));
}
mAllowNewBuckets = false;
} else {
mAllowNewBuckets = true;
}
}
/**
* Gets rid of all free values in the pool
* At the end of this method, mFreeSpace will be zero (reflecting that there are no more free
* values in the pool). mUsedSpace will however not be reset, since that's a reflection of the
* values that were allocated via the pool, but are in use elsewhere
*/
@VisibleForTesting
void trimToNothing() {
final List<Queue<V>> freeListList = new ArrayList<Queue<V>>(mBuckets.size());
final SparseIntArray inUseCounts = new SparseIntArray();
synchronized (this) {
for (int i = 0; i < mBuckets.size(); ++i) {
final Bucket<V> bucket = mBuckets.valueAt(i);
if (!bucket.mFreeList.isEmpty()) {
freeListList.add(bucket.mFreeList);
}
inUseCounts.put(mBuckets.keyAt(i), bucket.mInUseLength);
}
// reinitialize the buckets
initBuckets(inUseCounts);
// free up the stats
mFree.reset();
logStats();
}
// the pool parameters 'may' have changed.
onParamsChanged();
// Explicitly free all the values.
// All the core data structures have now been reset. We no longer need to block other calls.
// This is true even for a concurrent trim() call
for (int i = 0; i < freeListList.size(); ++i) {
final Queue<V> freeList = freeListList.get(i);
while (!freeList.isEmpty()) {
// what happens if we run into an exception during the recycle. I'm going to ignore
// these exceptions for now, and let the GC handle the rest of the to-be-recycled-bitmaps
// in its usual fashion
free(freeList.poll());
}
}
}
/**
* Trim the (free portion of the) pool so that the pool size is at or below the soft cap.
* This will try to free up values in the free portion of the pool, until
* (a) the pool size is now below the soft cap configured OR
* (b) the free portion of the pool is empty
*/
@VisibleForTesting
synchronized void trimToSoftCap() {
if (isMaxSizeSoftCapExceeded()) {
trimToSize(mPoolParams.maxSizeSoftCap);
}
}
/**
* (Try to) trim the pool until its total space falls below the max size (soft cap). This will
* get rid of values on the free list, until the free lists are empty, or we fall below the
* max size; whichever comes first.
* NOTE: It is NOT an error if we have eliminated all the free values, but the pool is still
* above its max size (soft cap)
* <p>
* The approach we take is to go from the smallest sized bucket down to the largest sized
* bucket. This may seem a bit counter-intuitive, but the rationale is that allocating
* larger-sized values is more expensive than the smaller-sized ones, so we want to keep them
* around for a while.
* @param targetSize target size to trim to
*/
@VisibleForTesting
synchronized void trimToSize(int targetSize) {
// find how much we need to free
int bytesToFree = Math.min(mUsed.mNumBytes + mFree.mNumBytes - targetSize, mFree.mNumBytes);
if (bytesToFree <= 0) {
return;
}
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(
TAG,
"trimToSize: TargetSize = %d; Initial Size = %d; Bytes to free = %d",
targetSize,
mUsed.mNumBytes + mFree.mNumBytes,
bytesToFree);
}
logStats();
// now walk through the buckets from the smallest to the largest. Keep freeing things
// until we've gotten to what we want
for (int i = 0; i < mBuckets.size(); ++i) {
if (bytesToFree <= 0) {
break;
}
Bucket<V> bucket = mBuckets.valueAt(i);
while (bytesToFree > 0) {
V value = bucket.pop();
if (value == null) {
break;
}
free(value);
bytesToFree -= bucket.mItemSize;
mFree.decrement(bucket.mItemSize);
}
}
// dump stats at the end
logStats();
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(
TAG,
"trimToSize: TargetSize = %d; Final Size = %d",
targetSize,
mUsed.mNumBytes + mFree.mNumBytes);
}
}
/**
* Gets the freelist for the specified bucket. Create the freelist if there isn't one
* @param bucketedSize the bucket size
* @return the freelist for the bucket
*/
@VisibleForTesting
synchronized Bucket<V> getBucket(int bucketedSize) {
// get an existing bucket
Bucket<V> bucket = mBuckets.get(bucketedSize);
if (bucket != null || !mAllowNewBuckets) {
return bucket;
}
// create a new bucket
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(TAG, "creating new bucket %s", bucketedSize);
}
Bucket<V> newBucket = new Bucket<V>(
/*itemSize*/getSizeInBytes(bucketedSize),
/*maxLength*/Integer.MAX_VALUE,
/*inUseLength*/0);
mBuckets.put(bucketedSize, newBucket);
return newBucket;
}
/**
* Returns true if the pool size (sum of the used and the free portions) exceeds its 'max size'
* soft cap as specified by the pool parameters.
*/
@VisibleForTesting
synchronized boolean isMaxSizeSoftCapExceeded() {
final boolean isMaxSizeSoftCapExceeded =
(mUsed.mNumBytes + mFree.mNumBytes) > mPoolParams.maxSizeSoftCap;
if (isMaxSizeSoftCapExceeded) {
mPoolStatsTracker.onSoftCapReached();
}
return isMaxSizeSoftCapExceeded;
}
/**
* Can we allocate a value of size 'sizeInBytes' without exceeding the hard cap on the pool size?
* If allocating this value will take the pool over the hard cap, we will first trim the pool down
* to its soft cap, and then check again.
* If the current used bytes + this new value will take us above the hard cap, then we return
* false immediately - there is no point freeing up anything.
* This will also take into account mReservedBytes
* @param sizeInBytes the size (in bytes) of the value to allocate
* @return true, if we can allocate this; false otherwise
*/
@VisibleForTesting
synchronized boolean canAllocate(int sizeInBytes) {
int hardCap = mPoolParams.maxSizeHardCap;
// even with our best effort we cannot ensure hard cap limit.
// Return immediately - no point in trimming any space
if ((mUsed.mNumBytes + mReservedBytes + sizeInBytes) > hardCap) {
mPoolStatsTracker.onHardCapReached();
return false;
}
// trim if we need to
int softCap = mPoolParams.maxSizeSoftCap;
if ((mUsed.mNumBytes + mFree.mNumBytes + mReservedBytes + sizeInBytes) > softCap) {
trimToSize(softCap - sizeInBytes);
}
// check again to see if we're below the hard cap
if (mUsed.mNumBytes + mFree.mNumBytes + mReservedBytes + sizeInBytes > hardCap) {
mPoolStatsTracker.onHardCapReached();
return false;
}
return true;
}
/**
* Simple 'debug' logging of stats.
* WARNING: The caller is responsible for synchronization
*/
@SuppressLint("InvalidAccessToGuardedField")
private void logStats() {
if (FLog.isLoggable(FLog.VERBOSE)) {
FLog.v(
TAG,
"Used = (%d, %d); Free = (%d, %d)",
mUsed.mCount,
mUsed.mNumBytes,
mFree.mCount,
mFree.mNumBytes);
}
}
/**
* Export memory stats regarding buckets used, memory caps, reused values.
*/
public synchronized Map<String, Integer> getStats() {
Map<String, Integer> stats = new HashMap<String, Integer>();
for (int i = 0; i < mBuckets.size(); ++i) {
final int bucketedSize = mBuckets.keyAt(i);
final Bucket<V> bucket = mBuckets.valueAt(i);
final String BUCKET_USED_KEY =
PoolStatsTracker.BUCKETS_USED_PREFIX + getSizeInBytes(bucketedSize);
stats.put(BUCKET_USED_KEY, bucket.mInUseLength);
}
stats.put(PoolStatsTracker.SOFT_CAP, mPoolParams.maxSizeSoftCap);
stats.put(PoolStatsTracker.HARD_CAP, mPoolParams.maxSizeHardCap);
stats.put(PoolStatsTracker.USED_COUNT, mUsed.mCount);
stats.put(PoolStatsTracker.USED_BYTES, mUsed.mNumBytes);
stats.put(PoolStatsTracker.FREE_COUNT, mFree.mCount);
stats.put(PoolStatsTracker.FREE_BYTES, mFree.mNumBytes);
return stats;
}
/**
* A simple 'counter' that keeps track of the number of items (mCount) as well as the byte
* mCount for the number of items
* WARNING: this class is not synchronized - the caller must ensure the appropriate
* synchronization
*/
@NotThreadSafe
@VisibleForTesting
static class Counter {
private static final String TAG = "com.facebook.imagepipeline.common.BasePool.Counter";
int mCount;
int mNumBytes;
/**
* Add a new item to the counter
* @param numBytes size of the item in bytes
*/
public void increment(int numBytes) {
this.mCount++;
this.mNumBytes += numBytes;
}
/**
* 'Decrement' an item from the counter
* @param numBytes size of the item in bytes
*/
public void decrement(int numBytes) {
if (this.mNumBytes >= numBytes && this.mCount > 0) {
this.mCount--;
this.mNumBytes -= numBytes;
} else {
FLog.wtf(
TAG,
"Unexpected decrement of %d. Current numBytes = %d, count = %d",
numBytes,
this.mNumBytes,
this.mCount);
}
}
/**
* Reset the counter
*/
public void reset() {
this.mCount = 0;
this.mNumBytes = 0;
}
}
/**
* An exception to indicate if the 'value' is invalid.
*/
public static class InvalidValueException extends RuntimeException {
public InvalidValueException(Object value) {
super("Invalid value: " + value.toString());
}
}
/**
* An exception to indicate that the requested size was invalid
*/
public static class InvalidSizeException extends RuntimeException {
public InvalidSizeException(Object size) {
super("Invalid size: " + size.toString());
}
}
/**
* A specific case of InvalidSizeException used to indicate that the requested size was too large
*/
public static class SizeTooLargeException extends InvalidSizeException {
public SizeTooLargeException(Object size) {
super(size);
}
}
/**
* Indicates that the pool size will exceed the hard cap if we allocated a value
* of size 'allocSize'
*/
public static class PoolSizeViolationException extends RuntimeException {
public PoolSizeViolationException(int hardCap, int usedBytes, int freeBytes, int allocSize) {
super(
"Pool hard cap violation? " +
"Hard cap = " + hardCap +
"Used size = " + usedBytes +
"Free size = " + freeBytes +
"Request size = " + allocSize);
}
}
}