Skip to content

Commit

Permalink
Add a BufferedOutputStream that works with ArrayPool to reuse byte[]s.
Browse files Browse the repository at this point in the history
  • Loading branch information
sjudd committed Jan 9, 2018
1 parent 416797e commit a0f3d30
Show file tree
Hide file tree
Showing 3 changed files with 1,214 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package com.bumptech.glide.load.data;

import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import java.io.IOException;
import java.io.OutputStream;

/**
* An {@link OutputStream} implementation that recycles and re-uses {@code byte[]}s using the
* provided {@link ArrayPool}.
*/
public final class BufferedOutputStream extends OutputStream {
@NonNull
private final OutputStream out;
private byte[] buffer;
private ArrayPool arrayPool;
private int index;

public BufferedOutputStream(@NonNull OutputStream out, @NonNull ArrayPool arrayPool) {
this(out, arrayPool, ArrayPool.STANDARD_BUFFER_SIZE_BYTES);
}

@VisibleForTesting
BufferedOutputStream(@NonNull OutputStream out, ArrayPool arrayPool, int bufferSize) {
this.out = out;
this.arrayPool = arrayPool;
buffer = arrayPool.get(bufferSize, byte[].class);
}

@Override
public void write(int b) throws IOException {
buffer[index++] = (byte) b;
maybeFlushBuffer();
}

@Override
public void write(@NonNull byte[] b) throws IOException {
write(b, 0, b.length);
}

@Override
public void write(@NonNull byte[] b, int initialOffset, int length) throws IOException {
int writtenSoFar = 0;
do {
int remainingToWrite = length - writtenSoFar;
int currentOffset = initialOffset + writtenSoFar;
// If we still need to write at least the buffer size worth of bytes, we might as well do so
// directly and avoid the overhead of copying to the buffer first.
if (index == 0 && remainingToWrite >= buffer.length) {
out.write(b, currentOffset, remainingToWrite);
return;
}

int remainingSpaceInBuffer = buffer.length - index;
int totalBytesToWriteToBuffer = Math.min(remainingToWrite, remainingSpaceInBuffer);

System.arraycopy(b, currentOffset, buffer, index, totalBytesToWriteToBuffer);

index += totalBytesToWriteToBuffer;
writtenSoFar += totalBytesToWriteToBuffer;

maybeFlushBuffer();
} while (writtenSoFar < length);
}

@Override
public void flush() throws IOException {
flushBuffer();
out.flush();
}

private void flushBuffer() throws IOException {
if (index > 0) {
out.write(buffer, 0, index);
index = 0;
}
}

private void maybeFlushBuffer() throws IOException {
if (index == buffer.length) {
flushBuffer();
}
}

@Override
public void close() throws IOException {
try {
flush();
} finally {
out.close();
}
release();
}

private void release() {
if (buffer != null) {
arrayPool.put(buffer);
buffer = null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package com.bumptech.glide.load.data;

import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.when;

import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

/**
* Runs some tests based on a random seed that asserts the output of writing to our buffered stream
* matches the output of writing to {@link java.io.ByteArrayOutputStream}.
*/
@RunWith(JUnit4.class)
public class BufferedOutputStreamFuzzTest {
private static final int TESTS = 500;
private static final int BUFFER_SIZE = 10;
private static final int WRITES_PER_TEST = 50;
private static final int MAX_BYTES_PER_WRITE = BUFFER_SIZE * 6;
private static final Random RANDOM = new Random(-3207167907493985134L);

@Mock private ArrayPool arrayPool;

@Before
public void setUp() {
MockitoAnnotations.initMocks(this);

when(arrayPool.get(anyInt(), eq(byte[].class)))
.thenAnswer(new Answer<byte[]>() {
@Override
public byte[] answer(InvocationOnMock invocation) throws Throwable {
int size = (Integer) invocation.getArguments()[0];
return new byte[size];
}
});
}

@Test
public void runFuzzTest() throws IOException {
for (int i = 0; i < TESTS; i++) {
runTest(RANDOM);
}
}

private void runTest(Random random) throws IOException {
List<Write> writes = new ArrayList<>(WRITES_PER_TEST);
for (int i = 0; i < WRITES_PER_TEST; i++) {
WriteType writeType = getType(random);
writes.add(getWrite(random, writeType));
}

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

ByteArrayOutputStream wrapped = new ByteArrayOutputStream();
BufferedOutputStream bufferedOutputStream =
new BufferedOutputStream(wrapped, arrayPool, BUFFER_SIZE);

for (Write write : writes) {
switch (write.writeType) {
case BYTE:
byteArrayOutputStream.write(write.data[0]);
bufferedOutputStream.write(write.data[0]);
break;
case BUFFER:
byteArrayOutputStream.write(write.data);
bufferedOutputStream.write(write.data);
break;
case OFFSET_BUFFER:
byteArrayOutputStream.write(write.data, write.offset, write.length);
bufferedOutputStream.write(write.data, write.offset, write.length);
break;
default:
throw new IllegalArgumentException();
}
}

byte[] fromByteArrayStream = byteArrayOutputStream.toByteArray();
bufferedOutputStream.close();
byte[] fromWrappedStream = wrapped.toByteArray();
if (!Arrays.equals(fromWrappedStream, fromByteArrayStream)) {
StringBuilder writesBuilder = new StringBuilder();
for (Write write : writes) {
writesBuilder.append(write).append("\n");
}
fail("Expected: " + Arrays.toString(fromByteArrayStream) + "\n"
+ "but got: " + Arrays.toString(fromWrappedStream) + "\n"
+ writesBuilder.toString());
}
}

private Write getWrite(Random random, WriteType type) {
switch (type) {
case BYTE:
return getByteWrite(random);
case BUFFER:
return getBufferWrite(random);
case OFFSET_BUFFER:
return getOffsetBufferWrite(random);
default:
throw new IllegalArgumentException("Unrecognized type: " + type);
}
}

private Write getOffsetBufferWrite(Random random) {
int dataSize = random.nextInt(MAX_BYTES_PER_WRITE * 2);
byte[] data = new byte[dataSize];
int length = dataSize == 0 ? 0 : random.nextInt(dataSize);
int offset = dataSize - length <= 0 ? 0 : random.nextInt(dataSize - length);
random.nextBytes(data);
return new Write(data, length, offset, WriteType.OFFSET_BUFFER);
}

private Write getBufferWrite(Random random) {
byte[] data = new byte[random.nextInt(MAX_BYTES_PER_WRITE)];
random.nextBytes(data);
return new Write(data, /*length=*/ data.length, /*offset=*/ 0, WriteType.BUFFER);
}

private Write getByteWrite(Random random) {
byte[] data = new byte[1];
random.nextBytes(data);
return new Write(data, /*length=*/ 1, /*offset=*/ 0, WriteType.BYTE);
}

private WriteType getType(Random random) {
return WriteType.values()[random.nextInt(WriteType.values().length)];
}

private static final class Write {
private final byte[] data;
private final int length;
private final int offset;
private final WriteType writeType;

@Override
public String toString() {
return "Write{"
+ "data=" + Arrays.toString(data)
+ ", length=" + length
+ ", offset=" + offset
+ ", writeType=" + writeType
+ '}';
}

Write(byte[] data, int length, int offset, WriteType writeType) {
this.data = data;
this.length = length;
this.offset = offset;
this.writeType = writeType;
}
}

private enum WriteType {
BYTE,
BUFFER,
OFFSET_BUFFER
}
}
Loading

0 comments on commit a0f3d30

Please sign in to comment.