/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* muCommander 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.commons.io;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.Vector;
/**
* This class allows to share and reuse byte buffers to avoid excessive memory allocation and garbage collection.
* Methods that use byte buffers and that are called repeatedly will benefit from using this class.
*
* <p>This class works with two types of byte buffers indifferently:
* <ul>
* <li>Byte array buffers (<code>byte[]</code>)</li>
* <li><code>java.nio.ByteBuffer</code></li>
* </ul>
* </p>
* <p>
* Usage of this class is similar to malloc/free:
* <ul>
* <li>Call <code>#get*Buffer(int)</code> to retrieve a buffer instance of a specified size</li>
* <li>Use the buffer</li>
* <li>When finished using the buffer, call <code>#release*Buffer(byte[])</code> to make this buffer available for
* subsequent calls to <code>#get*Buffer(int)</code>. Failing to call this method will prevent the buffer from being
* used again and from being garbage-collected.</li>
* </ul>
* </p>
*
* <p>Note: this class is thread safe and thus can safely be used by concurrent threads.</p>
*
* @author Maxence Bernard, Nicolas Rinaudo
* @see com.mucommander.commons.io.StreamUtils
*/
public class BufferPool {
/** Logger used by this class. */
private static final Logger LOGGER = LoggerFactory.getLogger(BufferPool.class);
/** List of BufferContainer instances that wraps available buffers */
private static Vector<BufferContainer> bufferContainers = new Vector<BufferContainer>();
/** The initial default buffer size */
public final static int INITIAL_DEFAULT_BUFFER_SIZE = 65536;
/** Size of buffers returned by get*Buffer methods without a size argument */
public static int defaultBufferSize = INITIAL_DEFAULT_BUFFER_SIZE;
/** The initial max pool size */
public final static long INITIAL_POOL_LIMIT = 10485760;
/** Maximum combined size of all pooled buffers, in bytes */
public static long maxPoolSize = INITIAL_POOL_LIMIT;
/** Current combined size of all pooled buffers, in bytes */
public static long poolSize;
/**
* Convenience method that has the same effect as calling {@link #getByteArray(int)} with
* a length equal to {@link #getDefaultBufferSize()}.
*
* @return a byte array with a length of {@link #getDefaultBufferSize()}
*/
public static synchronized byte[] getByteArray() {
return getByteArray(getDefaultBufferSize());
}
/**
* Returns a byte array of the specified length. This method first checks if a byte array of the specified length
* exists in the pool. If one is found, it is removed from the pool and returned. If not, a new instance is created
* and returned.
*
* <p>This method won't return the same buffer instance until it has been released with
* {@link #releaseByteArray(byte[])}.</p>
*
* <p>This method is a shorthand for {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} called
* with a {@link com.mucommander.commons.io.BufferPool.ByteArrayFactory} instance.</p>.
*
* @param length length of the byte array
* @return a byte array of the specified size
*/
public static synchronized byte[] getByteArray(int length) {
return (byte[])getBuffer(new ByteArrayFactory(), length);
}
/**
* Convenience method that has the same effect as calling {@link #getCharArray(int)} with
* a length equal to {@link #getDefaultBufferSize()}.
*
* @return a char array with a length of {@link #getDefaultBufferSize()}
*/
public static synchronized char[] getCharArray() {
return getCharArray(getDefaultBufferSize());
}
/**
* Returns a char array of the specified length. This method first checks if a char array of the specified length
* exists in the pool. If one is found, it is removed from the pool and returned. If not, a new instance is created
* and returned.
*
* <p>This method won't return the same buffer instance until it has been released with
* {@link #releaseCharArray(char[])}.</p>
*
* <p>This method is a shorthand for {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} called
* with a {@link com.mucommander.commons.io.BufferPool.CharArrayFactory} instance.</p>.
*
* @param length length of the char array
* @return a char array of the specified length
*/
public static synchronized char[] getCharArray(int length) {
return (char[])getBuffer(new CharArrayFactory(), length);
}
/**
* Convenience method that has the same effect as calling {@link #getByteBuffer(int)} with
* a buffer capacity of {@link #getDefaultBufferSize()}.
*
* @return a ByteBuffer with a capacity equal to {@link #getDefaultBufferSize()}
*/
public static synchronized ByteBuffer getByteBuffer() {
return getByteBuffer(getDefaultBufferSize());
}
/**
* Returns a ByteBuffer of the specified capacity. This method first checks if a ByteBuffer instance of the
* specified capacity exists in the pool. If one is found, it is removed from the pool and returned. If not,
* a new instance is created and returned.
*
* <p>This method won't return the same buffer instance until it has been released with
* {@link #releaseByteBuffer(ByteBuffer)}.</p>
*
* <p>This method is a shorthand for {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} called
* with a {@link com.mucommander.commons.io.BufferPool.ByteBufferFactory} instance.</p>.
* @param capacity capacity of the ByteBuffer
* @return a ByteBuffer with the specified capacity
*/
public static synchronized ByteBuffer getByteBuffer(int capacity) {
return (ByteBuffer)getBuffer(new ByteBufferFactory(), capacity);
}
/**
* Convenience method that has the same effect as calling {@link #getCharBuffer(int)} with
* a buffer capacity of {@link #getDefaultBufferSize()}.
*
* @return a CharBuffer with a capacity equal to {@link #getDefaultBufferSize()}
*/
public static synchronized CharBuffer getCharBuffer() {
return getCharBuffer(getDefaultBufferSize());
}
/**
* Returns a CharBuffer of the specified capacity. This method first checks if a CharBuffer instance of the
* specified capacity exists in the pool. If one is found, it is removed from the pool and returned. If not,
* a new instance is created and returned.
*
* <p>This method won't return the same buffer instance until it has been released with
* {@link #releaseCharBuffer(CharBuffer)}.</p>
*
* <p>This method is a shorthand for {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} called
* with a {@link com.mucommander.commons.io.BufferPool.CharBufferFactory} instance.</p>.
* @param capacity capacity of the CharBuffer
* @return a CharBuffer with the specified capacity
*/
public static synchronized CharBuffer getCharBuffer(int capacity) {
return (CharBuffer)getBuffer(new CharBufferFactory(), capacity);
}
/**
* Convenience method that has the same effect as calling {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory, int)}
* with a size equal to {@link #getDefaultBufferSize()}.
*
* @param factory BufferFactory used to identify the target buffer class and create a new buffer (if necessary)
* @return a buffer with a size equal to {@link #getDefaultBufferSize()}
*/
public static synchronized Object getBuffer(BufferFactory factory) {
return getBuffer(factory, getDefaultBufferSize());
}
/**
* Returns a byte array of the specified size. This method first checks if a buffer the same size as the specified
* one and a class compatible with the specified factory exists in the pool. If one is found, it is removed from the
* pool and returned.
* If not, a new instance is created and returned using {@link BufferFactory#newBuffer(int)}.
*
* <p>This method won't return the same buffer instance until it has been released with
* {@link #releaseBuffer(Object, BufferFactory)}.</p>
*
* @param factory BufferFactory used to identify the target buffer class and create a new buffer (if necessary)
* @param size size of the buffer
* @return a buffer of the specified size
*/
public static synchronized Object getBuffer(BufferFactory factory, int size) {
int nbBuffers = bufferContainers.size();
BufferContainer bufferContainer;
Object buffer;
// Looks for a buffer container in the pool that matches the specified size and buffer class.
for(int i=0; i<nbBuffers; i++) {
bufferContainer = bufferContainers.elementAt(i);
buffer = bufferContainer.getBuffer();
// Caution: mind the difference between BufferContainer#getLength() and BufferContainer#getSize()
if(bufferContainer.getLength()==size && (factory.matchesBufferClass(buffer.getClass()))) {
bufferContainers.removeElementAt(i);
poolSize -= bufferContainer.getSize();
return buffer;
}
}
LOGGER.trace("Creating new buffer with {} size=", factory, size);
// No buffer with the same class and size found in the pool, create a new one and return it
return factory.newBuffer(size);
}
/**
* Makes the given buffer available for further calls to {@link #getByteArray(int)} with the same buffer length.
* Returns <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in
* the pool.
*
* <p>After calling this method, the given buffer instance <b>must not be used</b>, otherwise it could get
* corrupted if other threads were using it.</p>
*
* @param buffer the buffer instance to make available for further use
* @return <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in the pool
* @throws IllegalArgumentException if specified buffer is null
*/
public static synchronized boolean releaseByteArray(byte buffer[]) {
return releaseBuffer(buffer, new ByteArrayFactory());
}
/**
* Makes the given buffer available for further calls to {@link #getCharArray(int)} with the same buffer length.
* Returns <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in
* the pool.
*
* <p>After calling this method, the given buffer instance <b>must not be used</b>, otherwise it could get
* corrupted if other threads were using it.</p>
*
* @param buffer the buffer instance to make available for further use
* @return <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in the pool
* @throws IllegalArgumentException if specified buffer is null
*/
public static synchronized boolean releaseCharArray(char buffer[]) {
return releaseBuffer(buffer, new CharArrayFactory());
}
/**
* Makes the given buffer available for further calls to {@link #getByteBuffer(int)} with the same buffer capacity.
* Returns <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in
* the pool.
*
* <p>After calling this method, the given buffer instance <b>must not be used</b>, otherwise it could get
* corrupted if other threads were using it.</p>
*
* @param buffer the buffer instance to make available for further use
* @return <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in the pool
* @throws IllegalArgumentException if specified buffer is null
*/
public static synchronized boolean releaseByteBuffer(ByteBuffer buffer) {
return releaseBuffer(buffer, new ByteBufferFactory());
}
/**
* Makes the given buffer available for further calls to {@link #getCharBuffer(int)} with the same buffer capacity.
* Returns <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in
* the pool.
*
* <p>After calling this method, the given buffer instance <b>must not be used</b>, otherwise it could get
* corrupted if other threads were using it.</p>
*
* @param buffer the buffer instance to make available for further use
* @return <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in the pool
* @throws IllegalArgumentException if specified buffer is null
*/
public static synchronized boolean releaseCharBuffer(CharBuffer buffer) {
return releaseBuffer(buffer, new CharBufferFactory());
}
/**
* Makes the given buffer available for further calls to {@link #getBuffer(com.mucommander.commons.io.BufferPool.BufferFactory,int)} with the same buffer
* size and factory.
* Returns <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in
* the pool or the pool size limit has been reached.
*
* <p>After calling this method, the given buffer instance <b>must not be used</b>, otherwise it could get
* corrupted if other threads were using it.</p>
*
* @param buffer the buffer instance to make available for further use
* @param factory the BufferFactory that was used to create the buffer
* @return <code>true</code> if the buffer was added to the pool, <code>false</code> if the buffer was already in the pool or the pool size limit has been reached
* @throws IllegalArgumentException if specified buffer is null
*/
public static synchronized boolean releaseBuffer(Object buffer, BufferFactory factory) {
if(buffer==null)
throw new IllegalArgumentException("specified buffer is null");
BufferContainer bufferContainer = factory.newBufferContainer(buffer);
if(bufferContainers.contains(bufferContainer)) {
LOGGER.info("Warning: specified buffer is already in the pool: {}", buffer);
return false;
}
long bufferSize = bufferContainer.getSize(); // size in bytes (!= length)
if(maxPoolSize!=-1 && poolSize+bufferSize>maxPoolSize) {
LOGGER.info("Warning: maximum pool size reached, buffer not added to the pool: {}", buffer);
return false;
}
bufferContainers.add(bufferContainer);
poolSize += bufferSize;
return true;
}
/**
* Returns <code>true</code> if the specified buffer is currently in the pool.
*
* <p>Note that it is not necessary (and thus not recommended for performance reasons) to call this method before
* calling <code>release*Buffer</code> as it already performs this test before adding a buffer to the pool.</p>
*
* @param buffer the buffer to look for in the pool
* @param factory the BufferFactory that was used to create the buffer
* @return <code>true</code> if the specified buffer is already in the pool
*/
public static boolean containsBuffer(Object buffer, BufferFactory factory) {
return bufferContainers.contains(factory.newBufferContainer(buffer));
}
/**
* Returns the number of buffers that currently are in the pool. This method is provided for debugging
* purposes only.
*
* @return the number of buffers currently in the pool
*/
public static int getBufferCount() {
return bufferContainers.size();
}
/**
* Returns the number of buffers that currently are in the pool and whose Class are the same as the specified
* factory's. This method is provided for debugging purposes only.
*
* @param factory the BufferFactory
* @return the number of buffers currently in the pool
*/
public static int getBufferCount(BufferFactory factory) {
int count = 0;
int nbBuffers = bufferContainers.size();
for(int i=0; i<nbBuffers; i++) {
if(factory.matchesBufferClass(bufferContainers.elementAt(i).getBuffer().getClass())) {
count ++;
}
}
return count;
}
/**
* Returns the default size of buffers returned by <code>get*Buffer</code> methods without a <code>size</code>
* argument.
*
* <p>The default buffer size is initially set to {@link #INITIAL_DEFAULT_BUFFER_SIZE}.</p>
*
* @return the default size of buffers returned by <code>get*Buffer</code> methods without a <code>size</code> argument
*/
public static int getDefaultBufferSize() {
return defaultBufferSize;
}
/**
* Sets the default size of buffers returned by <code>get*Buffer</code> methods without a <code>size</code> argument.
*
* @param bufferSize the new buffer size
*/
public static synchronized void setDefaultBufferSize(int bufferSize) {
BufferPool.defaultBufferSize = bufferSize;
}
/**
* Returns the combined size in bytes of all buffers that are currently in the pool.
*
* @return the combined size in bytes of all buffers that are currenty in the pool
*/
public static long getPoolSize() {
return poolSize;
}
/**
* Returns the maximum combined size in bytes for all buffers in the pool, <code>-1</code> for no limit.
* Before adding a buffer to the pool, <code>release*Buffer</code> methods ensure that the pool size will
* not be exceeded. If and only if that is the case, the buffer is added to the pool.
*
* <p>The max pool size is initially set to {@link #INITIAL_POOL_LIMIT}.</p>
*
* @return the maximum combined size in bytes for all buffers in the pool
*/
public static long getMaxPoolSize() {
return maxPoolSize;
}
/**
* Sets the maximum combined size in bytes for all buffers in the pool, <code>-1</code> for no limit.
* Before adding a buffer to the pool, <code>release*Buffer</code> methods ensure that the pool size will
* not be exceeded. If and only if that is the case, the buffer is added to the pool.
*
* @param maxPoolSize the maximum combined size in bytes for all buffers in the pool
*/
public static synchronized void setMaxPoolSize(long maxPoolSize) {
BufferPool.maxPoolSize = maxPoolSize;
}
///////////////////
// Inner classes //
///////////////////
/**
* Wraps a buffer instance and provides information about the wrapped buffer.
*/
public static abstract class BufferContainer {
/** The wrapped buffer instance */
protected Object buffer;
/**
* Creates a new BufferContainer that wraps the given buffer.
*
* @param buffer the buffer instance to wrap
*/
protected BufferContainer(Object buffer) {
this.buffer = buffer;
}
/**
* Returns the wrapped buffer instance.
*
* @return the wrapped buffer instance
*/
protected Object getBuffer() {
return buffer;
}
/**
* Implements a shallow equal comparison.
*/
public boolean equals(Object o) {
// Note: this method is used by Vector.contains()
return (o instanceof BufferContainer) && buffer == ((BufferContainer)o).buffer;
}
/**
* Returns the length of the wrapped buffer instance.
*
* @return the length of the wrapped buffer instance
*/
protected abstract int getLength();
/**
* Returns the size of the wrapped buffer instance, expressed in bytes.
*
* @return the size of the wrapped buffer instance, expressed in bytes
*/
protected abstract int getSize();
}
/**
* A BufferFactory is responsible for creating buffer and {@link BufferContainer} instances, and for returning the buffer
* Class. The Class returned by {@link #getBufferClass()} may be a superclass or superinterface of the actual
* objects returned by {@link #newBuffer(int)}.
*/
public static abstract class BufferFactory {
/**
* Returns <code>true</code> if the class returned by {@link #getBufferClass()} is equal or a
* superclass/superinterface of the specified buffer class.
*
* @param bufferClass the buffer Class to test
* @return true if the class returned by <code>#getBufferClass()</code> is equal or a superclass/superinterface
* of the specified buffer class
*/
public boolean matchesBufferClass(Class<?> bufferClass) {
return getBufferClass().isAssignableFrom(bufferClass);
}
/**
* Creates and returns a buffer instance of the specified size.
*
* @param size size of the buffer to create
* @return a buffer instance of the specified size
*/
public abstract Object newBuffer(int size);
/**
* Creates and returns a {@link BufferContainer} for the specified buffer instance.
*
* @param buffer the buffer to wrap in a BufferContainer
* @return returns a BufferContainer for the specified buffer instance
*/
public abstract BufferContainer newBufferContainer(Object buffer);
/**
* Returns the Class of buffer instances this factory creates.
*
* @return the Class of buffer instances this factory creates
*/
public abstract Class<?> getBufferClass();
}
/**
* This class is a {@link BufferFactory} implementation for byte array (<code>byte[]</code>) buffers.
*/
public static class ByteArrayFactory extends BufferFactory {
@Override
public Object newBuffer(int size) {
return new byte[size];
}
@Override
public BufferContainer newBufferContainer(Object buffer) {
return new BufferContainer(buffer) {
@Override
protected int getLength() {
return ((byte[])buffer).length;
}
@Override
protected int getSize() {
return getLength();
}
};
}
@Override
public Class<?> getBufferClass() {
return byte[].class;
}
}
/**
* This class is a {@link BufferFactory} implementation for char array (<code>char[]</code>) buffers.
*/
public static class CharArrayFactory extends BufferFactory {
@Override
public Object newBuffer(int size) {
return new char[size];
}
@Override
public BufferContainer newBufferContainer(Object buffer) {
return new BufferContainer(buffer) {
@Override
protected int getLength() {
return ((char[])buffer).length;
}
@Override
protected int getSize() {
return 2*getLength();
}
};
}
@Override
public Class<?> getBufferClass() {
return char[].class;
}
}
/**
* This class is a {@link BufferFactory} implementation for <code>java.nio.ByteBuffer</code> buffers.
* ByteBuffer instances created by {@link #newBuffer(int)} are direct ; the actually Class of those instances may be actually
* be <code>java.nio.DirectByteBuffer</code> and not <code>java.nio.ByteBuffer</code> as returned by
* {@link #getBufferClass()}.
*/
public static class ByteBufferFactory extends BufferFactory {
@Override
public Object newBuffer(int size) {
// Note: the returned instance is actually a java.nio.DirectByteBuffer, this is why it's important to
// compare classes using Class#isAssignableFrom(Class)
return ByteBuffer.allocateDirect(size);
}
@Override
public BufferContainer newBufferContainer(Object buffer) {
return new BufferContainer(buffer) {
@Override
protected int getLength() {
return ((ByteBuffer)buffer).capacity();
}
@Override
protected int getSize() {
return getLength();
}
};
}
@Override
public Class<?> getBufferClass() {
return ByteBuffer.class;
}
}
/**
* This class is a {@link BufferFactory} implementation for <code>java.nio.CharBuffer</code> buffers.
*/
public static class CharBufferFactory extends BufferFactory {
@Override
public Object newBuffer(int size) {
return CharBuffer.allocate(size);
}
@Override
public BufferContainer newBufferContainer(Object buffer) {
return new BufferContainer(buffer) {
@Override
protected int getLength() {
return ((CharBuffer)buffer).capacity();
}
@Override
protected int getSize() {
return 2*getLength();
}
};
}
@Override
public Class<?> getBufferClass() {
return CharBuffer.class;
}
}
}