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.
This commit is contained in:
Scott Mitchell 2015-11-20 10:22:24 -08:00
parent dc13a10e77
commit 9118f94648
5 changed files with 74 additions and 26 deletions

View File

@ -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
* <a href="https://tools.ietf.org/html/rfc7540">rfc7540</a>. {@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;

View File

@ -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());

View File

@ -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<CharSequence, CharSequence> 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;
}
/**

View File

@ -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);

View File

@ -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<K, V, T extends Headers<K, V, T>> implements Headers<K, V, T> {
/**
* How big the underlying array is for the hash data structure.
* <p>
* 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<K, V>[] entries = new DefaultHeaders.HeaderEntry[ARRAY_SIZE];
protected final HeaderEntry<K, V> head = new HeaderEntry<K, V>();
private final HeaderEntry<K, V>[] entries;
protected final HeaderEntry<K, V> head;
private final byte hashMask;
private final ValueConverter<V> valueConverter;
private final NameValidator<K> nameValidator;
private final HashingStrategy<K> hashingStrategy;
@ -102,10 +102,26 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> implements Headers
public DefaultHeaders(HashingStrategy<K> nameHashingStrategy,
ValueConverter<V> valueConverter, NameValidator<K> 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<K> nameHashingStrategy,
ValueConverter<V> valueConverter, NameValidator<K> 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<K, V>();
}
@Override
@ -892,6 +908,10 @@ public class DefaultHeaders<K, V, T extends Headers<K, V, T>> 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<K, V, T extends Headers<K, V, T>> implements Headers
HeaderEntry() {
hash = -1;
key = null;
before = after = this;
}
protected final void pointNeighborsToThis() {