/* This file is part of VoltDB.
* Copyright (C) 2008-2010 VoltDB L.L.C.
*
* VoltDB is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* VoltDB is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with VoltDB. If not, see <http://www.gnu.org/licenses/>.
*/
package org.voltdb.utils;
import java.nio.ByteBuffer;
import java.util.ArrayDeque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.concurrent.atomic.AtomicLong;
import org.apache.log4j.Logger;
import edu.brown.hstore.HStore;
/**
* A pool of {@link java.nio.ByteBuffer ByteBuffers} that are
* allocated with
* {@link java.nio.ByteBuffer#allocateDirect(int) * ByteBuffer.allocateDirect}.
* Buffers are stored in Arenas that are powers of 2. The smallest arena is 16 bytes.
* Arenas will shrink every 60 seconds if some of the memory isn't being used.
*/
public final class DBBPool {
/**
* An Arena that maintains allocated memory for a specific size of buffer.
*/
private static final class Arena {
/**
* A DicedBB wraps a larger byte buffer and divides the larger buffer into many smaller buffers
* based on the allocation size.
*
*/
private static final class DicedBB {
/**
* A container to hold the reference to the DicedBB/Arena/Pool
* that this buffer should be released to. The
* buffer is released into the pool when the discard method is called.
* A DicedBB will loan out consecutive slices until all slices are loaned out.
* Once all slices are loaned out the DicedBB is removed from the Arena's
* available list and is only added back once all slices have been returned.
*/
private final class DBBContainer extends BBContainer {
/*
* Potential storage for an exception with stack trace
* showing where this container was acquired from
*/
// public Throwable allocatedForException = null;
/**
* Construct the container with the buffer and address
* @param buffer
* @param address
*/
protected DBBContainer(final ByteBuffer buffer, final long address) {
super(buffer, address);
}
/**
* Return the buffer back to the DicedBB it was allocated from.
*/
@Override
public void discard() {
if (b != null) {
release(this);
}
}
// /**
// * It is possible to detect buffers not discarded by checking
// * to see of the buffer is not null as dicarded would have nulled
// * out the field.
// */
// @Override
// public void finalize() {
// if (traceAllocations && b != null) {
// System.err.println("DBBContainer was finalized without being released. Probable resource leak");
//// if (allocatedForException != null) {
//// System.err.println("Allocated at");
//// allocatedForException.printStackTrace();
//// }
// System.err.println("From pool " + m_arena.m_pool);
// m_arena.m_pool.poolLocation.printStackTrace();
// System.err.flush();
// VoltDB.crashVoltDB();
// }
// }
}
/**
* Indicates whether this DirectByteBuffer has been used recently
* where recently is since the last time the Shrinker has walked this pool.
*/
private boolean lastUsed = true;
/**
* Count and index of the next available slice.
*/
private int m_availableSlices = 0;
/**
* The larger ByteBuffer that is the source of the views of the smaller ByteBuffers
*/
private final BBContainer m_b;
/**
* The number of slices this DicedBB contains
*/
private final int m_numSlices;
/**
* The number of loaned out slices that have been returned
*/
private int m_returnedSlices = 0;
/**
* Storage for the ByteBuffers that are views into the larger diced up BB
*/
private final DBBContainer m_slices[];
/**
* Arena that created this DicedBB
*/
private final Arena m_arena;
/**
* Constructor that allocates a buffer of the specified size and then dices it up
* into allocationSize views.
* @param size Size of the larger buffer to allocate
* @param allocationSize Size the larger buffer should be diced into
*/
private DicedBB(int size, int allocationSize, Arena arena, boolean foundNativeSupport) {
if (size % allocationSize != 0) {
throw new RuntimeException("A ByteBuffer of size " + size +
" can't be evenly divided up into units of size " + allocationSize);
}
m_arena = arena;
m_b = arena.m_pool.allocateBuffer(size);
m_numSlices = size / allocationSize;
m_availableSlices = m_numSlices - 1;
m_slices = new DBBContainer[m_numSlices];
for (int ii = 0; ii < m_numSlices; ii++) {
m_b.b.limit(allocationSize * (ii + 1));
m_b.b.position(allocationSize * ii);
long address = 0;
if (foundNativeSupport) {
address = getBufferAddress(m_b.b);
}
m_slices[ii] = new DBBContainer( m_b.b.slice(), address );
}
}
/**
* Null out all the references to the views in m_slices.
* m_b is final so it can't be nulled out. It is an error
* to clear a DicedBB when there are still slices loaned out.
* The DicedBB should not be used again after it is cleared.
*/
private final void clear() {
if (m_availableSlices + m_returnedSlices != m_numSlices - 1) {
throw new RuntimeException("Attempted to clear A DicedByteBuffer " +
" while some portions were loaned out");
}
for (int ii = 0; ii < m_slices.length; ii++) {
m_slices[ii] = null;
}
m_b.discard();
}
/**
* Returns true if there are slices available to loan out.
* @return
*/
private final boolean hasRemaining() {
return m_availableSlices >= 0;
}
/**
* Get the next slice to loan out. It is an error to try and get a slice when none
* are available.
* @return A slice of the larger ByteBuffer
*/
private final DBBContainer nextSlice() {
if (m_availableSlices < 0) {
throw new RuntimeException("DicedBB has no more available slices");
}
lastUsed = true;
final DBBContainer slice = m_slices[m_availableSlices];
m_availableSlices--;
m_arena.m_pool.bytesLoanedLocally += slice.b.capacity();
return slice;
}
/**
* Release the view slice ByteBuffer in this container back into the
* DicedBB's list of slices.
* @param c
*/
private final void release( final DBBContainer c) {
synchronized (m_arena.m_pool) {
final int returnIndex = (m_numSlices - m_returnedSlices) - 1;
final int capacity = c.b.capacity();
m_arena.m_pool.bytesLoanedLocally -= capacity;
c.b.clear();
m_returnedSlices++;
// if (traceAllocations) {
// final DBBContainer newContainer = new DBBContainer( c.b, c.address);
// c.b = null;
// c.address = 0;
// m_slices[returnIndex] = newContainer;
// } else {
m_slices[returnIndex] = c;
// c.allocatedForException = null;
// }
/*
* When all the Buffers have been returned it is time
* to provide this DicedBB back to the arena
*/
if (returnIndex == 0) {
m_availableSlices = m_numSlices - 1;
m_returnedSlices = 0;
m_arena.m_availableDBBs.push(this);
}
}
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer(1024);
sb.append("DBB ").append(this.hashCode()).append(" Last used ").append(lastUsed);
sb.append(" Num slices ").append(m_numSlices);
sb.append(" Available slices ").append(m_availableSlices);
sb.append(" returned slices ").append(m_returnedSlices);
return sb.toString();
}
}
/**
* Set of all DicedBBs that this arena has created
*/
private final HashSet<DicedBB> m_allDBBs = new HashSet<DicedBB>();
/**
* Size of the allocations this arena provides
*/
private final int m_allocationSize;
/**
* Maximum size in bytes the arena is allowed to grow to.
*/
private final int m_maxArenaSize;
/**
* Total bytes allocated for this arena
*/
private int m_arenaSize = 0;
/**
* Queue of of available diced byte buffers that can provide slices to loan.
*
*/
private ArrayDeque<DicedBB> m_availableDBBs = new ArrayDeque<DicedBB>();
/**
* Pointer to the pool that this Arena belongs to. The Arena class
* is static so that instantiations of the nested
*/
private final DBBPool m_pool;
private final boolean m_foundNativeSupport;
/**
* Construct an Arena that allocates slices of the specified size
* @param allocationSize
* @param pool
*/
public Arena(int allocationSize, int maxArenaSize, DBBPool pool, boolean foundNativeSupport) {
m_allocationSize = allocationSize;
m_pool = pool;
m_maxArenaSize = maxArenaSize;
m_foundNativeSupport = foundNativeSupport;
}
/**
* Acquire a ByteBuffer of the size that this Arena allocates. Will allocate
* a new DicedBB if necessary
* @param minSize Minimum size of the buffer to be allocated. Use to size a heap byte buffer
* if the arena has nothing to loan out and is already too large.
* @return ByteBuffer of the size that this Arena allocates
*/
public BBContainer acquire(int minSize) {
// Throwable caughtException = null;
// if (traceAllocations) {
// caughtException = new Throwable();
// caughtException.fillInStackTrace();
// }
/*
* First attempt to supply the buffer without allocating a
* new DicedBB
*/
final DicedBB dbb = m_availableDBBs.peek();
if (dbb != null) {
final DicedBB.DBBContainer c = dbb.nextSlice();
if (!dbb.hasRemaining()) {
m_availableDBBs.poll();
}
// if (traceAllocations) {
// c.allocatedForException = caughtException;
// }
return c;
}
if (m_arenaSize > m_maxArenaSize) {
return DBBPool.wrapBB(ByteBuffer.allocate(minSize));
}
/*
* Create a new DicedBB to provide the slice. The Diced up BB will be MAX_ALLOCATION_SIZE
* or whatever size is necessary to fit at least 16 slices. If this allocation
* grows the arena beyond the max size log an error
*/
int allocationSize = MAX_ALLOCATION_SIZE;
if ((MAX_ALLOCATION_SIZE / m_allocationSize) < 16) {
allocationSize = m_allocationSize * 16;
}
m_arenaSize += allocationSize;
if (m_arenaSize > m_maxArenaSize) {
m_logger.error("Arena " + m_allocationSize + " grew to " + m_arenaSize +
" which is greater then the max of " + m_maxArenaSize +
". This could signal a potential leak of ByteBuffers, an inadequately sized arena, or" +
" some other shortcoming in the network subsystem");
System.err.println("Arena " + m_allocationSize + " grew to " + m_arenaSize +
" which is greater then the max of " + m_maxArenaSize +
". This could signal a potential leak of ByteBuffers, an inadequately sized arena, or" +
" some other shortcoming in the network subsystem");
}
final DicedBB newDBB =
new DicedBB(
allocationSize,
m_allocationSize,
this,
m_foundNativeSupport);
m_allDBBs.add(newDBB);
final DicedBB.DBBContainer c = newDBB.nextSlice();
assert(c != null);
if (newDBB.hasRemaining()) {
m_availableDBBs.push(newDBB);
}
// if (traceAllocations) {
// c.allocatedForException = caughtException;
// }
return c;
}
/**
* Clear all the DicedBBs out of this arena. It is an error
* to call clear while the Arena has slices loaned out.
*/
private void clear() {
//System.err.println("Clearing pool " + this);
for (DicedBB dbb : m_availableDBBs) {
dbb.clear();
}
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer(2048);
sb.append("\tArena ").append(m_allocationSize).append(" has ").append(m_allDBBs.size());
sb.append(" DBBs total ").append(" with ");
sb.append(m_availableDBBs.size()).append(" available\n");
for (DicedBB dbb : m_allDBBs) {
sb.append("\t\t").append(dbb.toString()).append('\n');
}
return sb.toString();
}
}
/**
* Abstract base class for a ByteBuffer container. A container serves to hold a reference
* to the pool/arena/whatever the ByteBuffer was allocated from and possibly the address
* of the ByteBuffer if it is a DirectByteBuffer. The container also provides the interface
* for discarding the ByteBuffer and returning it back to the pool. It is a good practice
* to discard a container even if it is wrapper for HeapByteBuffer that isn't pooled.
*
*/
public static abstract class BBContainer {
/**
* Pointer to the location in memory where this buffer is located. Useful if you
* want to pass it to the native side so it doesn't have to call GetDirectBufferAddress.
*/
final public long address;
/**
* The buffer
*/
final public ByteBuffer b;
public BBContainer(ByteBuffer b, long address) {
this.b = b;
this.address = address;
}
abstract public void discard();
}
/**
* Wrapper for HeapByteBuffers that allows them to pose as ByteBuffers from a pool.
* @author aweisberg
*
*/
private static final class BBWrapperContainer extends BBContainer {
protected BBWrapperContainer(ByteBuffer b) {
super( b, 0);
}
@Override
public final void discard() {
}
}
/**
* Number of bytes allocated globally by DBBPools
*/
private static AtomicLong bytesAllocatedGlobally = new AtomicLong(0);
/**
* True if the native library with the functionality necessary to retrieve buffer addresses
* was found.
*/
private final boolean foundNativeSupport;
private static final Logger m_logger = Logger.getLogger(DBBPool.class.getName(), VoltLoggerFactory.instance());
/**
* Boolean that determines whether code that traces and tracks allocations will be run
* A lot of this code is commented out anyways because of the extra storage required
* even if the code is compiled out.
*/
private static final boolean traceAllocations = true;
/**
* The maximum Arena size. Must be a power of 2.
*/
public static final int MAX_ALLOCATION_SIZE = 262144;
public static final void doShrink() {
}
/**
* Retrieve the native address of a DirectByteBuffer as a long
* @param b Buffer you want to retrieve the address of
* @return Native address of the buffer as a long.
*/
public static native long getBufferAddress( ByteBuffer b );
/**
* Retrieve the CRC32 value of a DirectByteBuffer as a long
* @param b Buffer you want to retrieve the CRC32 of
* @param offset Offset into buffer to start calculations
* @param length Length of the buffer to calculate
* @return CRC32 of the buffer as an int.
*/
public static native int getBufferCRC32( ByteBuffer b, int offset, int length);
/**
* Static factory method to wrap a ByteBuffer in a BBContainer that is not
* associated with any pool
* @param b
*/
public static final BBWrapperContainer wrapBB(ByteBuffer b) {
return new BBWrapperContainer(b);
}
private long bytesAllocatedLocally = 0;
private long bytesLoanedLocally = 0;
/**
* If set to true then this pool will allocate all buffers on the heap and not
* direct. Useful if a class is expecting to be passed a Pool as an allocator
* and it would be better to use heap ByteBuffers
*/
private final boolean m_allocateOnHeap;
/**
* Array containing references to the Arenas for each power of 2 allocation size
* greater then 16
*/
private final Arena m_arenas[];
/**
* Exception containing the stack trace that describes where this pool
* was instantiated. Used to idenify pools in log messages
*/
private final Throwable poolLocation = new Throwable();
/**
* No arg constructor that initializes a pool with the default number of buffers
* and buffer size.
*/
public DBBPool() {
this(false, false);
}
/**
* Constructor that initializes the pool with the default {@link Arena} sizes.
* If <code>allocateOnHeap</code> is <code>true</code> the <code>DBBPool</code> will allocate
* all {@link java.nio.ByteBuffer ByteBuffer}s as {@link java.nio.HeapByteBuffer HeapByteBuffer}s that are not pooled.
*/
public DBBPool(boolean allocateOnHeap, boolean loadNativeLib) {
this(allocateOnHeap, new int[] {
m_defaultMaxArenaSize,//16
m_defaultMaxArenaSize,//32
m_defaultMaxArenaSize,//64
m_defaultMaxArenaSize,//128
m_defaultMaxArenaSize,//256
m_defaultMaxArenaSize,//512
m_defaultMaxArenaSize,//1024
m_defaultMaxArenaSize,//2048
m_defaultMaxArenaSize,//4096
m_defaultMaxArenaSize,//8192
m_defaultMaxArenaSize,//16384
m_defaultMaxArenaSize,//32768
m_defaultMaxArenaSize,//65536
m_defaultMaxArenaSize,//131072
m_defaultMaxArenaSize//262144
},
loadNativeLib);
}
/**
* Constructor that allows the pool to be configured to perform all allocations on the heap as well
* as allowing the maximum size of each {@link Arena} to be configured.
* @param allocateOnHeap Boolean indicating whether the pool should act as a dummy pool that allocates
* all buffers as non-pooled heap {@link java.nio.ByteBuffer ByteBuffer}s
* @param maxArenaSizes Array of integers indicating the maximum size each arena can grow to. Must contain
* values for arenas from powers of 2 from 16 - 262144 e.g. have 15 positive values.
* May be <code>null</code> but not length zero or an incorrect length.
*/
public DBBPool(boolean allocateOnHeap, int maxArenaSizes[], boolean loadNativeLib) {
if (loadNativeLib) {
foundNativeSupport = org.voltdb.EELibraryLoader.loadExecutionEngineLibrary(false);
} else {
foundNativeSupport = false;
}
m_allocateOnHeap = allocateOnHeap;
if (maxArenaSizes == null) {
m_maxArenaSizes = new int[] {
m_defaultMaxArenaSize,//16
m_defaultMaxArenaSize,//32
m_defaultMaxArenaSize,//64
m_defaultMaxArenaSize,//128
m_defaultMaxArenaSize,//256
m_defaultMaxArenaSize,//512
m_defaultMaxArenaSize,//1024
m_defaultMaxArenaSize,//2048
m_defaultMaxArenaSize,//4096
m_defaultMaxArenaSize,//8192
m_defaultMaxArenaSize,//16384
m_defaultMaxArenaSize,//32768
m_defaultMaxArenaSize,//65536
m_defaultMaxArenaSize,//131072
m_defaultMaxArenaSize//262144
};
} else {
m_maxArenaSizes = maxArenaSizes;
}
m_arenas = initDBBPool();
}
/**
* Acquire a byte buffer from the pool that has at least <tt>minSize</tt> capacity.
* If the size is greater then the size of this pools allocation a dummy allocation
* will be done with a heap buffer
* @param minSize Minimum capacity in bytes that the <tt>ByteBuffer</tt> must have
* @return A <tt>DBBContainer</tt> with a <tt>ByteBuffer</tt> that is at least
* the minimum size requested.
*/
public synchronized BBContainer acquire(final int minSize) {
assert (minSize > 0);
if (m_allocateOnHeap) {
return DBBPool.wrapBB(ByteBuffer.allocate(minSize));
} else {
if (minSize > MAX_ALLOCATION_SIZE) {
return DBBPool.wrapBB(ByteBuffer.allocate(minSize));
}
return getArenaForAllocation(minSize).acquire(minSize);
}
}
/**
* Acquire an array of byte buffers from the pool that has at least <tt>minSize</tt> capacity.
* @param numBuffers Number of buffers.
* @param minSize Minimum capacity in bytes that the <tt>ByteBuffer</tt> must have
* @return An array of <tt>DBBContainer</tt> with <tt>ByteBuffers</tt> that are at least
* the minimum size requested.
*/
public synchronized final BBContainer[] acquire(final int numBuffers, final int minSize) {
BBContainer buffers[] = new BBContainer[numBuffers];
for (int ii = 0; ii < numBuffers; ii++) {
if (m_allocateOnHeap) {
buffers[ii] = DBBPool.wrapBB(ByteBuffer.allocate(minSize));
} else {
buffers[ii] = acquire(minSize);
}
}
return buffers;
}
/*
* Create a direct byte buffer of a specified size
* @param bufferSize Requested size of the buffer in bytes
* @return A <tt>ByteBuffer</tt> of the requested size.
*/
private final BBContainer allocateBuffer(final int bufferSize) {
bytesAllocatedGlobally.getAndAdd(bufferSize);
bytesAllocatedLocally += bufferSize;
try {
final BBContainer container = DBBPool.allocateDirect( bufferSize);
return container;
} catch (java.lang.OutOfMemoryError e) {
m_logger.fatal("Total bytes allocated globally before OOM is " + bytesAllocatedGlobally.get(), e);
HStore.crashDB();
}
return null;
}
public long bytesAllocatedGlobally() {
return bytesAllocatedGlobally.longValue();
}
public long bytesAllocatedLocally() {
return bytesAllocatedLocally;
}
public long bytesLoanedLocally() {
return bytesLoanedLocally;
}
/**
* Remove all references to DirectByteBuffers allocated by this pool allowing
* them to be garbage collected. A pool must be cleared before it is garbage collected
* to prevent false leak detection. All allocations must be returned to the pool
* before clearing. This strict policy is to ensure that leaks can be detected.
*/
public synchronized void clear() {
//System.err.println("Clearing pool " + this);
for (Arena pa : m_arenas) {
pa.clear();
}
}
/**
* Get the Arena that allocates the next largest power of 2 size
* @param minSize Size of the requested allocation
* @return Arena that will allocate a Buffer great then or equal to the requested size
*/
private final Arena getArenaForAllocation(int minSize) {
int arenaIndex = 28 - Integer.numberOfLeadingZeros(minSize -1);
return m_arenas[arenaIndex < 0 ? 0 : arenaIndex];
}
private static final int m_defaultMaxArenaSize = 67108864;
/**
* The maximum size each arena will be allowed to grow to before the arena
* starts substituting HeapByteBuffers. This will hurt performance but will ensure the server doesn't run
* out of memory.
*/
private final int m_maxArenaSizes[];
/**
* Init function shared by various constructors. Returns an Array of arenas
* to assign to m_arenas
* @return
*/
private final Arena[] initDBBPool() {
poolLocation.fillInStackTrace();
// assert(((MAX_ALLOCATION_SIZE & (MAX_ALLOCATION_SIZE -1)) == 0));
int arenaCount = 0;
for (int ii = 16; ii <= MAX_ALLOCATION_SIZE; ii *= 2) {
arenaCount++;
}
final Arena arenas[] = new Arena[arenaCount];
arenaCount = 0;
for (int ii = 16; ii <= MAX_ALLOCATION_SIZE; ii *= 2) {
arenas[arenaCount] =
new Arena(ii,
m_maxArenaSizes[arenaCount],
this,
foundNativeSupport);
arenaCount++;
}
return arenas;
}
@Override
public String toString() {
final StringBuffer sb = new StringBuffer(4096);
sb.append("\nDBBPool: ").append(this.hashCode()).append(" -- ");
sb.append(" bytes allocated locally ").append(bytesAllocatedLocally);
sb.append(" bytes allocated globally ").append(bytesAllocatedGlobally);
sb.append(" bytes loaned locally\n").append(bytesLoanedLocally);
for (Arena a : m_arenas) {
sb.append(a.toString()).append("\n");
}
return sb.toString();
}
private static final HashMap<Integer, ArrayDeque<ByteBuffer>> m_availableBufferStock =
new HashMap<Integer, ArrayDeque<ByteBuffer>>();
public static BBContainer allocateDirect(final int capacity) {
synchronized (m_availableBufferStock) {
ArrayDeque<ByteBuffer> buffers = m_availableBufferStock.get(capacity);
ByteBuffer retval = null;
if (buffers != null) {
retval = buffers.poll();
}
if (retval != null) {
retval.clear();
} else {
retval = ByteBuffer.allocateDirect(capacity);
}
return new BBContainer(retval, 0) {
@Override
public void discard() {
synchronized (m_availableBufferStock) {
ArrayDeque<ByteBuffer> buffers = m_availableBufferStock.get(b.capacity());
if (buffers == null) {
buffers = new ArrayDeque<ByteBuffer>();
m_availableBufferStock.put(b.capacity(), buffers);
}
buffers.offer(b);
}
}
};
}
}
}