HttpObjectEncoder buffer size estimation

Motivation:
HttpObjectEncoder allocates a new buffer when encoding the initial line and headers, and also allocates a buffer when encoding the trailers. The allocation always uses the default size of 256. This may lead to consistent under allocation and require a few resize/copy operations which can cause GC/memory pressure.

Modifications:
- Introduce a weighted average which tracks the historical size of encoded data and uses this as an estimate for future buffer allocations

Result:
Better approximation of buffer sizes.
This commit is contained in:
Scott Mitchell 2017-08-28 17:58:00 -07:00
parent 7528e5a11e
commit 9bd6d8129e

View File

@ -53,6 +53,10 @@ public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageTo
private static final ByteBuf CRLF_BUF = unreleasableBuffer(directBuffer(2).writeByte(CR).writeByte(LF)); private static final ByteBuf CRLF_BUF = unreleasableBuffer(directBuffer(2).writeByte(CR).writeByte(LF));
private static final ByteBuf ZERO_CRLF_CRLF_BUF = unreleasableBuffer(directBuffer(ZERO_CRLF_CRLF.length) private static final ByteBuf ZERO_CRLF_CRLF_BUF = unreleasableBuffer(directBuffer(ZERO_CRLF_CRLF.length)
.writeBytes(ZERO_CRLF_CRLF)); .writeBytes(ZERO_CRLF_CRLF));
private static final float HEADERS_WEIGHT_NEW = 1 / 5f;
private static final float HEADERS_WEIGHT_HISTORICAL = 1 - HEADERS_WEIGHT_NEW;
private static final float TRAILERS_WEIGHT_NEW = HEADERS_WEIGHT_NEW;
private static final float TRAILERS_WEIGHT_HISTORICAL = HEADERS_WEIGHT_HISTORICAL;
private static final int ST_INIT = 0; private static final int ST_INIT = 0;
private static final int ST_CONTENT_NON_CHUNK = 1; private static final int ST_CONTENT_NON_CHUNK = 1;
@ -62,6 +66,18 @@ public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageTo
@SuppressWarnings("RedundantFieldInitialization") @SuppressWarnings("RedundantFieldInitialization")
private int state = ST_INIT; private int state = ST_INIT;
/**
* Used to calculate an exponential moving average of the encoded size of the initial line and the headers for
* a guess for future buffer allocations.
*/
private float headersEncodedSizeAccumulator = 256;
/**
* Used to calculate an exponential moving average of the encoded size of the trailers for
* a guess for future buffer allocations.
*/
private float trailersEncodedSizeAccumulator = 256;
@Override @Override
protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception { protected void encode(ChannelHandlerContext ctx, Object msg, List<Object> out) throws Exception {
ByteBuf buf = null; ByteBuf buf = null;
@ -73,10 +89,12 @@ public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageTo
@SuppressWarnings({ "unchecked", "CastConflictsWithInstanceof" }) @SuppressWarnings({ "unchecked", "CastConflictsWithInstanceof" })
H m = (H) msg; H m = (H) msg;
buf = ctx.alloc().buffer(); buf = ctx.alloc().buffer((int) headersEncodedSizeAccumulator);
// Encode the message. // Encode the message.
encodeInitialLine(buf, m); encodeInitialLine(buf, m);
encodeHeaders(m.headers(), buf); encodeHeaders(m.headers(), buf);
headersEncodedSizeAccumulator = HEADERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
HEADERS_WEIGHT_HISTORICAL * headersEncodedSizeAccumulator;
ByteBufUtil.writeShortBE(buf, CRLF_SHORT); ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
state = isContentAlwaysEmpty(m) ? ST_CONTENT_ALWAYS_EMPTY : state = isContentAlwaysEmpty(m) ? ST_CONTENT_ALWAYS_EMPTY :
HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK; HttpUtil.isTransferEncodingChunked(m) ? ST_CONTENT_CHUNK : ST_CONTENT_NON_CHUNK;
@ -177,10 +195,12 @@ public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageTo
if (headers.isEmpty()) { if (headers.isEmpty()) {
out.add(ZERO_CRLF_CRLF_BUF.duplicate()); out.add(ZERO_CRLF_CRLF_BUF.duplicate());
} else { } else {
ByteBuf buf = ctx.alloc().buffer(); ByteBuf buf = ctx.alloc().buffer((int) trailersEncodedSizeAccumulator);
ByteBufUtil.writeMediumBE(buf, ZERO_CRLF_MEDIUM); ByteBufUtil.writeMediumBE(buf, ZERO_CRLF_MEDIUM);
encodeHeaders(headers, buf); encodeHeaders(headers, buf);
ByteBufUtil.writeShortBE(buf, CRLF_SHORT); ByteBufUtil.writeShortBE(buf, CRLF_SHORT);
trailersEncodedSizeAccumulator = TRAILERS_WEIGHT_NEW * padSizeForAccumulation(buf.readableBytes()) +
TRAILERS_WEIGHT_HISTORICAL * trailersEncodedSizeAccumulator;
out.add(buf); out.add(buf);
} }
} else if (contentLength == 0) { } else if (contentLength == 0) {
@ -232,6 +252,16 @@ public abstract class HttpObjectEncoder<H extends HttpMessage> extends MessageTo
throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg)); throw new IllegalStateException("unexpected message type: " + StringUtil.simpleClassName(msg));
} }
/**
* Add some additional overhead to the buffer. The rational is that it is better to slightly over allocate and waste
* some memory, rather than under allocate and require a resize/copy.
* @param readableBytes The readable bytes in the buffer.
* @return The {@code readableBytes} with some additional padding.
*/
private static int padSizeForAccumulation(int readableBytes) {
return (readableBytes << 2) / 3;
}
@Deprecated @Deprecated
protected static void encodeAscii(String s, ByteBuf buf) { protected static void encodeAscii(String s, ByteBuf buf) {
buf.writeCharSequence(s, CharsetUtil.US_ASCII); buf.writeCharSequence(s, CharsetUtil.US_ASCII);