-
Notifications
You must be signed in to change notification settings - Fork 6.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a BufferedOutputStream that works with ArrayPool to reuse byte[]s.
- Loading branch information
Showing
3 changed files
with
1,214 additions
and
0 deletions.
There are no files selected for viewing
102 changes: 102 additions & 0 deletions
102
library/src/main/java/com/bumptech/glide/load/data/BufferedOutputStream.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
172 changes: 172 additions & 0 deletions
172
library/test/src/test/java/com/bumptech/glide/load/data/BufferedOutputStreamFuzzTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
Oops, something went wrong.