HttpResponseStatus object allocation reduction

Motivation:
Usages of HttpResponseStatus may result in more object allocation then necessary due to not looking for cached objects and the AsciiString parsing method not being used due to CharSequence method being used instead.

Modifications:
- HttpResponseDecoder should attempt to get the HttpResponseStatus from cache instead of allocating a new object
- HttpResponseStatus#parseLine(CharSequence) should check if the type is AsciiString and redirect to the AsciiString parsing method which may not require an additional toString call
- HttpResponseStatus#parseLine(AsciiString) can be optimized and doesn't require and may not require object allocation

Result:
Less allocations when dealing with HttpResponseStatus.
This commit is contained in:
Scott Mitchell 2018-01-24 22:01:52 -08:00 committed by GitHub
parent b769ec0934
commit 4921f62c8a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 187 additions and 96 deletions

View File

@ -37,6 +37,7 @@ public class ByteProcessorTest {
assertEquals(16, buf.forEachByte(14, length - 14, ByteProcessor.FIND_NON_LF)); assertEquals(16, buf.forEachByte(14, length - 14, ByteProcessor.FIND_NON_LF));
assertEquals(19, buf.forEachByte(16, length - 16, ByteProcessor.FIND_NUL)); assertEquals(19, buf.forEachByte(16, length - 16, ByteProcessor.FIND_NUL));
assertEquals(21, buf.forEachByte(19, length - 19, ByteProcessor.FIND_NON_NUL)); assertEquals(21, buf.forEachByte(19, length - 19, ByteProcessor.FIND_NON_NUL));
assertEquals(24, buf.forEachByte(19, length - 19, ByteProcessor.FIND_ASCII_SPACE));
assertEquals(24, buf.forEachByte(21, length - 21, ByteProcessor.FIND_LINEAR_WHITESPACE)); assertEquals(24, buf.forEachByte(21, length - 21, ByteProcessor.FIND_LINEAR_WHITESPACE));
assertEquals(28, buf.forEachByte(24, length - 24, ByteProcessor.FIND_NON_LINEAR_WHITESPACE)); assertEquals(28, buf.forEachByte(24, length - 24, ByteProcessor.FIND_NON_LINEAR_WHITESPACE));
assertEquals(-1, buf.forEachByte(28, length - 28, ByteProcessor.FIND_LINEAR_WHITESPACE)); assertEquals(-1, buf.forEachByte(28, length - 28, ByteProcessor.FIND_LINEAR_WHITESPACE));
@ -51,6 +52,7 @@ public class ByteProcessorTest {
final int length = buf.readableBytes(); final int length = buf.readableBytes();
assertEquals(27, buf.forEachByteDesc(0, length, ByteProcessor.FIND_LINEAR_WHITESPACE)); assertEquals(27, buf.forEachByteDesc(0, length, ByteProcessor.FIND_LINEAR_WHITESPACE));
assertEquals(25, buf.forEachByteDesc(0, length, ByteProcessor.FIND_ASCII_SPACE));
assertEquals(23, buf.forEachByteDesc(0, 28, ByteProcessor.FIND_NON_LINEAR_WHITESPACE)); assertEquals(23, buf.forEachByteDesc(0, 28, ByteProcessor.FIND_NON_LINEAR_WHITESPACE));
assertEquals(20, buf.forEachByteDesc(0, 24, ByteProcessor.FIND_NUL)); assertEquals(20, buf.forEachByteDesc(0, 24, ByteProcessor.FIND_NUL));
assertEquals(18, buf.forEachByteDesc(0, 21, ByteProcessor.FIND_NON_NUL)); assertEquals(18, buf.forEachByteDesc(0, 21, ByteProcessor.FIND_NON_NUL));

View File

@ -116,7 +116,7 @@ public class HttpResponseDecoder extends HttpObjectDecoder {
protected HttpMessage createMessage(String[] initialLine) { protected HttpMessage createMessage(String[] initialLine) {
return new DefaultHttpResponse( return new DefaultHttpResponse(
HttpVersion.valueOf(initialLine[0]), HttpVersion.valueOf(initialLine[0]),
new HttpResponseStatus(Integer.parseInt(initialLine[1]), initialLine[2]), validateHeaders); HttpResponseStatus.valueOf(Integer.parseInt(initialLine[1]), initialLine[2]), validateHeaders);
} }
@Override @Override

View File

@ -15,13 +15,15 @@
*/ */
package io.netty.handler.codec.http; package io.netty.handler.codec.http;
import static io.netty.handler.codec.http.HttpConstants.SP;
import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil; import io.netty.buffer.ByteBufUtil;
import io.netty.util.AsciiString; import io.netty.util.AsciiString;
import io.netty.util.ByteProcessor;
import io.netty.util.CharsetUtil; import io.netty.util.CharsetUtil;
import static io.netty.handler.codec.http.HttpConstants.SP;
import static io.netty.util.ByteProcessor.FIND_ASCII_SPACE;
import static java.lang.Integer.parseInt;
/** /**
* The response code and its description of HTTP or its derived protocols, such as * The response code and its description of HTTP or its derived protocols, such as
* <a href="http://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and * <a href="http://en.wikipedia.org/wiki/Real_Time_Streaming_Protocol">RTSP</a> and
@ -324,10 +326,15 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
/** /**
* Returns the {@link HttpResponseStatus} represented by the specified code. * Returns the {@link HttpResponseStatus} represented by the specified code.
* If the specified code is a standard HTTP getStatus code, a cached instance * If the specified code is a standard HTTP status code, a cached instance
* will be returned. Otherwise, a new instance will be returned. * will be returned. Otherwise, a new instance will be returned.
*/ */
public static HttpResponseStatus valueOf(int code) { public static HttpResponseStatus valueOf(int code) {
HttpResponseStatus status = valueOf0(code);
return status != null ? status : new HttpResponseStatus(code);
}
private static HttpResponseStatus valueOf0(int code) {
switch (code) { switch (code) {
case 100: case 100:
return CONTINUE; return CONTINUE;
@ -442,12 +449,25 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
case 511: case 511:
return NETWORK_AUTHENTICATION_REQUIRED; return NETWORK_AUTHENTICATION_REQUIRED;
} }
return null;
return new HttpResponseStatus(code);
} }
/** /**
* Parses the specified HTTP status line into a {@link HttpResponseStatus}. The expected formats of the line are: * Returns the {@link HttpResponseStatus} represented by the specified {@code code} and {@code reasonPhrase}.
* If the specified code is a standard HTTP status {@code code} and {@code reasonPhrase}, a cached instance
* will be returned. Otherwise, a new instance will be returned.
* @param code The response code value.
* @param reasonPhrase The response code reason phrase.
* @return the {@link HttpResponseStatus} represented by the specified {@code code} and {@code reasonPhrase}.
*/
public static HttpResponseStatus valueOf(int code, String reasonPhrase) {
HttpResponseStatus responseStatus = valueOf0(code);
return responseStatus != null && responseStatus.reasonPhrase().contentEquals(reasonPhrase) ? responseStatus :
new HttpResponseStatus(code, reasonPhrase);
}
/**
* Parses the specified HTTP status line into a {@link HttpResponseStatus}. The expected formats of the line are:
* <ul> * <ul>
* <li>{@code statusCode} (e.g. 200)</li> * <li>{@code statusCode} (e.g. 200)</li>
* <li>{@code statusCode} {@code reasonPhrase} (e.g. 404 Not Found)</li> * <li>{@code statusCode} {@code reasonPhrase} (e.g. 404 Not Found)</li>
@ -456,79 +476,25 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
* @throws IllegalArgumentException if the specified status line is malformed * @throws IllegalArgumentException if the specified status line is malformed
*/ */
public static HttpResponseStatus parseLine(CharSequence line) { public static HttpResponseStatus parseLine(CharSequence line) {
String status = line.toString(); return (line instanceof AsciiString) ? parseLine((AsciiString) line) : parseLine(line.toString());
try {
int space = status.indexOf(' ');
if (space == -1) {
return valueOf(Integer.parseInt(status));
} else {
int code = Integer.parseInt(status.substring(0, space));
String reasonPhrase = status.substring(space + 1);
HttpResponseStatus responseStatus = valueOf(code);
if (responseStatus.reasonPhrase().contentEquals(reasonPhrase)) {
return responseStatus;
} else {
return new HttpResponseStatus(code, reasonPhrase);
}
}
} catch (Exception e) {
throw new IllegalArgumentException("malformed status line: " + status, e);
}
} }
private static final class HttpStatusLineProcessor implements ByteProcessor { /**
private static final byte ASCII_SPACE = (byte) ' '; * Parses the specified HTTP status line into a {@link HttpResponseStatus}. The expected formats of the line are:
private final AsciiString string; * <ul>
private int i; * <li>{@code statusCode} (e.g. 200)</li>
/** * <li>{@code statusCode} {@code reasonPhrase} (e.g. 404 Not Found)</li>
* 0 = New or havn't seen {@link #ASCII_SPACE}. * </ul>
* 1 = Last byte was {@link #ASCII_SPACE}. *
* 2 = Terminal State. Processed the byte after {@link #ASCII_SPACE}, and parsed the status line. * @throws IllegalArgumentException if the specified status line is malformed
* 3 = Terminal State. There was no byte after {@link #ASCII_SPACE} but status has been parsed with what we saw. */
*/ public static HttpResponseStatus parseLine(String line) {
private int state; try {
private HttpResponseStatus status; int space = line.indexOf(' ');
return space == -1 ? valueOf(parseInt(line)) :
public HttpStatusLineProcessor(AsciiString string) { valueOf(parseInt(line.substring(0, space)), line.substring(space + 1));
this.string = string; } catch (Exception e) {
} throw new IllegalArgumentException("malformed status line: " + line, e);
@Override
public boolean process(byte value) {
switch (state) {
case 0:
if (value == ASCII_SPACE) {
state = 1;
}
break;
case 1:
parseStatus(i);
state = 2;
return false;
default:
break;
}
++i;
return true;
}
private void parseStatus(int codeEnd) {
int code = string.parseInt(0, codeEnd);
status = valueOf(code);
if (codeEnd < string.length()) {
String actualReason = string.toString(codeEnd + 1, string.length());
if (!status.reasonPhrase().contentEquals(actualReason)) {
status = new HttpResponseStatus(code, actualReason);
}
}
}
public HttpResponseStatus status() {
if (state <= 1) {
parseStatus(string.length());
state = 3;
}
return status;
} }
} }
@ -543,13 +509,8 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
*/ */
public static HttpResponseStatus parseLine(AsciiString line) { public static HttpResponseStatus parseLine(AsciiString line) {
try { try {
HttpStatusLineProcessor processor = new HttpStatusLineProcessor(line); int space = line.forEachByte(FIND_ASCII_SPACE);
line.forEachByte(processor); return space == -1 ? valueOf(line.parseInt()) : valueOf(line.parseInt(0, space), line.toString(space + 1));
HttpResponseStatus status = processor.status();
if (status == null) {
throw new IllegalArgumentException("unable to get status after parsing input");
}
return status;
} catch (Exception e) { } catch (Exception e) {
throw new IllegalArgumentException("malformed status line: " + line, e); throw new IllegalArgumentException("malformed status line: " + line, e);
} }
@ -598,10 +559,11 @@ public class HttpResponseStatus implements Comparable<HttpResponseStatus> {
} }
this.code = code; this.code = code;
codeAsText = new AsciiString(Integer.toString(code)); String codeString = Integer.toString(code);
codeAsText = new AsciiString(codeString);
this.reasonPhrase = reasonPhrase; this.reasonPhrase = reasonPhrase;
if (bytes) { if (bytes) {
this.bytes = (code + " " + reasonPhrase).getBytes(CharsetUtil.US_ASCII); this.bytes = (codeString + ' ' + reasonPhrase).getBytes(CharsetUtil.US_ASCII);
} else { } else {
this.bytes = null; this.bytes = null;
} }

View File

@ -0,0 +1,91 @@
/*
* Copyright 2018 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.http;
import io.netty.util.AsciiString;
import org.junit.Test;
import static io.netty.handler.codec.http.HttpResponseStatus.parseLine;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertSame;
public class HttpResponseStatusTest {
@Test
public void parseLineStringJustCode() {
assertSame(HttpResponseStatus.OK, parseLine("200"));
}
@Test
public void parseLineStringCodeAndPhrase() {
assertSame(HttpResponseStatus.OK, parseLine("200 OK"));
}
@Test
public void parseLineStringCustomCode() {
HttpResponseStatus customStatus = parseLine("612");
assertEquals(612, customStatus.code());
}
@Test
public void parseLineStringCustomCodeAndPhrase() {
HttpResponseStatus customStatus = parseLine("612 FOO");
assertEquals(612, customStatus.code());
assertEquals("FOO", customStatus.reasonPhrase());
}
@Test(expected = IllegalArgumentException.class)
public void parseLineStringMalformedCode() {
parseLine("200a");
}
@Test(expected = IllegalArgumentException.class)
public void parseLineStringMalformedCodeWithPhrase() {
parseLine("200a foo");
}
@Test
public void parseLineAsciiStringJustCode() {
assertSame(HttpResponseStatus.OK, parseLine(new AsciiString("200")));
}
@Test
public void parseLineAsciiStringCodeAndPhrase() {
assertSame(HttpResponseStatus.OK, parseLine(new AsciiString("200 OK")));
}
@Test
public void parseLineAsciiStringCustomCode() {
HttpResponseStatus customStatus = parseLine(new AsciiString("612"));
assertEquals(612, customStatus.code());
}
@Test
public void parseLineAsciiStringCustomCodeAndPhrase() {
HttpResponseStatus customStatus = parseLine(new AsciiString("612 FOO"));
assertEquals(612, customStatus.code());
assertEquals("FOO", customStatus.reasonPhrase());
}
@Test(expected = IllegalArgumentException.class)
public void parseLineAsciiStringMalformedCode() {
parseLine(new AsciiString("200a"));
}
@Test(expected = IllegalArgumentException.class)
public void parseLineAsciiStringMalformedCodeWithPhrase() {
parseLine(new AsciiString("200a foo"));
}
}

View File

@ -44,6 +44,7 @@ import static io.netty.handler.codec.http.HttpHeaderNames.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE; import static io.netty.handler.codec.http.HttpHeaderNames.COOKIE;
import static io.netty.handler.codec.http.HttpHeaderNames.TE; import static io.netty.handler.codec.http.HttpHeaderNames.TE;
import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS; import static io.netty.handler.codec.http.HttpHeaderValues.TRAILERS;
import static io.netty.handler.codec.http.HttpResponseStatus.parseLine;
import static io.netty.handler.codec.http.HttpScheme.HTTP; import static io.netty.handler.codec.http.HttpScheme.HTTP;
import static io.netty.handler.codec.http.HttpScheme.HTTPS; import static io.netty.handler.codec.http.HttpScheme.HTTPS;
import static io.netty.handler.codec.http.HttpUtil.isAsteriskForm; import static io.netty.handler.codec.http.HttpUtil.isAsteriskForm;
@ -183,7 +184,7 @@ public final class HttpConversionUtil {
public static HttpResponseStatus parseStatus(CharSequence status) throws Http2Exception { public static HttpResponseStatus parseStatus(CharSequence status) throws Http2Exception {
HttpResponseStatus result; HttpResponseStatus result;
try { try {
result = HttpResponseStatus.parseLine(status); result = parseLine(status);
if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) { if (result == HttpResponseStatus.SWITCHING_PROTOCOLS) {
throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 status code '%d'", result.code()); throw connectionError(PROTOCOL_ERROR, "Invalid HTTP/2 status code '%d'", result.code());
} }

View File

@ -14,6 +14,11 @@
*/ */
package io.netty.util; package io.netty.util;
import static io.netty.util.ByteProcessorUtils.CARRIAGE_RETURN;
import static io.netty.util.ByteProcessorUtils.HTAB;
import static io.netty.util.ByteProcessorUtils.LINE_FEED;
import static io.netty.util.ByteProcessorUtils.SPACE;
/** /**
* Provides a mechanism to iterate over a collection of bytes. * Provides a mechanism to iterate over a collection of bytes.
*/ */
@ -63,22 +68,22 @@ public interface ByteProcessor {
/** /**
* Aborts on a {@code CR ('\r')}. * Aborts on a {@code CR ('\r')}.
*/ */
ByteProcessor FIND_CR = new IndexOfProcessor((byte) '\r'); ByteProcessor FIND_CR = new IndexOfProcessor(CARRIAGE_RETURN);
/** /**
* Aborts on a non-{@code CR ('\r')}. * Aborts on a non-{@code CR ('\r')}.
*/ */
ByteProcessor FIND_NON_CR = new IndexNotOfProcessor((byte) '\r'); ByteProcessor FIND_NON_CR = new IndexNotOfProcessor(CARRIAGE_RETURN);
/** /**
* Aborts on a {@code LF ('\n')}. * Aborts on a {@code LF ('\n')}.
*/ */
ByteProcessor FIND_LF = new IndexOfProcessor((byte) '\n'); ByteProcessor FIND_LF = new IndexOfProcessor(LINE_FEED);
/** /**
* Aborts on a non-{@code LF ('\n')}. * Aborts on a non-{@code LF ('\n')}.
*/ */
ByteProcessor FIND_NON_LF = new IndexNotOfProcessor((byte) '\n'); ByteProcessor FIND_NON_LF = new IndexNotOfProcessor(LINE_FEED);
/** /**
* Aborts on a semicolon {@code (';')}. * Aborts on a semicolon {@code (';')}.
@ -90,13 +95,18 @@ public interface ByteProcessor {
*/ */
ByteProcessor FIND_COMMA = new IndexOfProcessor((byte) ','); ByteProcessor FIND_COMMA = new IndexOfProcessor((byte) ',');
/**
* Aborts on a ascii space character ({@code ' '}).
*/
ByteProcessor FIND_ASCII_SPACE = new IndexOfProcessor(SPACE);
/** /**
* Aborts on a {@code CR ('\r')} or a {@code LF ('\n')}. * Aborts on a {@code CR ('\r')} or a {@code LF ('\n')}.
*/ */
ByteProcessor FIND_CRLF = new ByteProcessor() { ByteProcessor FIND_CRLF = new ByteProcessor() {
@Override @Override
public boolean process(byte value) { public boolean process(byte value) {
return value != '\r' && value != '\n'; return value != CARRIAGE_RETURN && value != LINE_FEED;
} }
}; };
@ -106,7 +116,7 @@ public interface ByteProcessor {
ByteProcessor FIND_NON_CRLF = new ByteProcessor() { ByteProcessor FIND_NON_CRLF = new ByteProcessor() {
@Override @Override
public boolean process(byte value) { public boolean process(byte value) {
return value == '\r' || value == '\n'; return value == CARRIAGE_RETURN || value == LINE_FEED;
} }
}; };
@ -116,7 +126,7 @@ public interface ByteProcessor {
ByteProcessor FIND_LINEAR_WHITESPACE = new ByteProcessor() { ByteProcessor FIND_LINEAR_WHITESPACE = new ByteProcessor() {
@Override @Override
public boolean process(byte value) { public boolean process(byte value) {
return value != ' ' && value != '\t'; return value != SPACE && value != HTAB;
} }
}; };
@ -126,7 +136,7 @@ public interface ByteProcessor {
ByteProcessor FIND_NON_LINEAR_WHITESPACE = new ByteProcessor() { ByteProcessor FIND_NON_LINEAR_WHITESPACE = new ByteProcessor() {
@Override @Override
public boolean process(byte value) { public boolean process(byte value) {
return value == ' ' || value == '\t'; return value == SPACE || value == HTAB;
} }
}; };

View File

@ -0,0 +1,25 @@
/*
* Copyright 2018 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.util;
final class ByteProcessorUtils {
static final byte SPACE = (byte) ' ';
static final byte HTAB = (byte) '\t';
static final byte CARRIAGE_RETURN = (byte) '\r';
static final byte LINE_FEED = (byte) '\n';
private ByteProcessorUtils() {
}
}