core: speed up Status code and message parsing

This introduces the idea of a "Trusted" Ascii Marshaller, which is
known to always produce valid ASCII byte arrays.  This saves a
surprising amount of garbage, since String conversion involves
creating a new java.lang.StringCoding, and a sun.nio.cs.US_ASCII.

There are other types that can be converted (notably
Http2ClientStream's :status marshaller, which is particularly
wasteful).

Before:
Benchmark                              Mode     Cnt     Score    Error  Units
StatusBenchmark.codeDecode           sample  641278    88.889 ±  9.673  ns/op
StatusBenchmark.codeEncode           sample  430800    73.014 ±  1.444  ns/op
StatusBenchmark.messageDecodeEscape  sample  433467   441.078 ± 58.373  ns/op
StatusBenchmark.messageDecodePlain   sample  676526   268.620 ±  7.849  ns/op
StatusBenchmark.messageEncodeEscape  sample  547350  1211.243 ± 29.907  ns/op
StatusBenchmark.messageEncodePlain   sample  419318   223.263 ±  9.673  ns/op

After:
Benchmark                              Mode     Cnt    Score    Error  Units
StatusBenchmark.codeDecode           sample  442241   48.310 ±  2.409  ns/op
StatusBenchmark.codeEncode           sample  622026   35.475 ±  0.642  ns/op
StatusBenchmark.messageDecodeEscape  sample  595572  312.407 ± 15.870  ns/op
StatusBenchmark.messageDecodePlain   sample  565581   99.090 ±  8.799  ns/op
StatusBenchmark.messageEncodeEscape  sample  479147  201.422 ± 10.765  ns/op
StatusBenchmark.messageEncodePlain   sample  560957   94.722 ±  1.187  ns/op

Also fixes #2237

Before:
Result "unaryCall1024":
  mean = 155710.268 ±(99.9%) 149.278 ns/op

  Percentiles, ns/op:
      p(0.0000) =  63552.000 ns/op
     p(50.0000) = 151552.000 ns/op
     p(90.0000) = 188672.000 ns/op
     p(95.0000) = 207360.000 ns/op
     p(99.0000) = 260608.000 ns/op
     p(99.9000) = 358912.000 ns/op
     p(99.9900) = 1851425.792 ns/op
     p(99.9990) = 11161178.767 ns/op
     p(99.9999) = 14985005.383 ns/op
    p(100.0000) = 17235968.000 ns/op

Benchmark                         (direct)  (transport)    Mode      Cnt       Score     Error  Units
TransportBenchmark.unaryCall1024      true        NETTY  sample  3205966  155710.268 ± 149.278  ns/op

After:
Result "unaryCall1024":
  mean = 147474.794 ±(99.9%) 128.733 ns/op

  Percentiles, ns/op:
      p(0.0000) =  59520.000 ns/op
     p(50.0000) = 144640.000 ns/op
     p(90.0000) = 176128.000 ns/op
     p(95.0000) = 190464.000 ns/op
     p(99.0000) = 236544.000 ns/op
     p(99.9000) = 314880.000 ns/op
     p(99.9900) = 1113084.723 ns/op
     p(99.9990) = 10783126.979 ns/op
     p(99.9999) = 13887153.242 ns/op
    p(100.0000) = 15253504.000 ns/op

Benchmark                         (direct)  (transport)    Mode      Cnt       Score     Error  Units
TransportBenchmark.unaryCall1024      true        NETTY  sample  3385015  147474.794 ± 128.733  ns/op
This commit is contained in:
Carl Mastrangelo 2016-09-07 17:13:40 -07:00
parent af2434375a
commit 1623063143
5 changed files with 371 additions and 59 deletions

View File

