From 9118f94648d506307bb4d4e1bc2fb53b581d9604 Mon Sep 17 00:00:00 2001 From: Scott Mitchell Date: Fri, 20 Nov 2015 10:22:24 -0800 Subject: [PATCH] Adjustable size of DefaultHeaders array Motivation: DefaultHeaders creates an array of size 16 for all headers. This may waste a good deal of memory if applications only have a small number of headers. This memory may be critical when the number of connections grows large. Modifications: - Make the size of the array for DefaultHeaders configurable Result: Applications can control the size of the DefaultHeaders array and save memory. --- .../codec/http2/DefaultHttp2Headers.java | 17 ++++++ .../http2/DefaultHttp2HeadersDecoder.java | 16 ++++-- .../codec/http2/HttpConversionUtil.java | 12 +++-- .../InboundHttp2ToHttpPriorityAdapter.java | 2 +- .../netty/handler/codec/DefaultHeaders.java | 53 +++++++++++++------ 5 files changed, 74 insertions(+), 26 deletions(-) diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java index 69b1e17e07..5b54fd6187 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2Headers.java @@ -90,6 +90,23 @@ public class DefaultHttp2Headers validate ? HTTP2_NAME_VALIDATOR : NameValidator.NOT_NULL); } + /** + * Create a new instance. + * @param validate {@code true} to validate header names according to + * rfc7540. {@code false} to not validate header names. + * @param arraySizeHint A hint as to how large the hash data structure should be. + * The next positive power of two will be used. An upper bound may be enforced. + */ + @SuppressWarnings("unchecked") + public DefaultHttp2Headers(boolean validate, int arraySizeHint) { + // Case sensitive compare is used because it is cheaper, and header validation can be used to catch invalid + // headers. + super(CASE_SENSITIVE_HASHER, + CharSequenceValueConverter.INSTANCE, + validate ? HTTP2_NAME_VALIDATOR : NameValidator.NOT_NULL, + arraySizeHint); + } + @Override public Http2Headers clear() { this.firstNonPseudo = head; diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java index e0f6cb1254..f08a87b278 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/DefaultHttp2HeadersDecoder.java @@ -33,10 +33,18 @@ import static io.netty.handler.codec.http2.Http2Error.PROTOCOL_ERROR; import static io.netty.handler.codec.http2.Http2Exception.connectionError; public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2HeadersDecoder.Configuration { + private static final float HEADERS_COUNT_WEIGHT_NEW = 1 / 5; + private static final float HEADERS_COUNT_WEIGHT_HISTORICAL = 1 - HEADERS_COUNT_WEIGHT_NEW; + private final int maxHeaderSize; private final Decoder decoder; private final Http2HeaderTable headerTable; private final boolean validateHeaders; + /** + * Used to calculate an exponential moving average of header sizes to get an estimate of how large the data + * structure for storing headers should be. + */ + private float headerArraySizeAccumulator = 8; public DefaultHttp2HeadersDecoder() { this(true); @@ -46,10 +54,6 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea this(DEFAULT_MAX_HEADER_SIZE, DEFAULT_HEADER_TABLE_SIZE, validateHeaders); } - public DefaultHttp2HeadersDecoder(int maxHeaderSize, int maxHeaderTableSize) { - this(maxHeaderSize, maxHeaderTableSize, true); - } - public DefaultHttp2HeadersDecoder(int maxHeaderSize, int maxHeaderTableSize, boolean validateHeaders) { if (maxHeaderSize <= 0) { throw new IllegalArgumentException("maxHeaderSize must be positive: " + maxHeaderSize); @@ -87,7 +91,7 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea public Http2Headers decodeHeaders(ByteBuf headerBlock) throws Http2Exception { InputStream in = new ByteBufInputStream(headerBlock); try { - final Http2Headers headers = new DefaultHttp2Headers(validateHeaders); + final Http2Headers headers = new DefaultHttp2Headers(validateHeaders, (int) headerArraySizeAccumulator); HeaderListener listener = new HeaderListener() { @Override public void addHeader(byte[] key, byte[] value, boolean sensitive) { @@ -105,6 +109,8 @@ public class DefaultHttp2HeadersDecoder implements Http2HeadersDecoder, Http2Hea headers.size(), headerTable.maxHeaderListSize()); } + headerArraySizeAccumulator = HEADERS_COUNT_WEIGHT_NEW * headers.size() + + HEADERS_COUNT_WEIGHT_HISTORICAL * headerArraySizeAccumulator; return headers; } catch (IOException e) { throw connectionError(COMPRESSION_ERROR, e, e.getMessage()); diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java index afa1aeee91..e4d7e94d37 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HttpConversionUtil.java @@ -288,8 +288,8 @@ public final class HttpConversionUtil { * {@link ExtensionHeaderNames#PATH} is ignored and instead extracted from the {@code Request-Line}. */ public static Http2Headers toHttp2Headers(HttpMessage in, boolean validateHeaders) throws Exception { - final Http2Headers out = new DefaultHttp2Headers(validateHeaders); HttpHeaders inHeaders = in.headers(); + final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size()); if (in instanceof HttpRequest) { HttpRequest request = (HttpRequest) in; URI requestTargetUri = URI.create(request.uri()); @@ -308,7 +308,8 @@ public final class HttpConversionUtil { } // Add the HTTP headers which have not been consumed above - return out.add(toHttp2Headers(inHeaders, validateHeaders)); + toHttp2Headers(inHeaders, out); + return out; } public static Http2Headers toHttp2Headers(HttpHeaders inHeaders, boolean validateHeaders) throws Exception { @@ -316,8 +317,12 @@ public final class HttpConversionUtil { return EmptyHttp2Headers.INSTANCE; } - final Http2Headers out = new DefaultHttp2Headers(validateHeaders); + final Http2Headers out = new DefaultHttp2Headers(validateHeaders, inHeaders.size()); + toHttp2Headers(inHeaders, out); + return out; + } + public static void toHttp2Headers(HttpHeaders inHeaders, Http2Headers out) throws Exception { for (Entry entry : inHeaders) { final AsciiString aName = AsciiString.of(entry.getKey()).toLowerCase(); if (!HTTP_TO_HTTP2_HEADER_BLACKLIST.contains(aName)) { @@ -328,7 +333,6 @@ public final class HttpConversionUtil { } } } - return out; } /** diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java index 87ba14e378..9a6aa6c3cd 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/InboundHttp2ToHttpPriorityAdapter.java @@ -206,7 +206,7 @@ public final class InboundHttp2ToHttpPriorityAdapter extends InboundHttp2ToHttpA throw connectionError(PROTOCOL_ERROR, "Priority Frame recieved for unknown stream id %d", streamId); } - Http2Headers http2Headers = new DefaultHttp2Headers(); + Http2Headers http2Headers = new DefaultHttp2Headers(validateHttpHeaders, httpHeaders.size()); initializePseudoHeaders(http2Headers); addHttpHeadersToHttp2Headers(httpHeaders, http2Headers); msg = newMessage(streamId, http2Headers, validateHttpHeaders); diff --git a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java index 442cbf43bb..1828f1647c 100644 --- a/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java +++ b/codec/src/main/java/io/netty/handler/codec/DefaultHeaders.java @@ -16,6 +16,7 @@ package io.netty.handler.codec; import io.netty.util.HashingStrategy; import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.internal.SystemPropertyUtil; import java.text.DateFormat; import java.text.ParseException; @@ -34,7 +35,10 @@ import java.util.Set; import java.util.TimeZone; import static io.netty.util.HashingStrategy.JAVA_HASHER; +import static io.netty.util.internal.MathUtil.findNextPositivePowerOfTwo; import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static java.lang.Math.min; +import static java.lang.Math.max; /** * Default implementation of {@link Headers}; @@ -45,24 +49,20 @@ import static io.netty.util.internal.ObjectUtil.checkNotNull; */ public class DefaultHeaders> implements Headers { /** - * How big the underlying array is for the hash data structure. - *

- * This should be a power of 2 so the {@link #index(int)} method can full address the memory. + * Enforce an upper bound of 128 because {@link #hashMask} is a byte. + * The max possible value of {@link #hashMask} is one less than this value. */ - private static final int ARRAY_SIZE = 1 << 4; - private static final int HASH_MASK = ARRAY_SIZE - 1; - static final int HASH_CODE_SEED = 0xc2b2ae35; // constant borrowed from murmur3 + private static final int ARRAY_SIZE_HINT_MAX = min(128, + max(1, SystemPropertyUtil.getInt("io.netty.DefaultHeaders.arraySizeHintMax", 16))); + /** + * Constant used to seed the hash code generation. Could be anything but this was borrowed from murmur3. + */ + static final int HASH_CODE_SEED = 0xc2b2ae35; - private static int index(int hash) { - // Fold the upper 16 bits onto the 16 lower bits so more of the hash code is represented - // when translating to an index. - return ((hash >>> 16) ^ hash) & HASH_MASK; - } - - @SuppressWarnings("unchecked") - private final HeaderEntry[] entries = new DefaultHeaders.HeaderEntry[ARRAY_SIZE]; - protected final HeaderEntry head = new HeaderEntry(); + private final HeaderEntry[] entries; + protected final HeaderEntry head; + private final byte hashMask; private final ValueConverter valueConverter; private final NameValidator nameValidator; private final HashingStrategy hashingStrategy; @@ -102,10 +102,26 @@ public class DefaultHeaders> implements Headers public DefaultHeaders(HashingStrategy nameHashingStrategy, ValueConverter valueConverter, NameValidator nameValidator) { + this(nameHashingStrategy, valueConverter, nameValidator, 16); + } + + /** + * Create a new instance. + * @param nameHashingStrategy Used to hash and equality compare names. + * @param valueConverter Used to convert values to/from native types. + * @param nameValidator Used to validate name elements. + * @param arraySizeHint A hint as to how large the hash data structure should be. + * The next positive power of two will be used. An upper bound may be enforced. + */ + @SuppressWarnings("unchecked") + public DefaultHeaders(HashingStrategy nameHashingStrategy, + ValueConverter valueConverter, NameValidator nameValidator, int arraySizeHint) { this.valueConverter = checkNotNull(valueConverter, "valueConverter"); this.nameValidator = checkNotNull(nameValidator, "nameValidator"); this.hashingStrategy = checkNotNull(nameHashingStrategy, "nameHashingStrategy"); - head.before = head.after = head; + entries = new DefaultHeaders.HeaderEntry[findNextPositivePowerOfTwo(min(arraySizeHint, ARRAY_SIZE_HINT_MAX))]; + hashMask = (byte) (entries.length - 1); + head = new HeaderEntry(); } @Override @@ -892,6 +908,10 @@ public class DefaultHeaders> implements Headers return valueConverter; } + private int index(int hash) { + return hash & hashMask; + } + private void add0(int h, int i, K name, V value) { // Update the hash table. entries[i] = newHeaderEntry(h, name, value, entries[i]); @@ -1068,6 +1088,7 @@ public class DefaultHeaders> implements Headers HeaderEntry() { hash = -1; key = null; + before = after = this; } protected final void pointNeighborsToThis() {