diff --git a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java index 389d8ef150f..666c14c3b18 100644 --- a/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java +++ b/codec-http2/src/main/java/io/netty/handler/codec/http2/HpackStaticTable.java @@ -31,8 +31,8 @@ */ package io.netty.handler.codec.http2; -import io.netty.handler.codec.UnsupportedValueConverter; import io.netty.util.AsciiString; +import io.netty.util.internal.PlatformDependent; import java.util.Arrays; import java.util.List; @@ -117,9 +117,53 @@ private static HpackHeaderField newHeaderField(String name, String value) { return new HpackHeaderField(AsciiString.cached(name), AsciiString.cached(value)); } - private static final CharSequenceMap STATIC_INDEX_BY_NAME = createMap(); + // The table size and bit shift are chosen so that each hash bucket contains a single header name. + private static final int HEADER_NAMES_TABLE_SIZE = 1 << 9; - private static final int MAX_SAME_NAME_FIELD_INDEX = maxSameNameFieldIndex(); + private static final int HEADER_NAMES_TABLE_SHIFT = PlatformDependent.BIG_ENDIAN_NATIVE_ORDER ? 22 : 18; + + // A table mapping header names to their associated indexes. + private static final HeaderNameIndex[] HEADER_NAMES = new HeaderNameIndex[HEADER_NAMES_TABLE_SIZE]; + static { + // Iterate through the static table in reverse order to + // save the smallest index for a given name in the table. + for (int index = STATIC_TABLE.size(); index > 0; index--) { + HpackHeaderField entry = getEntry(index); + int bucket = headerNameBucket(entry.name); + HeaderNameIndex tableEntry = HEADER_NAMES[bucket]; + if (tableEntry != null && !equalsVariableTime(tableEntry.name, entry.name)) { + // Can happen if AsciiString.hashCode changes + throw new IllegalStateException("Hash bucket collision between " + + tableEntry.name + " and " + entry.name); + } + HEADER_NAMES[bucket] = new HeaderNameIndex(entry.name, index, entry.value.length() == 0); + } + } + + // The table size and bit shift are chosen so that each hash bucket contains a single header. + private static final int HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SIZE = 1 << 6; + + private static final int HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SHIFT = + PlatformDependent.BIG_ENDIAN_NATIVE_ORDER ? 0 : 6; + + // A table mapping headers with non-empty values to their associated indexes. + private static final HeaderIndex[] HEADERS_WITH_NON_EMPTY_VALUES = + new HeaderIndex[HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SIZE]; + static { + for (int index = STATIC_TABLE.size(); index > 0; index--) { + HpackHeaderField entry = getEntry(index); + if (entry.value.length() > 0) { + int bucket = headerBucket(entry.value); + HeaderIndex tableEntry = HEADERS_WITH_NON_EMPTY_VALUES[bucket]; + if (tableEntry != null) { + // Can happen if AsciiString.hashCode changes + throw new IllegalStateException("Hash bucket collision between " + + tableEntry.value + " and " + entry.value); + } + HEADERS_WITH_NON_EMPTY_VALUES[bucket] = new HeaderIndex(entry.name, entry.value, index); + } + } + } /** * The number of header fields in the static table. @@ -138,11 +182,8 @@ static HpackHeaderField getEntry(int index) { * -1 if the header field name is not in the static table. */ static int getIndex(CharSequence name) { - Integer index = STATIC_INDEX_BY_NAME.get(name); - if (index == null) { - return NOT_FOUND; - } - return index; + HeaderNameIndex entry = getEntry(name); + return entry == null ? NOT_FOUND : entry.index; } /** @@ -150,70 +191,64 @@ static int getIndex(CharSequence name) { * header field is not in the static table. */ static int getIndexInsensitive(CharSequence name, CharSequence value) { - int index = getIndex(name); - if (index == NOT_FOUND) { + if (value.length() == 0) { + HeaderNameIndex entry = getEntry(name); + return entry == null || !entry.emptyValue ? NOT_FOUND : entry.index; + } + int bucket = headerBucket(value); + HeaderIndex header = HEADERS_WITH_NON_EMPTY_VALUES[bucket]; + if (header == null) { return NOT_FOUND; } - - // Compare values for the first name match - HpackHeaderField entry = getEntry(index); - if (equalsVariableTime(value, entry.value)) { - return index; + if (equalsVariableTime(header.name, name) && equalsVariableTime(header.value, value)) { + return header.index; } + return NOT_FOUND; + } - // Note this assumes all entries for a given header field are sequential. - index++; - while (index <= MAX_SAME_NAME_FIELD_INDEX) { - entry = getEntry(index); - if (!equalsVariableTime(name, entry.name)) { - // As far as fields with the same name are placed in the table sequentially - // and INDEX_BY_NAME returns index of the fist position, - it's safe to - // exit immediately. - return NOT_FOUND; - } - if (equalsVariableTime(value, entry.value)) { - return index; - } - index++; + private static HeaderNameIndex getEntry(CharSequence name) { + int bucket = headerNameBucket(name); + HeaderNameIndex entry = HEADER_NAMES[bucket]; + if (entry == null) { + return null; } + return equalsVariableTime(entry.name, name) ? entry : null; + } - return NOT_FOUND; + private static int headerNameBucket(CharSequence name) { + return bucket(name, HEADER_NAMES_TABLE_SHIFT, HEADER_NAMES_TABLE_SIZE - 1); } - // create a map CharSequenceMap header name to index value to allow quick lookup - private static CharSequenceMap createMap() { - int length = STATIC_TABLE.size(); - @SuppressWarnings("unchecked") - CharSequenceMap ret = new CharSequenceMap(true, - UnsupportedValueConverter.instance(), length); - // Iterate through the static table in reverse order to - // save the smallest index for a given name in the map. - for (int index = length; index > 0; index--) { - HpackHeaderField entry = getEntry(index); - CharSequence name = entry.name; - ret.set(name, index); + private static int headerBucket(CharSequence value) { + return bucket(value, HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SHIFT, HEADERS_WITH_NON_EMPTY_VALUES_TABLE_SIZE - 1); + } + + private static int bucket(CharSequence s, int shift, int mask) { + return (AsciiString.hashCode(s) >> shift) & mask; + } + + private static final class HeaderNameIndex { + final CharSequence name; + final int index; + final boolean emptyValue; + + HeaderNameIndex(CharSequence name, int index, boolean emptyValue) { + this.name = name; + this.index = index; + this.emptyValue = emptyValue; } - return ret; } - /** - * Returns the last position in the array that contains multiple - * fields with the same name. Starting from this position, all - * names are unique. Similar to {@link #getIndexInsensitive(CharSequence, CharSequence)} method - * assumes all entries for a given header field are sequential - */ - private static int maxSameNameFieldIndex() { - final int length = STATIC_TABLE.size(); - HpackHeaderField cursor = getEntry(length); - for (int index = length - 1; index > 0; index--) { - HpackHeaderField entry = getEntry(index); - if (equalsVariableTime(entry.name, cursor.name)) { - return index + 1; - } else { - cursor = entry; - } + private static final class HeaderIndex { + final CharSequence name; + final CharSequence value; + final int index; + + HeaderIndex(CharSequence name, CharSequence value, int index) { + this.name = name; + this.value = value; + this.index = index; } - return length; } // singleton diff --git a/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackStaticTableTest.java b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackStaticTableTest.java new file mode 100644 index 00000000000..6f238f3c4c7 --- /dev/null +++ b/codec-http2/src/test/java/io/netty/handler/codec/http2/HpackStaticTableTest.java @@ -0,0 +1,76 @@ +/* + * Copyright 2022 The Netty Project + * + * The Netty Project licenses this file to you 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: + * + * https://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 io.netty.handler.codec.http2; + +import io.netty.util.AsciiString; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class HpackStaticTableTest { + + @Test + public void testEmptyHeaderName() { + assertEquals(-1, HpackStaticTable.getIndex("")); + } + + @Test + public void testMissingHeaderName() { + assertEquals(-1, HpackStaticTable.getIndex("missing")); + } + + @Test + public void testExistingHeaderName() { + assertEquals(6, HpackStaticTable.getIndex(":scheme")); + } + + @Test + public void testMissingHeaderNameAndValue() { + assertEquals(-1, HpackStaticTable.getIndexInsensitive("missing", "value")); + } + + @Test + public void testMissingHeaderNameButValueExists() { + assertEquals(-1, HpackStaticTable.getIndexInsensitive("missing", "https")); + } + + @Test + public void testExistingHeaderNameAndValueFirstMatch() { + assertEquals(6, HpackStaticTable.getIndexInsensitive(":scheme", "http")); + } + + @Test + public void testExistingHeaderNameAndValueSecondMatch() { + assertEquals(7, HpackStaticTable.getIndexInsensitive( + AsciiString.cached(":scheme"), AsciiString.cached("https"))); + } + + @Test + public void testExistingHeaderNameAndEmptyValueMismatch() { + assertEquals(-1, HpackStaticTable.getIndexInsensitive(":scheme", "")); + } + + @Test + public void testExistingHeaderNameAndEmptyValueMatch() { + assertEquals(27, HpackStaticTable.getIndexInsensitive("content-language", "")); + } + + @Test + public void testExistingHeaderNameButMissingValue() { + assertEquals(-1, HpackStaticTable.getIndexInsensitive(":scheme", "missing")); + } + +} diff --git a/microbench/src/main/java/io/netty/handler/codec/http2/HpackStaticTableBenchmark.java b/microbench/src/main/java/io/netty/handler/codec/http2/HpackStaticTableBenchmark.java index e771cfcea88..4cc7b2fe96a 100644 --- a/microbench/src/main/java/io/netty/handler/codec/http2/HpackStaticTableBenchmark.java +++ b/microbench/src/main/java/io/netty/handler/codec/http2/HpackStaticTableBenchmark.java @@ -43,6 +43,9 @@ public class HpackStaticTableBenchmark extends AbstractMicrobenchmark { private static final CharSequence X_CONTENT_ENCODING = new AsciiString("x-content-encoding".getBytes(CharsetUtil.US_ASCII), false); private static final CharSequence X_GZIP = new AsciiString("x-gzip".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence SCHEME = new AsciiString(":scheme".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence HTTP = new AsciiString("http".getBytes(CharsetUtil.US_ASCII), false); + private static final CharSequence HTTPS = new AsciiString("https".getBytes(CharsetUtil.US_ASCII), false); private static final CharSequence STATUS = new AsciiString(":status".getBytes(CharsetUtil.US_ASCII), false); private static final CharSequence STATUS_200 = new AsciiString("200".getBytes(CharsetUtil.US_ASCII), false); private static final CharSequence STATUS_500 = new AsciiString("500".getBytes(CharsetUtil.US_ASCII), false); @@ -79,6 +82,18 @@ public int lookupNameOnlyMatchBeginTable() { return HpackStaticTable.getIndexInsensitive(AUTHORITY, AUTHORITY_NETTY); } + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int lookupHttp() { + return HpackStaticTable.getIndexInsensitive(SCHEME, HTTP); + } + + @Benchmark + @BenchmarkMode(Mode.AverageTime) + public int lookupHttps() { + return HpackStaticTable.getIndexInsensitive(SCHEME, HTTPS); + } + @Benchmark @BenchmarkMode(Mode.AverageTime) public int lookupNameOnlyMatchEndTable() {