1
0
mirror of https://github.com/square/okhttp.git synced 2026-01-24 04:02:07 +03:00

Merge pull request #485 from square/okbuffer

OkBuffer API sketch.
This commit is contained in:
Jesse Wilson
2014-01-26 16:07:35 -08:00
21 changed files with 529 additions and 14 deletions

View File

@@ -18,7 +18,7 @@
package com.squareup.okhttp.mockwebserver;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import com.squareup.okhttp.internal.NamedRunnable;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;

View File

@@ -15,7 +15,7 @@
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.util.Arrays;

View File

@@ -17,6 +17,7 @@
package com.squareup.okhttp.internal;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.internal.bytes.ByteString;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Constructor;

View File

@@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal;
package com.squareup.okhttp.internal.bytes;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -34,7 +35,7 @@ import static com.squareup.okhttp.internal.Util.asciiLowerCase;
* process.
*/
public final class ByteString {
private final byte[] data;
final byte[] data;
private transient int hashCode; // Lazily computed; 0 if unknown.
private transient String utf8; // Lazily computed.
@@ -119,7 +120,7 @@ public final class ByteString {
return new ByteString(result);
}
private ByteString(byte[] data) {
ByteString(byte[] data) {
this.data = data; // Trusted internal constructor doesn't clone data.
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.bytes;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
/**
* A collection of bytes in memory.
*
* <p><strong>Moving data from one OkBuffer to another is fast.</strong> Instead
* of copying bytes from one place in memory to another, this class just changes
* ownership of the underlying bytes.
*
* <p><strong>This buffer grows with your data.</strong> Just like ArrayList,
* each OkBuffer starts small. It consumes only the memory it needs to.
*
* <p><strong>This buffer pools its byte arrays.</strong> When you allocate a
* byte array in Java, the runtime must zero-fill the requested array before
* returning it to you. Even if you're going to write over that space anyway.
* This class avoids zero-fill and GC churn by pooling byte arrays.
*/
public final class OkBuffer implements Source, Sink {
private static final char[] HEX_DIGITS =
{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
private Segment segment;
private long byteCount;
public OkBuffer() {
}
/** Returns the number of bytes currently in this buffer. */
public long byteCount() {
return byteCount;
}
/** Removes {@code byteCount} bytes from this and returns them as a byte string. */
public ByteString readByteString(int byteCount) {
return new ByteString(readBytes(byteCount));
}
/** Removes {@code byteCount} bytes from this, decodes them as UTF-8 and returns the string. */
public String readUtf8(int byteCount) {
return new String(readBytes(byteCount), Util.UTF_8);
}
private byte[] readBytes(int byteCount) {
if (byteCount > this.byteCount) {
throw new IllegalArgumentException(
String.format("requested %s > available %s", byteCount, this.byteCount));
}
int offset = 0;
byte[] result = new byte[byteCount];
while (offset < byteCount) {
int toCopy = Math.min(byteCount - offset, segment.limit - segment.pos);
System.arraycopy(segment.data, segment.pos, result, offset, toCopy);
offset += toCopy;
segment.pos += toCopy;
if (segment.pos == segment.limit) {
segment = segment.pop(); // Recycle this empty segment.
}
}
this.byteCount -= byteCount;
return result;
}
/** Appends {@code byteString} to this. */
public void write(ByteString byteString) {
write(byteString.data);
}
/** Encodes {@code string} as UTF-8 and appends the bytes to this. */
public void writeUtf8(String string) {
write(string.getBytes(Util.UTF_8));
}
private void write(byte[] data) {
int offset = 0;
while (offset < data.length) {
if (segment == null) {
segment = SegmentPool.INSTANCE.take(); // Acquire a first segment.
segment.next = segment.prev = segment;
}
Segment tail = segment.prev;
if (tail.limit == Segment.SIZE) {
tail = tail.push(); // Acquire a new empty segment.
}
int toCopy = Math.min(data.length - offset, Segment.SIZE - tail.limit);
System.arraycopy(data, offset, tail.data, tail.limit, toCopy);
offset += toCopy;
tail.limit += toCopy;
}
this.byteCount += data.length;
}
@Override public void write(OkBuffer source, long byteCount, Timeout timeout) {
throw new UnsupportedOperationException();
}
@Override public long read(OkBuffer sink, long byteCount, Timeout timeout) throws IOException {
throw new UnsupportedOperationException();
}
@Override public long indexOf(byte b, Timeout timeout) throws IOException {
throw new UnsupportedOperationException();
}
@Override public void flush(Timeout timeout) {
throw new UnsupportedOperationException();
}
@Override public void close(Timeout timeout) {
throw new UnsupportedOperationException();
}
/**
* Returns the contents of this buffer in hex. For buffers larger than 1 MiB
* this method is undefined.
*/
@Override public String toString() {
if (byteCount > 0x100000) return super.toString();
char[] result = new char[(int) (byteCount * 2)];
int offset = 0;
for (Segment s = segment; offset < byteCount; s = s.next) {
for (int i = s.pos; i < s.limit; i++) {
result[offset++] = HEX_DIGITS[(s.data[i] >> 4) & 0xf];
result[offset++] = HEX_DIGITS[s.data[i] & 0xf];
}
offset += s.limit - s.pos;
}
return new String(result);
}
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.bytes;
/**
* A segment of an OkBuffer.
*
* <p>Each segment in an OkBuffer is a circularly-linked list node referencing
* the following and preceding segments in the buffer.
*
* <p>Each segment in the pool is a singly-linked list node referencing the rest
* of segments in the pool.
*/
final class Segment {
/** The size of all segments in bytes. */
// TODO: Using fixed-size segments makes pooling easier. But it harms memory
// efficiency and encourages copying. Try variable sized segments?
// TODO: Is 2 KiB a good default segment size?
static final int SIZE = 2048;
final byte[] data;
int pos;
int limit;
/** Next segment in a linked list. */
Segment next;
/** Previous segment in a linked list. */
Segment prev;
Segment() {
data = new byte[SIZE];
}
/**
* Removes this head of a circularly-linked list, recycles it, and returns the
* new head of the list. Returns null if the list is now empty.
*/
public Segment pop() {
Segment result = next != this ? next : null;
prev.next = next;
next.prev = prev;
next = null;
prev = null;
SegmentPool.INSTANCE.recycle(this);
return result;
}
/**
* Acquires a segment and appends it to this tail of a circularly-linked list.
* Returns the new tail segment.
*/
public Segment push() {
Segment result = SegmentPool.INSTANCE.take();
result.prev = this;
result.next = next;
next.prev = result;
next = result;
return result;
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.bytes;
/**
* A collection of unused segments, necessary to avoid GC churn and zero-fill.
* This pool is a thread-safe static singleton.
*/
final class SegmentPool {
static final SegmentPool INSTANCE = new SegmentPool();
/** The maximum number of bytes to pool. */
// TODO: Is 64 KiB a good maximum size? Do we ever have that many idle segments?
static final long MAX_SIZE = 64 * 1024; // 64 KiB.
/** Singly-linked list of segments. */
private Segment next;
/** Total bytes in this pool. */
long byteCount;
private SegmentPool() {
}
Segment take() {
synchronized (this) {
if (next != null) {
Segment result = next;
next = result.next;
result.next = null;
byteCount -= Segment.SIZE;
return result;
}
}
return new Segment(); // Pool is empty. Don't zero-fill while holding a lock.
}
void recycle(Segment segment) {
if (segment.next != null || segment.prev != null) throw new IllegalArgumentException();
synchronized (this) {
if (byteCount + Segment.SIZE > MAX_SIZE) return; // Pool is full.
byteCount += Segment.SIZE;
segment.next = next;
segment.pos = segment.limit = 0;
next = segment;
}
}
}

View File

@@ -0,0 +1,36 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.bytes;
import java.io.IOException;
/**
* An alternative to OutputStream.
*/
public interface Sink {
/** Removes {@code byteCount} bytes from {@code source} and appends them to this. */
void write(OkBuffer source, long byteCount, Timeout timeout) throws IOException;
/** Pushes all buffered bytes to their final destination. */
void flush(Timeout timeout) throws IOException;
/**
* Pushes all buffered bytes to their final destination and releases the
* resources held by this sink. It is an error to write a closed sink. It is
* safe to close a sink more than once.
*/
void close(Timeout timeout) throws IOException;
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.bytes;
import java.io.IOException;
/**
* An alternative to InputStream.
*/
public interface Source {
/**
* Removes {@code byteCount} bytes from this and appends them to {@code sink}.
* Returns the number of bytes actually written.
*/
long read(OkBuffer sink, long byteCount, Timeout timeout) throws IOException;
/**
* Returns the index of {@code b} in this, or -1 if this source is exhausted
* first. This may cause this source to buffer a large number of bytes.
*/
long indexOf(byte b, Timeout timeout) throws IOException;
/**
* Closes this source and releases the resources held by this source. It is an
* error to read a closed source. It is safe to close a source more than once.
*/
void close(Timeout timeout) throws IOException;
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.bytes;
import java.util.concurrent.TimeUnit;
/**
* The deadline for a requested operation. If the timeout elapses before the
* operation has completed, the operation should be aborted.
*/
public class Timeout {
public static final Timeout NONE = new Timeout() {
@Override public Timeout start(long timeout, TimeUnit unit) {
throw new UnsupportedOperationException();
}
@Override public boolean reached() {
return false;
}
};
private long deadlineNanos;
public Timeout() {
}
public Timeout start(long timeout, TimeUnit unit) {
deadlineNanos = System.nanoTime() + unit.toNanos(timeout);
return this;
}
public boolean reached() {
return System.nanoTime() - deadlineNanos >= 0; // Subtract to avoid overflow!
}
}

View File

@@ -1,6 +1,6 @@
package com.squareup.okhttp.internal.spdy;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
/** HTTP header: the name is an ASCII string, but the value can be UTF-8. */
public final class Header {

View File

@@ -1,7 +1,7 @@
package com.squareup.okhttp.internal.spdy;
import com.squareup.okhttp.internal.BitArray;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import com.squareup.okhttp.internal.Util;
import java.io.IOException;
import java.io.InputStream;

View File

@@ -1,6 +1,6 @@
package com.squareup.okhttp.internal.spdy;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import com.squareup.okhttp.internal.Util;
import java.io.Closeable;
import java.io.DataInputStream;

View File

@@ -16,9 +16,9 @@
package com.squareup.okhttp.internal.spdy;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.bytes.ByteString;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;

View File

@@ -15,6 +15,7 @@
*/
package com.squareup.okhttp.internal;
import com.squareup.okhttp.internal.bytes.ByteString;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.okhttp.internal.bytes;
import java.util.Arrays;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
public final class OkBufferTest {
@Test public void readAndWriteUtf8() throws Exception {
OkBuffer buffer = new OkBuffer();
buffer.writeUtf8("ab");
assertEquals(2, buffer.byteCount());
buffer.writeUtf8("cdef");
assertEquals(6, buffer.byteCount());
assertEquals("abcd", buffer.readUtf8(4));
assertEquals(2, buffer.byteCount());
assertEquals("ef", buffer.readUtf8(2));
assertEquals(0, buffer.byteCount());
try {
buffer.readUtf8(1);
fail();
} catch (IllegalArgumentException expected) {
}
}
@Test public void bufferToString() throws Exception {
OkBuffer buffer = new OkBuffer();
buffer.writeUtf8("\u0000\u0001\u0002\u007f");
assertEquals("0001027f", buffer.toString());
}
@Test public void multipleSegmentBuffers() throws Exception {
OkBuffer buffer = new OkBuffer();
buffer.writeUtf8(repeat('a', 1000));
buffer.writeUtf8(repeat('b', 2500));
buffer.writeUtf8(repeat('c', 5000));
buffer.writeUtf8(repeat('d', 10000));
buffer.writeUtf8(repeat('e', 25000));
buffer.writeUtf8(repeat('f', 50000));
assertEquals(repeat('a', 999), buffer.readUtf8(999)); // a...a
assertEquals("a" + repeat('b', 2500) + "c", buffer.readUtf8(2502)); // ab...bc
assertEquals(repeat('c', 4998), buffer.readUtf8(4998)); // c...c
assertEquals("c" + repeat('d', 10000) + "e", buffer.readUtf8(10002)); // cd...de
assertEquals(repeat('e', 24998), buffer.readUtf8(24998)); // e...e
assertEquals("e" + repeat('f', 50000), buffer.readUtf8(50001)); // ef...f
assertEquals(0, buffer.byteCount());
}
@Test public void fillAndDrainPool() throws Exception {
OkBuffer buffer = new OkBuffer();
// Take 2 * MAX_SIZE segments. This will drain the pool, even if other tests filled it.
buffer.write(ByteString.of(new byte[(int) SegmentPool.MAX_SIZE]));
buffer.write(ByteString.of(new byte[(int) SegmentPool.MAX_SIZE]));
assertEquals(0, SegmentPool.INSTANCE.byteCount);
// Recycle MAX_SIZE segments. They're all in the pool.
buffer.readByteString((int) SegmentPool.MAX_SIZE);
assertEquals(SegmentPool.MAX_SIZE, SegmentPool.INSTANCE.byteCount);
// Recycle MAX_SIZE more segments. The pool is full so they get garbage collected.
buffer.readByteString((int) SegmentPool.MAX_SIZE);
assertEquals(SegmentPool.MAX_SIZE, SegmentPool.INSTANCE.byteCount);
// Take MAX_SIZE segments to drain the pool.
buffer.write(ByteString.of(new byte[(int) SegmentPool.MAX_SIZE]));
assertEquals(0, SegmentPool.INSTANCE.byteCount);
// Take MAX_SIZE more segments. The pool is drained so these will need to be allocated.
buffer.write(ByteString.of(new byte[(int) SegmentPool.MAX_SIZE]));
assertEquals(0, SegmentPool.INSTANCE.byteCount);
}
private String repeat(char c, int count) {
char[] array = new char[count];
Arrays.fill(array, c);
return new String(array);
}
}

View File

@@ -15,7 +15,7 @@
*/
package com.squareup.okhttp.internal.spdy;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;

View File

@@ -16,7 +16,7 @@
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.http.HttpEngine;

View File

@@ -15,7 +15,7 @@
*/
package com.squareup.okhttp;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.http.HttpAuthenticator;
import com.squareup.okhttp.internal.http.HttpURLConnectionImpl;

View File

@@ -24,7 +24,7 @@ import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.Route;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import com.squareup.okhttp.internal.Platform;
import com.squareup.okhttp.internal.Util;
import java.io.FileNotFoundException;

View File

@@ -20,7 +20,7 @@ import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Protocol;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.Response;
import com.squareup.okhttp.internal.ByteString;
import com.squareup.okhttp.internal.bytes.ByteString;
import com.squareup.okhttp.internal.Util;
import com.squareup.okhttp.internal.spdy.ErrorCode;
import com.squareup.okhttp.internal.spdy.Header;