package edu.brown.net;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
public class ByteBufferFifo {
/** These buffers are available for reading (taking data). */
private final ArrayDeque<ByteBuffer> readBuffers = new ArrayDeque<ByteBuffer>();
/** This buffer is where data is written (putting data). */
private ByteBuffer currentWrite;
/** Cache of available ByteBuffers, since direct ByteBuffers are "expensive" to allocate. */
private final ArrayDeque<ByteBuffer> emptyBuffers = new ArrayDeque<ByteBuffer>();
/** If we alternate reads and writes, this is used to save the read position of the buffer. */
private int savedReadPosition = 0;
private boolean bigEndian = false;
private static final int BUFFER_SIZE = 4096;
public void clear() {
readBuffers.clear();
emptyBuffers.clear();
currentWrite = null;
}
public ByteBuffer getWriteBuffer() {
if (currentWrite == null) {
// If we have a single read buffer, there may be space left in it (combine writes!)
if (readBuffers.size() == 1) {
ByteBuffer lastRead = readBuffers.peekLast();
if (!lastRead.hasRemaining()) {
// The buffer is empty: remove it
removeEmptyReadBuffer();
} else if (lastRead.limit() < lastRead.capacity()) {
// There is space! Remember its read position and make it a write buffer
readBuffers.removeLast();
assert savedReadPosition == 0;
savedReadPosition = lastRead.position();
lastRead.position(lastRead.limit());
lastRead.limit(lastRead.capacity());
currentWrite = lastRead;
}
} else {
// if more than 1 read buffer, the buffer is full
assert readBuffers.isEmpty() ||
readBuffers.peekLast().limit() == readBuffers.peekLast().capacity();
}
if (currentWrite == null) {
// last resort: allocate a new buffer
currentWrite = allocateBuffer();
}
} else if (currentWrite.remaining() == 0) {
// buffer is full: get a new one
/* TODO: Support flushing buffers early? Does this help performance?
if (readBuffers.isEmpty()) {
// no queued buffers yet: attempt to write it?
flush();
}*/
queueWriteBuffer();
currentWrite = allocateBuffer();
assert currentWrite.remaining() == currentWrite.capacity();
}
assert currentWrite.remaining() > 0;
return currentWrite;
}
/** Returns the next ByteBuffer for reading or null if the FIFO is empty. */
public ByteBuffer getReadBuffer() {
ByteBuffer buffer = readBuffers.peekFirst();
if (buffer != null && buffer.remaining() == 0) {
// this buffer has been consumed: discard it and try the next
removeEmptyReadBuffer();
buffer = readBuffers.peekFirst();
assert buffer == null || buffer.remaining() > 0;
}
if (buffer == null && currentWrite != null && currentWrite.position() > 0) {
// Last resort: there is data in our current write buffer: return it
buffer = currentWrite;
queueWriteBuffer();
}
assert buffer == null || buffer.remaining() > 0;
return buffer;
}
private void removeEmptyReadBuffer() {
ByteBuffer buffer = readBuffers.removeFirst();
assert !buffer.hasRemaining();
buffer.clear();
emptyBuffers.add(buffer);
}
private void queueWriteBuffer() {
// Flip and queue the write buffer
currentWrite.flip();
currentWrite.position(savedReadPosition);
savedReadPosition = 0;
assert currentWrite.remaining() > 0;
readBuffers.add(currentWrite);
// Delay allocation of write buffers: this lets us typically reuse one buffer. We write to
// it, flip it and read from it, then recycle it via the emptyBuffers queue.
currentWrite = null;
}
private ByteBuffer allocateBuffer() {
ByteBuffer buffer = emptyBuffers.pollLast();
if (buffer == null) {
buffer = ByteBuffer.allocateDirect(BUFFER_SIZE);
if (!bigEndian) {
buffer.order(ByteOrder.nativeOrder());
}
}
return buffer;
}
// TODO: This is a hack. Make a proper setByteOrder method?
public void setBigEndian() {
assert !bigEndian;
bigEndian = true;
@SuppressWarnings("rawtypes")
ArrayDeque[] collections = {
emptyBuffers, readBuffers
};
for (ArrayDeque<ByteBuffer> collection : collections) {
for (ByteBuffer buffer : collection) {
buffer.order(ByteOrder.BIG_ENDIAN);
}
}
if (currentWrite != null) {
currentWrite.order(ByteOrder.BIG_ENDIAN);
}
}
}