Permalink
Browse files

Read Only Http2Headers

Motivation:
A read only implementation of Http2Headers can allow for a more efficient usage of memory and more performant combined construction and iteration during serialization.

Modifications:
- Add a new ReadOnlyHttp2Headers class

Result:
ReadOnlyHttp2Headers exists and can be used for performance reasons when appropriate.

```
Benchmark                                            (headerCount)  Mode  Cnt    Score   Error  Units
ReadOnlyHttp2HeadersBenchmark.defaultClientHeaders               1  avgt   20   96.156 ± 1.902  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultClientHeaders               5  avgt   20  157.925 ± 3.847  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultClientHeaders              10  avgt   20  236.257 ± 2.663  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultClientHeaders              20  avgt   20  392.861 ± 3.932  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultServerHeaders               1  avgt   20   48.759 ± 0.466  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultServerHeaders               5  avgt   20  113.122 ± 0.948  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultServerHeaders              10  avgt   20  192.698 ± 1.936  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultServerHeaders              20  avgt   20  348.974 ± 3.111  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultTrailers                    1  avgt   20   35.694 ± 0.271  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultTrailers                    5  avgt   20   98.993 ± 2.933  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultTrailers                   10  avgt   20  171.035 ± 5.068  ns/op
ReadOnlyHttp2HeadersBenchmark.defaultTrailers                   20  avgt   20  330.621 ± 3.381  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyClientHeaders              1  avgt   20   40.573 ± 0.474  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyClientHeaders              5  avgt   20   56.516 ± 0.660  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyClientHeaders             10  avgt   20   76.890 ± 0.776  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyClientHeaders             20  avgt   20  117.531 ± 1.393  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyServerHeaders              1  avgt   20   29.206 ± 0.264  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyServerHeaders              5  avgt   20   44.587 ± 0.312  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyServerHeaders             10  avgt   20   64.458 ± 1.169  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyServerHeaders             20  avgt   20  107.179 ± 0.881  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyTrailers                   1  avgt   20   21.563 ± 0.202  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyTrailers                   5  avgt   20   41.019 ± 0.440  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyTrailers                  10  avgt   20   64.053 ± 0.785  ns/op
ReadOnlyHttp2HeadersBenchmark.readOnlyTrailers                  20  avgt   20  113.737 ± 4.433  ns/op
```
  • Loading branch information...
