Skip to content

Commit

Permalink
Revert "Revert "Speed-up HTTP 1.1 header and line parsing (netty#12321)…
Browse files Browse the repository at this point in the history
…""

This reverts commit 9993e07.
  • Loading branch information
franz1981 authored and normanmaurer committed Feb 13, 2023
1 parent 9993e07 commit a803e10
Show file tree
Hide file tree
Showing 9 changed files with 713 additions and 220 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ public class HttpMethod implements Comparable<HttpMethod> {
* will be returned. Otherwise, a new instance will be returned.
*/
public static HttpMethod valueOf(String name) {
// fast-path
if (name == HttpMethod.GET.name()) {
return HttpMethod.GET;
}
if (name == HttpMethod.POST.name()) {
return HttpMethod.POST;
}
// "slow"-path
HttpMethod result = methodMap.get(name);
return result != null ? result : new HttpMethod(name);
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelPipeline;
import io.netty.util.AsciiString;

/**
* Decodes {@link ByteBuf}s into {@link HttpRequest}s and {@link HttpContent}s.
Expand Down Expand Up @@ -76,6 +77,34 @@
*/
public class HttpRequestDecoder extends HttpObjectDecoder {

private static final AsciiString Host = AsciiString.cached("Host");
private static final AsciiString Connection = AsciiString.cached("Connection");
private static final AsciiString ContentType = AsciiString.cached("Content-Type");
private static final AsciiString ContentLength = AsciiString.cached("Content-Length");

private static final int GET_AS_INT = 'G' | 'E' << 8 | 'T' << 16;
private static final int POST_AS_INT = 'P' | 'O' << 8 | 'S' << 16 | 'T' << 24;
private static final long HTTP_1_1_AS_LONG = 'H' | 'T' << 8 | 'T' << 16 | 'P' << 24 | (long) '/' << 32 |
(long) '1' << 40 | (long) '.' << 48 | (long) '1' << 56;

private static final long HTTP_1_0_AS_LONG = 'H' | 'T' << 8 | 'T' << 16 | 'P' << 24 | (long) '/' << 32 |
(long) '1' << 40 | (long) '.' << 48 | (long) '0' << 56;

private static final int HOST_AS_INT = 'H' | 'o' << 8 | 's' << 16 | 't' << 24;

private static final long CONNECTION_AS_LONG_0 = 'C' | 'o' << 8 | 'n' << 16 | 'n' << 24 |
(long) 'e' << 32 | (long) 'c' << 40 | (long) 't' << 48 | (long) 'i' << 56;

private static final short CONNECTION_AS_SHORT_1 = 'o' | 'n' << 8;

private static final long CONTENT_AS_LONG = 'C' | 'o' << 8 | 'n' << 16 | 't' << 24 |
(long) 'e' << 32 | (long) 'n' << 40 | (long) 't' << 48 | (long) '-' << 56;

private static final int TYPE_AS_INT = 'T' | 'y' << 8 | 'p' << 16 | 'e' << 24;

private static final long LENGTH_AS_LONG = 'L' | 'e' << 8 | 'n' << 16 | 'g' << 24 |
(long) 't' << 32 | (long) 'h' << 40;

/**
* Creates a new instance with the default
* {@code maxInitialLineLength (4096)}, {@code maxHeaderSize (8192)}, and
Expand Down Expand Up @@ -125,6 +154,144 @@ protected HttpMessage createMessage(String[] initialLine) throws Exception {
HttpMethod.valueOf(initialLine[0]), initialLine[1], validateHeaders);
}

@Override
protected AsciiString splitHeaderName(final byte[] sb, final int start, final int length) {
final byte firstChar = sb[start];
if (firstChar == 'H' && length == 4) {
if (isHost(sb, start)) {
return Host;
}
} else if (firstChar == 'C') {
if (length == 10) {
if (isConnection(sb, start)) {
return Connection;
}
} else if (length == 12) {
if (isContentType(sb, start)) {
return ContentType;
}
} else if (length == 14) {
if (isContentLength(sb, start)) {
return ContentLength;
}
}
}
return super.splitHeaderName(sb, start, length);
}

private static boolean isHost(byte[] sb, int start) {
final int maybeHost = sb[start] |
sb[start + 1] << 8 |
sb[start + 2] << 16 |
sb[start + 3] << 24;
return maybeHost == HOST_AS_INT;
}

private static boolean isConnection(byte[] sb, int start) {
final long maybeConnecti = sb[start] |
sb[start + 1] << 8 |
sb[start + 2] << 16 |
sb[start + 3] << 24 |
(long) sb[start + 4] << 32 |
(long) sb[start + 5] << 40 |
(long) sb[start + 6] << 48 |
(long) sb[start + 7] << 56;
if (maybeConnecti != CONNECTION_AS_LONG_0) {
return false;
}
final short maybeOn = (short) (sb[start + 8] | sb[start + 9] << 8);
return maybeOn == CONNECTION_AS_SHORT_1;
}

private static boolean isContentType(byte[] sb, int start) {
final long maybeContent = sb[start] |
sb[start + 1] << 8 |
sb[start + 2] << 16 |
sb[start + 3] << 24 |
(long) sb[start + 4] << 32 |
(long) sb[start + 5] << 40 |
(long) sb[start + 6] << 48 |
(long) sb[start + 7] << 56;
if (maybeContent != CONTENT_AS_LONG) {
return false;
}
final int maybeType = sb[start + 8] |
sb[start + 9] << 8 |
sb[start + 10] << 16 |
sb[start + 11] << 24;
return maybeType == TYPE_AS_INT;
}

private static boolean isContentLength(byte[] sb, int start) {
final long maybeContent = sb[start] |
sb[start + 1] << 8 |
sb[start + 2] << 16 |
sb[start + 3] << 24 |
(long) sb[start + 4] << 32 |
(long) sb[start + 5] << 40 |
(long) sb[start + 6] << 48 |
(long) sb[start + 7] << 56;
if (maybeContent != CONTENT_AS_LONG) {
return false;
}
final long maybeLength = sb[start + 8] |
sb[start + 9] << 8 |
sb[start + 10] << 16 |
sb[start + 11] << 24 |
(long) sb[start + 12] << 32 |
(long) sb[start + 13] << 40;
return maybeLength == LENGTH_AS_LONG;
}

private static boolean isGetMethod(final byte[] sb, int start) {
final int maybeGet = sb[start] |
sb[start + 1] << 8 |
sb[start + 2] << 16;
return maybeGet == GET_AS_INT;
}

private static boolean isPostMethod(final byte[] sb, int start) {
final int maybePost = sb[start] |
sb[start + 1] << 8 |
sb[start + 2] << 16 |
sb[start + 3] << 24;
return maybePost == POST_AS_INT;
}

@Override
protected String splitFirstWordInitialLine(final byte[] sb, final int start, final int length) {
if (length == 3) {
if (isGetMethod(sb, start)) {
return HttpMethod.GET.name();
}
} else if (length == 4) {
if (isPostMethod(sb, start)) {
return HttpMethod.POST.name();
}
}
return super.splitFirstWordInitialLine(sb, start, length);
}

@Override
protected String splitThirdWordInitialLine(final byte[] sb, final int start, final int length) {
if (length == 8) {
final long maybeHttp1_x = sb[start] |
sb[start + 1] << 8 |
sb[start + 2] << 16 |
sb[start + 3] << 24 |
(long) sb[start + 4] << 32 |
(long) sb[start + 5] << 40 |
(long) sb[start + 6] << 48 |
(long) sb[start + 7] << 56;
if (maybeHttp1_x == HTTP_1_1_AS_LONG) {
return HttpVersion.HTTP_1_1_STRING;
} else if (maybeHttp1_x == HTTP_1_0_AS_LONG) {
return HttpVersion.HTTP_1_0_STRING;
}
}
return super.splitThirdWordInitialLine(sb, start, length);
}

@Override
protected HttpMessage createInvalidMessage() {
return new DefaultFullHttpRequest(HttpVersion.HTTP_1_0, HttpMethod.GET, "/bad-request", validateHeaders);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ public class HttpVersion implements Comparable<HttpVersion> {
private static final Pattern VERSION_PATTERN =
Pattern.compile("(\\S+)/(\\d+)\\.(\\d+)");

private static final String HTTP_1_0_STRING = "HTTP/1.0";
private static final String HTTP_1_1_STRING = "HTTP/1.1";
static final String HTTP_1_0_STRING = "HTTP/1.0";
static final String HTTP_1_1_STRING = "HTTP/1.1";

/**
* HTTP/1.0
Expand All @@ -59,6 +59,13 @@ public class HttpVersion implements Comparable<HttpVersion> {
public static HttpVersion valueOf(String text) {
ObjectUtil.checkNotNull(text, "text");

// super fast-path
if (text == HTTP_1_1_STRING) {
return HTTP_1_1;
} else if (text == HTTP_1_0_STRING) {
return HTTP_1_0;
}

text = text.trim();

if (text.isEmpty()) {
Expand Down
4 changes: 3 additions & 1 deletion common/src/main/java/io/netty/util/AsciiString.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ public AsciiString(byte[] value, boolean copy) {
*/
public AsciiString(byte[] value, int start, int length, boolean copy) {
if (copy) {
this.value = Arrays.copyOfRange(value, start, start + length);
final byte[] rangedCopy = new byte[length];
System.arraycopy(value, start, rangedCopy, 0, rangedCopy.length);
this.value = rangedCopy;
this.offset = 0;
} else {
if (isOutOfBounds(start, length, value.length)) {
Expand Down
14 changes: 13 additions & 1 deletion common/src/main/java/io/netty/util/internal/StringUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,24 @@ public static <T extends Appendable> T toHexString(T dst, byte[] src, int offset
* given, or {@code -1} if the character is invalid.
*/
public static int decodeHexNibble(final char c) {
assert HEX2B.length == (Character.MAX_VALUE + 1);
// Character.digit() is not used here, as it addresses a larger
// set of characters (both ASCII and full-width latin letters).
return HEX2B[c];
}

/**
* Helper to decode half of a hexadecimal number from a string.
* @param b The ASCII character of the hexadecimal number to decode.
* Must be in the range {@code [0-9a-fA-F]}.
* @return The hexadecimal value represented in the ASCII character
* given, or {@code -1} if the character is invalid.
*/
public static int decodeHexNibble(final byte b) {
// Character.digit() is not used here, as it addresses a larger
// set of characters (both ASCII and full-width latin letters).
return HEX2B[b];
}

/**
* Decode a 2-digit hex byte from within a string.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright 2023 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.microbench.http;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.buffer.UnpooledByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.HttpRequestDecoder;
import io.netty.microbench.util.AbstractMicrobenchmark;
import io.netty.util.ReferenceCountUtil;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.CompilerControl;
import org.openjdk.jmh.annotations.CompilerControl.Mode;
import org.openjdk.jmh.annotations.Measurement;
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.TearDown;
import org.openjdk.jmh.annotations.Warmup;

import java.util.ArrayList;
import java.util.Queue;
import java.util.concurrent.TimeUnit;

import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_INITIAL_BUFFER_SIZE;
import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_CHUNK_SIZE;
import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_HEADER_SIZE;
import static io.netty.handler.codec.http.HttpObjectDecoder.DEFAULT_MAX_INITIAL_LINE_LENGTH;
import static io.netty.microbench.http.HttpRequestDecoderUtils.CONTENT_LENGTH;
import static io.netty.microbench.http.HttpRequestDecoderUtils.CONTENT_MIXED_DELIMITERS;

/**
* This benchmark is based on HttpRequestDecoderTest class.
*/
@State(Scope.Benchmark)
@Warmup(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = 10, time = 500, timeUnit = TimeUnit.MILLISECONDS)
public class HttpFragmentedRequestDecoderBenchmark extends AbstractMicrobenchmark {
@Param({ "64", "128" })
public int headerFragmentBytes;

@Param({ "false", "true" })
public boolean direct;

@Param({ "false", "true" })
public boolean pooled;

@Param({ "true", "false"})
public boolean validateHeaders;

private EmbeddedChannel channel;

private ByteBuf[] fragmentedRequest;

private static ByteBuf[] stepsBuffers(ByteBufAllocator alloc, byte[] content, int fragmentSize, boolean direct) {
// allocate a single big buffer and just slice it
final int headerLength = content.length - CONTENT_LENGTH;
final ArrayList<ByteBuf> bufs = new ArrayList<ByteBuf>();
for (int a = 0; a < headerLength;) {
int amount = fragmentSize;
if (a + amount > headerLength) {
amount = headerLength - a;
}
final ByteBuf buf = direct? alloc.directBuffer(amount, amount) : alloc.heapBuffer(amount, amount);
buf.writeBytes(content, a, amount);
bufs.add(buf);
a += amount;
}
// don't split the content
// Should produce HttpContent
final ByteBuf buf = direct?
alloc.directBuffer(CONTENT_LENGTH, CONTENT_LENGTH) :
alloc.heapBuffer(CONTENT_LENGTH, CONTENT_LENGTH);
buf.writeBytes(content, content.length - CONTENT_LENGTH, CONTENT_LENGTH);
bufs.add(buf);
return bufs.toArray(new ByteBuf[0]);
}

@Setup
public void initPipeline() {
final ByteBufAllocator allocator = pooled? PooledByteBufAllocator.DEFAULT : UnpooledByteBufAllocator.DEFAULT;
fragmentedRequest = stepsBuffers(allocator, CONTENT_MIXED_DELIMITERS, headerFragmentBytes, direct);
channel = new EmbeddedChannel(
new HttpRequestDecoder(DEFAULT_MAX_INITIAL_LINE_LENGTH, DEFAULT_MAX_HEADER_SIZE, DEFAULT_MAX_CHUNK_SIZE,
validateHeaders, DEFAULT_INITIAL_BUFFER_SIZE));
}

@TearDown
public void releaseStepBuffers() {
for (ByteBuf buf : fragmentedRequest) {
buf.release();
}
}

@Benchmark
@CompilerControl(Mode.DONT_INLINE)
public void testDecodeWholeRequestInMultipleStepsMixedDelimiters() {
final EmbeddedChannel channel = this.channel;
for (ByteBuf buf : this.fragmentedRequest) {
buf.resetReaderIndex();
buf.retain();
channel.writeInbound(buf);
final Queue<Object> decoded = channel.inboundMessages();
Object o;
while ((o = decoded.poll()) != null) {
ReferenceCountUtil.release(o);
}
}
}
}
Loading

0 comments on commit a803e10

Please sign in to comment.