@ -0,0 +1,110 @@
/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;
/** StatusBenchmark. */
@State(Scope.Benchmark)
public class StatusBenchmark {
/**
* Javadoc comment.
*/
@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public byte[] messageEncodePlain() {
return Status.MESSAGE_KEY.toBytes("Unexpected RST in stream");
}
/**
* Javadoc comment.
*/
@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public byte[] messageEncodeEscape() {
return Status.MESSAGE_KEY.toBytes("Some Error\nWasabi and Horseradish are the same");
}
/**
* Javadoc comment.
*/
@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public String messageDecodePlain() {
return Status.MESSAGE_KEY.parseBytes(
"Unexpected RST in stream".getBytes(Charset.forName("US-ASCII")));
}
/**
* Javadoc comment.
*/
@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public String messageDecodeEscape() {
return Status.MESSAGE_KEY.parseBytes(
"Some Error%10Wasabi and Horseradish are the same".getBytes(Charset.forName("US-ASCII")));
}
/**
* Javadoc comment.
*/
@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public byte[] codeEncode() {
return Status.CODE_KEY.toBytes(Status.DATA_LOSS);
}
/**
* Javadoc comment.
*/
@Benchmark
@BenchmarkMode(Mode.SampleTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public Status codeDecode() {
return Status.CODE_KEY.parseBytes("15".getBytes(Charset.forName("US-ASCII")));
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright 2016, Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
*
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package io.grpc;
import io.grpc.Metadata.Key;
import java.nio.charset.Charset;
/**
* Internal {@link Metadata} accessor. This is intended for use by io.grpc.internal, and the
* specifically supported transport packages. If you *really* think you need to use this, contact
* the gRPC team first.
*/
@Internal
public final class InternalMetadata {
/**
* A specialized plain ASCII marshaller. Both input and output are assumed to be valid header
* ASCII.
*/
@Internal
public interface TrustedAsciiMarshaller<T> {
/**
* Serialize a metadata value to a ASCII string that contains only the characters listed in the
* class comment of {@link io.grpc.Metadata.AsciiMarshaller}. Otherwise the output may be
* considered invalid and discarded by the transport, or the call may fail.
*
* @param value to serialize
* @return serialized version of value, or null if value cannot be transmitted.
*/
byte[] toAsciiString(T value);
/**
* Parse a serialized metadata value from an ASCII string.
*
* @param serialized value of metadata to parse
* @return a parsed instance of type T
*/
T parseAsciiString(byte[] serialized);
}
/**
* Copy of StandardCharsets, which is only available on Java 1.7 and above.
*/
public static final Charset US_ASCII = Charset.forName("US-ASCII");
@Internal
public static <T> Key<T> keyOf(String name, TrustedAsciiMarshaller<T> marshaller) {
return Metadata.Key.of(name, marshaller);
}
}

View File

@ -38,6 +38,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import io.grpc.InternalMetadata.TrustedAsciiMarshaller;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.BitSet; import java.util.BitSet;
@ -460,6 +462,10 @@ public final class Metadata {
return new AsciiKey<T>(name, marshaller); return new AsciiKey<T>(name, marshaller);
} }
static <T> Key<T> of(String name, TrustedAsciiMarshaller<T> marshaller) {
return new TrustedAsciiKey<T>(name, marshaller);
}
private final String originalName; private final String originalName;
private final String name; private final String name;
@ -603,7 +609,7 @@ public final class Metadata {
super(name); super(name);
Preconditions.checkArgument( Preconditions.checkArgument(
!name.endsWith(BINARY_HEADER_SUFFIX), !name.endsWith(BINARY_HEADER_SUFFIX),
"ASCII header is named %s. It must not end with %s", "ASCII header is named %s. Only binary headers may end with %s",
name, BINARY_HEADER_SUFFIX); name, BINARY_HEADER_SUFFIX);
this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller"); this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller");
} }
@ -619,6 +625,32 @@ public final class Metadata {
} }
} }
private static final class TrustedAsciiKey<T> extends Key<T> {
private final TrustedAsciiMarshaller<T> marshaller;
/**
* Keys have a name and an ASCII marshaller used for serialization.
*/
private TrustedAsciiKey(String name, TrustedAsciiMarshaller<T> marshaller) {
super(name);
Preconditions.checkArgument(
!name.endsWith(BINARY_HEADER_SUFFIX),
"ASCII header is named %s. Only binary headers may end with %s",
name, BINARY_HEADER_SUFFIX);
this.marshaller = Preconditions.checkNotNull(marshaller, "marshaller");
}
@Override
byte[] toBytes(T value) {
return marshaller.toAsciiString(value);
}
@Override
T parseBytes(byte[] serialized) {
return marshaller.parseAsciiString(serialized);
}
}
private static class MetadataEntry { private static class MetadataEntry {
Object parsed; Object parsed;

View File

@ -36,7 +36,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import io.grpc.Metadata.AsciiMarshaller; import io.grpc.InternalMetadata.TrustedAsciiMarshaller;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
@ -215,11 +215,11 @@ public final class Status {
UNAUTHENTICATED(16); UNAUTHENTICATED(16);
private final int value; private final int value;
private final String valueAscii; private final byte[] valueAscii;
private Code(int value) { private Code(int value) {
this.value = value; this.value = value;
this.valueAscii = Integer.toString(value); this.valueAscii = Integer.toString(value).getBytes(US_ASCII);
} }
/** /**
@ -233,11 +233,14 @@ public final class Status {
return STATUS_LIST.get(value); return STATUS_LIST.get(value);
} }
private String valueAscii() { private byte[] valueAscii() {
return valueAscii; return valueAscii;
} }
} }
private static final Charset US_ASCII = Charset.forName("US-ASCII");
private static final Charset UTF_8 = Charset.forName("UTF-8");
// Create the canonical list of Status instances indexed by their code values. // Create the canonical list of Status instances indexed by their code values.
private static final List<Status> STATUS_LIST = buildStatusList(); private static final List<Status> STATUS_LIST = buildStatusList();
@ -314,6 +317,39 @@ public final class Status {
} }
} }
private static Status fromCodeValue(byte[] asciiCodeValue) {
if (asciiCodeValue.length == 1 && asciiCodeValue[0] == '0') {
return Status.OK;
}
return fromCodeValueSlow(asciiCodeValue);
}
@SuppressWarnings("fallthrough")
private static Status fromCodeValueSlow(byte[] asciiCodeValue) {
int index = 0;
int codeValue = 0;
switch (asciiCodeValue.length) {
case 2:
if (asciiCodeValue[index] < '0' || asciiCodeValue[index] > '9') {
break;
}
codeValue += (asciiCodeValue[index++] - '0') * 10;
// fall through
case 1:
if (asciiCodeValue[index] < '0' || asciiCodeValue[index] > '9') {
break;
}
codeValue += asciiCodeValue[index] - '0';
if (codeValue < STATUS_LIST.size()) {
return STATUS_LIST.get(codeValue);
}
break;
default:
break;
}
return UNKNOWN.withDescription("Unknown code " + new String(asciiCodeValue, US_ASCII));
}
/** /**
* Return a {@link Status} given a canonical error {@link Code} object. * Return a {@link Status} given a canonical error {@link Code} object.
*/ */
@ -350,53 +386,15 @@ public final class Status {
* sequence. After the input header bytes are converted into UTF-8 bytes, the new byte array is * sequence. After the input header bytes are converted into UTF-8 bytes, the new byte array is
* reinterpretted back as a string. * reinterpretted back as a string.
*/ */
private static final AsciiMarshaller<String> STATUS_MESSAGE_MARSHALLER = private static final InternalMetadata.TrustedAsciiMarshaller<String> STATUS_MESSAGE_MARSHALLER =
new AsciiMarshaller<String>() { new StatusMessageMarshaller();
@Override
public String toAsciiString(String value) {
// This can be made faster if necessary.
StringBuilder sb = new StringBuilder(value.length());
for (byte b : value.getBytes(Charset.forName("UTF-8"))) {
if (b >= ' ' && b < '%' || b > '%' && b < '~') {
// fast path, if it's plain ascii and not a percent, pass it through.
sb.append((char) b);
} else {
sb.append(String.format("%%%02X", b));
}
}
return sb.toString();
}
@Override
public String parseAsciiString(String value) {
Charset transerEncoding = Charset.forName("US-ASCII");
// This can be made faster if necessary.
byte[] source = value.getBytes(transerEncoding);
ByteBuffer buf = ByteBuffer.allocate(source.length);
for (int i = 0; i < source.length; ) {
if (source[i] == '%' && i + 2 < source.length) {
try {
buf.put((byte)Integer.parseInt(new String(source, i + 1, 2, transerEncoding), 16));
i += 3;
continue;
} catch (NumberFormatException e) {
// ignore, fall through, just push the bytes.
}
}
buf.put(source[i]);
i += 1;
}
return new String(buf.array(), 0, buf.position(), Charset.forName("UTF-8"));
}
};
/** /**
* Key to bind status message to trailing metadata. * Key to bind status message to trailing metadata.
*/ */
@Internal @Internal
public static final Metadata.Key<String> MESSAGE_KEY public static final Metadata.Key<String> MESSAGE_KEY =
= Metadata.Key.of("grpc-message", STATUS_MESSAGE_MARSHALLER); Metadata.Key.of("grpc-message", STATUS_MESSAGE_MARSHALLER);
/** /**
* Extract an error {@link Status} from the causal chain of a {@link Throwable}. * Extract an error {@link Status} from the causal chain of a {@link Throwable}.
@ -571,15 +569,97 @@ public final class Status {
.toString(); .toString();
} }
private static class StatusCodeMarshaller implements Metadata.AsciiMarshaller<Status> { private static final class StatusCodeMarshaller implements TrustedAsciiMarshaller<Status> {
@Override @Override
public String toAsciiString(Status status) { public byte[] toAsciiString(Status status) {
return status.getCode().valueAscii(); return status.getCode().valueAscii();
} }
@Override @Override
public Status parseAsciiString(String serialized) { public Status parseAsciiString(byte[] serialized) {
return fromCodeValue(Integer.valueOf(serialized)); return fromCodeValue(serialized);
}
}
private static final class StatusMessageMarshaller implements TrustedAsciiMarshaller<String> {
private static final byte[] HEX =
{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
@Override
public byte[] toAsciiString(String value) {
byte[] valueBytes = value.getBytes(UTF_8);
for (int i = 0; i < valueBytes.length; i++) {
byte b = valueBytes[i];
// If there are only non escaping characters, skip the slow path.
if (isEscapingChar(b)) {
return toAsciiStringSlow(valueBytes, i);
}
}
return valueBytes;
}
private static boolean isEscapingChar(byte b) {
return b < ' ' || b >= '~' || b == '%';
}
/**
* @param valueBytes the UTF-8 bytes
* @param ri The reader index, pointed at the first byte that needs escaping.
*/
private static byte[] toAsciiStringSlow(byte[] valueBytes, int ri) {
byte[] escapedBytes = new byte[ri + (valueBytes.length - ri) * 3];
// copy over the good bytes
if (ri != 0) {
System.arraycopy(valueBytes, 0, escapedBytes, 0, ri);
}
int wi = ri;
for (; ri < valueBytes.length; ri++) {
byte b = valueBytes[ri];
// Manually implement URL encoding, per the gRPC spec.
if (isEscapingChar(b)) {
escapedBytes[wi] = '%';
escapedBytes[wi + 1] = HEX[(b >> 4) & 0xF];
escapedBytes[wi + 2] = HEX[b & 0xF];
wi += 3;
continue;
}
escapedBytes[wi++] = b;
}
byte[] dest = new byte[wi];
System.arraycopy(escapedBytes, 0, dest, 0, wi);
return dest;
}
@SuppressWarnings("deprecation") // Use fast but deprecated String ctor
@Override
public String parseAsciiString(byte[] value) {
for (int i = 0; i < value.length; i++) {
byte b = value[i];
if (b < ' ' || b >= '~' || b == '%' && i + 2 < value.length) {
return parseAsciiStringSlow(value);
}
}
return new String(value, 0);
}
private static String parseAsciiStringSlow(byte[] value) {
ByteBuffer buf = ByteBuffer.allocate(value.length);
for (int i = 0; i < value.length;) {
if (value[i] == '%' && i + 2 < value.length) {
try {
buf.put((byte)Integer.parseInt(new String(value, i + 1, 2, US_ASCII), 16));
i += 3;
continue;
} catch (NumberFormatException e) {
// ignore, fall through, just push the bytes.
}
}
buf.put(value[i]);
i += 1;
}
return new String(buf.array(), 0, buf.position(), UTF_8);
} }
} }

View File

@ -34,6 +34,7 @@ package io.grpc.internal;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import io.grpc.InternalMetadata;
import io.grpc.Metadata; import io.grpc.Metadata;
import io.grpc.Status; import io.grpc.Status;
@ -49,21 +50,30 @@ public abstract class Http2ClientStream extends AbstractClientStream {
/** /**
* Metadata marshaller for HTTP status lines. * Metadata marshaller for HTTP status lines.
*/ */
private static final Metadata.AsciiMarshaller<Integer> HTTP_STATUS_LINE_MARSHALLER = private static final InternalMetadata.TrustedAsciiMarshaller<Integer> HTTP_STATUS_MARSHALLER =
new Metadata.AsciiMarshaller<Integer>() { new InternalMetadata.TrustedAsciiMarshaller<Integer>() {
@Override @Override
public String toAsciiString(Integer value) { public byte[] toAsciiString(Integer value) {
return value.toString(); throw new UnsupportedOperationException();
} }
/**
* RFC 7231 says status codes are 3 digits long.
*
* @see: <a href="https://tools.ietf.org/html/rfc7231#section-6">RFC 7231</a>
*/
@Override @Override
public Integer parseAsciiString(String serialized) { public Integer parseAsciiString(byte[] serialized) {
return Integer.parseInt(serialized.split(" ", 2)[0]); if (serialized.length >= 3) {
return (serialized[0] - '0') * 100 + (serialized[1] - '0') * 10 + (serialized[2] - '0');
}
throw new NumberFormatException(
"Malformed status code " + new String(serialized, InternalMetadata.US_ASCII));
} }
}; };
private static final Metadata.Key<Integer> HTTP2_STATUS = Metadata.Key.of(":status", private static final Metadata.Key<Integer> HTTP2_STATUS = InternalMetadata.keyOf(":status",
HTTP_STATUS_LINE_MARSHALLER); HTTP_STATUS_MARSHALLER);
/** When non-{@code null}, {@link #transportErrorMetadata} must also be non-{@code null}. */ /** When non-{@code null}, {@link #transportErrorMetadata} must also be non-{@code null}. */
private Status transportError; private Status transportError;