1 parent fe2b55c commit 06e7627b5f39b96c05042d7bd85218560293fc19 @Scottmitch Scottmitch committed Nov 12, 2016
@@ -34,7 +34,7 @@ public boolean process(byte value) throws Exception {
return !isUpperCase(value);
}
};
- private static final NameValidator<CharSequence> HTTP2_NAME_VALIDATOR = new NameValidator<CharSequence>() {
+ static final NameValidator<CharSequence> HTTP2_NAME_VALIDATOR = new NameValidator<CharSequence>() {
@Override
public void validateName(CharSequence name) {
if (name == null || name.length() == 0) {
Oops, something went wrong.
@@ -153,7 +153,7 @@ private static void verifyAllPseudoHeadersPresent(Http2Headers headers) {
}
}
- private static void verifyPseudoHeadersFirst(Http2Headers headers) {
+ static void verifyPseudoHeadersFirst(Http2Headers headers) {
CharSequence lastNonPseudoName = null;
for (Entry<CharSequence, CharSequence> entry: headers) {
if (entry.getKey().length() == 0 || entry.getKey().charAt(0) != ':') {
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2016 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:
+ *
+ * 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 io.netty.handler.codec.http2;
+
+import io.netty.util.AsciiString;
+import org.junit.Test;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import static io.netty.handler.codec.http2.DefaultHttp2HeadersTest.verifyPseudoHeadersFirst;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class ReadOnlyHttp2HeadersTest {
+ @Test(expected = IllegalArgumentException.class)
+ public void notKeyValuePairThrows() {
+ ReadOnlyHttp2Headers.trailers(false, new AsciiString[]{ null });
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void nullTrailersNotAllowed() {
+ ReadOnlyHttp2Headers.trailers(false, (AsciiString[]) null);
+ }
+
+ @Test
+ public void nullHeaderNameNotChecked() {
+ ReadOnlyHttp2Headers.trailers(false, null, null);
+ }
+
+ @Test(expected = Http2Exception.class)
+ public void nullHeaderNameValidated() {
+ ReadOnlyHttp2Headers.trailers(true, null, new AsciiString("foo"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void pseudoHeaderNotAllowedAfterNonPseudoHeaders() {
+ ReadOnlyHttp2Headers.trailers(true, new AsciiString(":name"), new AsciiString("foo"),
+ new AsciiString("othername"), new AsciiString("goo"),
+ new AsciiString(":pseudo"), new AsciiString("val"));
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void nullValuesAreNotAllowed() {
+ ReadOnlyHttp2Headers.trailers(true, new AsciiString("foo"), null);
+ }
+
+ @Test
+ public void emtpyHeaderNameAllowed() {
+ ReadOnlyHttp2Headers.trailers(false, AsciiString.EMPTY_STRING, new AsciiString("foo"));
+ }
+
+ @Test
+ public void testPseudoHeadersMustComeFirstWhenIteratingServer() {
+ Http2Headers headers = newServerHeaders();
+ verifyPseudoHeadersFirst(headers);
+ }
+
+ @Test
+ public void testPseudoHeadersMustComeFirstWhenIteratingClient() {
+ Http2Headers headers = newClientHeaders();
+ verifyPseudoHeadersFirst(headers);
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testIteratorReadOnlyClient() {
+ testIteratorReadOnly(newClientHeaders());
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testIteratorReadOnlyServer() {
+ testIteratorReadOnly(newServerHeaders());
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testIteratorReadOnlyTrailers() {
+ testIteratorReadOnly(newTrailers());
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testIteratorEntryReadOnlyClient() {
+ testIteratorEntryReadOnly(newClientHeaders());
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testIteratorEntryReadOnlyServer() {
+ testIteratorEntryReadOnly(newServerHeaders());
+ }
+
+ @Test(expected = UnsupportedOperationException.class)
+ public void testIteratorEntryReadOnlyTrailers() {
+ testIteratorEntryReadOnly(newTrailers());
+ }
+
+ @Test
+ public void testSize() {
+ Http2Headers headers = newTrailers();
+ assertEquals(otherHeaders().length / 2, headers.size());
+ }
+
+ @Test
+ public void testIsNotEmpty() {
+ Http2Headers headers = newTrailers();
+ assertFalse(headers.isEmpty());
+ }
+
+ @Test
+ public void testIsEmpty() {
+ Http2Headers headers = ReadOnlyHttp2Headers.trailers(false);
+ assertTrue(headers.isEmpty());
+ }
+
+ @Test
+ public void testContainsName() {
+ Http2Headers headers = newClientHeaders();
+ assertTrue(headers.contains("Name1"));
+ assertTrue(headers.contains(Http2Headers.PseudoHeaderName.PATH.value()));
+ assertFalse(headers.contains(Http2Headers.PseudoHeaderName.STATUS.value()));
+ assertFalse(headers.contains("a missing header"));
+ }
+
+ @Test
+ public void testContainsNameAndValue() {
+ Http2Headers headers = newClientHeaders();
+ assertTrue(headers.contains("Name1", "Value1"));
+ assertTrue(headers.contains(Http2Headers.PseudoHeaderName.PATH.value(), "/foo"));
+ assertFalse(headers.contains(Http2Headers.PseudoHeaderName.STATUS.value(), "200"));
+ assertFalse(headers.contains("a missing header", "a missing value"));
+ }
+
+ @Test
+ public void testGet() {
+ Http2Headers headers = newClientHeaders();
+ assertTrue(AsciiString.contentEqualsIgnoreCase("value1", headers.get("Name1")));
+ assertTrue(AsciiString.contentEqualsIgnoreCase("/foo",
+ headers.get(Http2Headers.PseudoHeaderName.PATH.value())));
+ assertEquals(null, headers.get(Http2Headers.PseudoHeaderName.STATUS.value()));
+ assertEquals(null, headers.get("a missing header"));
+ }
+
+ private void testIteratorReadOnly(Http2Headers headers) {
+ Iterator<Map.Entry<CharSequence, CharSequence>> itr = headers.iterator();
+ assertTrue(itr.hasNext());
+ itr.remove();
+ }
+
+ private void testIteratorEntryReadOnly(Http2Headers headers) {
+ Iterator<Map.Entry<CharSequence, CharSequence>> itr = headers.iterator();
+ assertTrue(itr.hasNext());
+ itr.next().setValue("foo");
+ }
+
+ private ReadOnlyHttp2Headers newServerHeaders() {
+ return ReadOnlyHttp2Headers.serverHeaders(false, new AsciiString("200"), otherHeaders());
+ }
+
+ private ReadOnlyHttp2Headers newClientHeaders() {
+ return ReadOnlyHttp2Headers.clientHeaders(false, new AsciiString("meth"), new AsciiString("/foo"),
+ new AsciiString("schemer"), new AsciiString("respect_my_authority"), otherHeaders());
+ }
+
+ private ReadOnlyHttp2Headers newTrailers() {
+ return ReadOnlyHttp2Headers.trailers(false, otherHeaders());
+ }
+
+ private AsciiString[] otherHeaders() {
+ return new AsciiString[] {
+ new AsciiString("name1"), new AsciiString("value1"),
+ new AsciiString("name2"), new AsciiString("value2"),
+ new AsciiString("name3"), new AsciiString("value3")
+ };
+ }
+}
@@ -974,7 +974,7 @@ public boolean hasNext() {
@Override
public void remove() {
- throw new UnsupportedOperationException("read-only iterator");
+ throw new UnsupportedOperationException("read only");
}
}
@@ -16,6 +16,8 @@
package io.netty.util.internal;
+import io.netty.util.AsciiString;
+
import java.nio.ByteBuffer;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
@@ -27,6 +29,7 @@
public static final Object[] EMPTY_OBJECTS = {};
public static final Class<?>[] EMPTY_CLASSES = {};
public static final String[] EMPTY_STRINGS = {};
+ public static final AsciiString[] EMPTY_ASCII_STRINGS = {};
public static final StackTraceElement[] EMPTY_STACK_TRACE = {};
public static final ByteBuffer[] EMPTY_BYTE_BUFFERS = {};
public static final Certificate[] EMPTY_CERTIFICATES = {};
@@ -54,7 +54,7 @@ private static String toHttpName(String name) {
return (name.startsWith(":")) ? name.substring(1) : name;
}
- private static String toHttp2Name(String name) {
+ static String toHttp2Name(String name) {
name = name.toLowerCase();
return (name.equals("host")) ? "xhost" : name;
}
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2016 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:
+ *
+ * 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 io.netty.microbench.headers;
+
+import io.netty.handler.codec.http.HttpMethod;
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.netty.handler.codec.http.HttpScheme;
+import io.netty.handler.codec.http2.DefaultHttp2Headers;
+import io.netty.handler.codec.http2.Http2Headers;
+import io.netty.handler.codec.http2.ReadOnlyHttp2Headers;
+import io.netty.microbench.util.AbstractMicrobenchmark;
+import io.netty.util.AsciiString;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Threads;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+@Threads(1)
+@State(Scope.Benchmark)
+@Fork(2)
+@Warmup(iterations = 10)
+@Measurement(iterations = 10)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+public class ReadOnlyHttp2HeadersBenchmark extends AbstractMicrobenchmark {
+ private AsciiString[] headerNames;
+ private AsciiString[] headerValues;
+
+ @Param({ "1", "5", "10", "20" })
+ public int headerCount;
+
+ private final AsciiString path = new AsciiString("/BigDynamicPayload");
+ private final AsciiString authority = new AsciiString("io.netty");
+
+ @Setup
+ public void setUp() throws Exception {
+ headerNames = new AsciiString[headerCount];
+ headerValues = new AsciiString[headerCount];
+ for (int i = 0; i < headerCount; ++i) {
+ headerNames[i] = new AsciiString("key-" + i);
+ headerValues[i] = new AsciiString(UUID.randomUUID().toString());
+ }
+ }
+
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ public int defaultTrailers() {
+ Http2Headers headers = new DefaultHttp2Headers(false);
+ for (int i = 0; i < headerCount; ++i) {
+ headers.add(headerNames[i], headerValues[i]);
+ }
+ return iterate(headers);
+ }
+
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ public int readOnlyTrailers() {
+ return iterate(ReadOnlyHttp2Headers.trailers(false, buildPairs()));
+ }
+
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ public int defaultClientHeaders() {
+ Http2Headers headers = new DefaultHttp2Headers(false);
+ for (int i = 0; i < headerCount; ++i) {
+ headers.add(headerNames[i], headerValues[i]);
+ }
+ headers.method(HttpMethod.POST.asciiName());
+ headers.scheme(HttpScheme.HTTPS.name());
+ headers.path(path);
+ headers.authority(authority);
+ return iterate(headers);
+ }
+
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ public int readOnlyClientHeaders() {
+ return iterate(ReadOnlyHttp2Headers.clientHeaders(false, HttpMethod.POST.asciiName(), path,
+ HttpScheme.HTTPS.name(), authority, buildPairs()));
+ }
+
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ public int defaultServerHeaders() {
+ Http2Headers headers = new DefaultHttp2Headers(false);
+ for (int i = 0; i < headerCount; ++i) {
+ headers.add(headerNames[i], headerValues[i]);
+ }
+ headers.status(HttpResponseStatus.OK.codeAsText());
+ return iterate(headers);
+ }
+
+ @Benchmark
+ @BenchmarkMode(Mode.AverageTime)
+ public int readOnlyServerHeaders() {
+ return iterate(ReadOnlyHttp2Headers.serverHeaders(false, HttpResponseStatus.OK.codeAsText(), buildPairs()));
+ }
+
+ private static int iterate(Http2Headers headers) {
+ int length = 0;
+ for (Map.Entry<CharSequence, CharSequence> entry : headers) {
+ length += entry.getKey().length() + entry.getValue().length();
+ }
+ return length;
+ }
+
+ private AsciiString[] buildPairs() {
+ AsciiString[] headerPairs = new AsciiString[headerCount * 2];
+ for (int i = 0, j = 0; i < headerCount; ++i, ++j) {
+ headerPairs[j] = headerNames[i];
+ headerPairs[++j] = headerValues[i];
+ }
+ return headerPairs;
+ }
+}

0 comments on commit 06e7627

Please sign in to comment.