Skip to content

Commit f6909fb

Browse files
authored
Miscellaneous Core performance improvements (Azure#39552)
Miscellaneous Core performance improvements
1 parent a3d2d26 commit f6909fb

File tree

11 files changed

+149
-33
lines changed

11 files changed

+149
-33
lines changed

sdk/core/azure-core-http-jdk-httpclient/src/main/java/com/azure/core/http/jdk/httpclient/implementation/JdkHttpUtils.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package com.azure.core.http.jdk.httpclient.implementation;
44

55
import com.azure.core.http.HttpHeaders;
6+
import com.azure.core.implementation.util.HttpHeadersAccessHelper;
67
import com.azure.core.util.CoreUtils;
78

89
import java.nio.ByteBuffer;
@@ -25,13 +26,14 @@ public final class JdkHttpUtils {
2526
* @param headers the JDK Http headers
2627
* @return the azure-core Http headers
2728
*/
28-
@SuppressWarnings("deprecation")
2929
public static HttpHeaders fromJdkHttpHeaders(java.net.http.HttpHeaders headers) {
3030
final HttpHeaders httpHeaders = new HttpHeaders((int) (headers.map().size() / 0.75F));
3131

3232
for (Map.Entry<String, List<String>> kvp : headers.map().entrySet()) {
3333
if (!CoreUtils.isNullOrEmpty(kvp.getValue())) {
34-
httpHeaders.set(kvp.getKey(), kvp.getValue());
34+
// JDK HttpClient parses headers to lower case, use the access helper to bypass lowercasing the header
35+
// name (or in this case, just checking that the header name is lowercased).
36+
HttpHeadersAccessHelper.setInternal(httpHeaders, kvp.getKey(), kvp.getKey(), kvp.getValue());
3537
}
3638
}
3739

sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/NettyAsyncHttpResponseBase.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
import com.azure.core.http.HttpHeaders;
88
import com.azure.core.http.HttpRequest;
99
import com.azure.core.http.HttpResponse;
10+
import com.azure.core.implementation.util.HttpHeadersAccessHelper;
11+
import io.netty.util.AsciiString;
1012
import reactor.netty.http.client.HttpClientResponse;
1113

1214
import java.util.Iterator;
1315
import java.util.Map;
16+
import java.util.Objects;
1417

1518
/**
1619
* Base response class for Reactor Netty with implementations for response metadata.
@@ -40,8 +43,19 @@ public abstract class NettyAsyncHttpResponseBase extends HttpResponse {
4043
while (nettyHeadersIterator.hasNext()) {
4144
Map.Entry<CharSequence, CharSequence> next = nettyHeadersIterator.next();
4245
// Value may be null and that needs to be guarded but key should never be null.
43-
CharSequence value = next.getValue();
44-
this.headers.add(next.getKey().toString(), (value == null) ? null : value.toString());
46+
String value = Objects.toString(next.getValue(), null);
47+
CharSequence key = next.getKey();
48+
49+
// Check for the header name being a Netty AsciiString as it has optimizations around lowercasing.
50+
if (key instanceof AsciiString) {
51+
// Hook into optimizations exposed through shared implementation to speed up the conversion.
52+
AsciiString asciiString = (AsciiString) key;
53+
HttpHeadersAccessHelper.addInternal(headers, asciiString.toLowerCase().toString(),
54+
asciiString.toString(), value);
55+
} else {
56+
// If it isn't an AsciiString, then fallback to the shared, albeit, slower path.
57+
this.headers.add(key.toString(), value);
58+
}
4559
}
4660
} else {
4761
this.headers = new NettyToAzureCoreHttpHeadersWrapper(nettyHeaders);

sdk/core/azure-core-http-netty/src/main/java/com/azure/core/http/netty/implementation/Utility.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,9 @@ public final class Utility {
5151
* @return A newly allocated {@link ByteBuffer} containing the copied bytes.
5252
*/
5353
public static ByteBuffer deepCopyBuffer(ByteBuf byteBuf) {
54-
ByteBuffer buffer = ByteBuffer.allocate(byteBuf.readableBytes());
55-
byteBuf.readBytes(buffer);
56-
buffer.rewind();
57-
return buffer;
54+
byte[] bytes = new byte[byteBuf.readableBytes()];
55+
byteBuf.getBytes(byteBuf.readerIndex(), bytes);
56+
return ByteBuffer.wrap(bytes);
5857
}
5958

6059
/**

sdk/core/azure-core/spotbugs-exclude.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,4 +449,10 @@
449449
<Class name="com.azure.core.implementation.http.rest.SwaggerMethodParser" />
450450
<Method name="serialize" />
451451
</Match>
452+
453+
<Match>
454+
<Bug pattern="EI_EXPOSE_STATIC_REP2" />
455+
<Class name="com.azure.core.implementation.util.HttpHeadersAccessHelper" />
456+
<Method name="setAccessor" />
457+
</Match>
452458
</FindBugsFilter>

sdk/core/azure-core/src/main/java/com/azure/core/http/HttpHeaders.java

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,22 @@ public class HttpHeaders implements Iterable<HttpHeader> {
3333
private final Map<String, HttpHeader> headers;
3434

3535
static {
36-
HttpHeadersAccessHelper.setAccessor(headers -> headers.headers);
36+
HttpHeadersAccessHelper.setAccessor(new HttpHeadersAccessHelper.HttpHeadersAccessor() {
37+
@Override
38+
public Map<String, HttpHeader> getRawHeaderMap(HttpHeaders headers) {
39+
return headers.headers;
40+
}
41+
42+
@Override
43+
public void addInternal(HttpHeaders headers, String formattedName, String name, String value) {
44+
headers.addInternal(formattedName, name, value);
45+
}
46+
47+
@Override
48+
public void setInternal(HttpHeaders headers, String formattedName, String name, List<String> values) {
49+
headers.setInternal(formattedName, name, values);
50+
}
51+
});
3752
}
3853

3954
/**

sdk/core/azure-core/src/main/java/com/azure/core/http/rest/RestProxy.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,6 @@ public Mono<HttpResponse> send(HttpRequest request, Context contextData) {
8282

8383
@Override
8484
public Object invoke(Object proxy, final Method method, Object[] args) {
85-
RestProxyUtils.validateResumeOperationIsNotPresent(method);
86-
8785
// Note: request options need to be evaluated here, as it is a public class with package private methods.
8886
// Evaluating here allows the package private methods to be invoked here for downstream use.
8987
final SwaggerMethodParser methodParser = getMethodParser(method);

sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/AsyncRestProxy.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ Mono<HttpResponse> send(HttpRequest request, Context contextData) {
7575
@SuppressWarnings({ "try", "unused" })
7676
public Object invoke(Object proxy, Method method, RequestOptions options, EnumSet<ErrorOptions> errorOptions,
7777
Consumer<HttpRequest> requestCallback, SwaggerMethodParser methodParser, HttpRequest request, Context context) {
78-
RestProxyUtils.validateResumeOperationIsNotPresent(method);
79-
8078
context = startTracingSpan(methodParser, context);
8179

8280
// If there is 'RequestOptions' apply its request callback operations before validating the body.

sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/RestProxyBase.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,6 @@ public RestProxyBase(HttpPipeline httpPipeline, SerializerAdapter serializer,
9898
*/
9999
public final Object invoke(Object proxy, Method method, RequestOptions options, EnumSet<ErrorOptions> errorOptions,
100100
Consumer<HttpRequest> requestCallback, SwaggerMethodParser methodParser, boolean isAsync, Object[] args) {
101-
RestProxyUtils.validateResumeOperationIsNotPresent(method);
102-
103101
try {
104102
HttpRequest request = createHttpRequest(methodParser, serializer, isAsync, args);
105103

sdk/core/azure-core/src/main/java/com/azure/core/implementation/http/rest/SwaggerMethodParser.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ public SwaggerMethodParser(Method swaggerMethod) {
285285
this.responseEagerlyRead = isResponseEagerlyRead(unwrappedReturnType);
286286
this.ignoreResponseBody = isResponseBodyIgnored(unwrappedReturnType);
287287
this.spanName = interfaceParser.getServiceName() + "." + swaggerMethod.getName();
288+
289+
RestProxyUtils.validateResumeOperationIsNotPresent(swaggerMethod);
288290
}
289291

290292
/**

sdk/core/azure-core/src/main/java/com/azure/core/implementation/util/HttpHeadersAccessHelper.java

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import com.azure.core.http.HttpHeader;
66
import com.azure.core.http.HttpHeaders;
77

8+
import java.util.List;
9+
import java.util.Locale;
810
import java.util.Map;
911

1012
/**
@@ -24,6 +26,34 @@ public interface HttpHeadersAccessor {
2426
* @return The raw header map.
2527
*/
2628
Map<String, HttpHeader> getRawHeaderMap(HttpHeaders headers);
29+
30+
/**
31+
* Adds a header value to the backing map in {@link HttpHeaders}.
32+
* <p>
33+
* This bypasses using {@link HttpHeaders#add(String, String)} which uses {@link String#toLowerCase(Locale)},
34+
* which may be slower than options available by implementing HTTP stacks (such as Netty which has an ASCII
35+
* string class which has optimizations around lowercasing due to ASCII constraints).
36+
*
37+
* @param headers The {@link HttpHeaders} to add the header to.
38+
* @param formattedName The lower-cased header name.
39+
* @param name The original header name.
40+
* @param value The header value.
41+
*/
42+
void addInternal(HttpHeaders headers, String formattedName, String name, String value);
43+
44+
/**
45+
* Sets a header value to the backing map in {@link HttpHeaders}.
46+
* <p>
47+
* This bypasses using {@link HttpHeaders#set(String, List)} which uses {@link String#toLowerCase(Locale)},
48+
* which may be slower than options available by implementing HTTP stacks (such as JDK HttpClient where all
49+
* response header names are already lowercased).
50+
*
51+
* @param headers The {@link HttpHeaders} to set the header to.
52+
* @param formattedName The lower-cased header name.
53+
* @param name The original header name.
54+
* @param values The header values.
55+
*/
56+
void setInternal(HttpHeaders headers, String formattedName, String name, List<String> values);
2757
}
2858

2959
/**
@@ -36,6 +66,38 @@ public static Map<String, HttpHeader> getRawHeaderMap(HttpHeaders headers) {
3666
return accessor.getRawHeaderMap(headers);
3767
}
3868

69+
/**
70+
* Adds a header value to the backing map in {@link HttpHeaders}.
71+
* <p>
72+
* This bypasses using {@link HttpHeaders#add(String, String)} which uses {@link String#toLowerCase(Locale)},
73+
* which may be slower than options available by implementing HTTP stacks (such as Netty which has an ASCII
74+
* string class which has optimizations around lowercasing due to ASCII constraints).
75+
*
76+
* @param headers The {@link HttpHeaders} to add the header to.
77+
* @param formattedName The lower-cased header name.
78+
* @param name The original header name.
79+
* @param value The header value.
80+
*/
81+
public static void addInternal(HttpHeaders headers, String formattedName, String name, String value) {
82+
accessor.addInternal(headers, formattedName, name, value);
83+
}
84+
85+
/**
86+
* Sets a header value to the backing map in {@link HttpHeaders}.
87+
* <p>
88+
* This bypasses using {@link HttpHeaders#set(String, List)} which uses {@link String#toLowerCase(Locale)},
89+
* which may be slower than options available by implementing HTTP stacks (such as JDK HttpClient where all
90+
* response header names are already lowercased).
91+
*
92+
* @param headers The {@link HttpHeaders} to set the header to.
93+
* @param formattedName The lower-cased header name.
94+
* @param name The original header name.
95+
* @param values The header values.
96+
*/
97+
public static void setInternal(HttpHeaders headers, String formattedName, String name, List<String> values) {
98+
accessor.setInternal(headers, formattedName, name, values);
99+
}
100+
39101
/**
40102
* Sets the {@link HttpHeadersAccessor}.
41103
*

sdk/core/azure-core/src/main/java/com/azure/core/util/serializer/SerializerEncoding.java

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,6 @@
99
import com.azure.core.util.logging.ClientLogger;
1010
import com.azure.core.util.logging.LogLevel;
1111

12-
import java.util.Map;
13-
import java.util.TreeMap;
14-
1512
/**
1613
* Supported serialization encoding formats.
1714
*/
@@ -32,20 +29,6 @@ public enum SerializerEncoding {
3229
TEXT;
3330

3431
private static final ClientLogger LOGGER = new ClientLogger(SerializerEncoding.class);
35-
private static final Map<String, SerializerEncoding> SUPPORTED_MIME_TYPES;
36-
37-
static {
38-
// Encodings and suffixes from: https://tools.ietf.org/html/rfc6838
39-
SUPPORTED_MIME_TYPES = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
40-
SUPPORTED_MIME_TYPES.put("text/xml", XML);
41-
SUPPORTED_MIME_TYPES.put("application/xml", XML);
42-
SUPPORTED_MIME_TYPES.put("application/json", JSON);
43-
SUPPORTED_MIME_TYPES.put("text/css", TEXT);
44-
SUPPORTED_MIME_TYPES.put("text/csv", TEXT);
45-
SUPPORTED_MIME_TYPES.put("text/html", TEXT);
46-
SUPPORTED_MIME_TYPES.put("text/javascript", TEXT);
47-
SUPPORTED_MIME_TYPES.put("text/plain", TEXT);
48-
}
4932

5033
/**
5134
* Determines the serializer encoding to use based on the Content-Type header.
@@ -63,7 +46,7 @@ public static SerializerEncoding fromHeaders(HttpHeaders headers) {
6346

6447
int contentTypeEnd = mimeContentType.indexOf(';');
6548
String contentType = (contentTypeEnd == -1) ? mimeContentType : mimeContentType.substring(0, contentTypeEnd);
66-
final SerializerEncoding encoding = SUPPORTED_MIME_TYPES.get(contentType);
49+
SerializerEncoding encoding = checkForKnownEncoding(contentType);
6750
if (encoding != null) {
6851
return encoding;
6952
}
@@ -97,4 +80,43 @@ public static SerializerEncoding fromHeaders(HttpHeaders headers) {
9780

9881
return JSON;
9982
}
83+
84+
/*
85+
* There is a limited set of serialization encodings that are known ahead of time. Instead of using a TreeMap with
86+
* a case-insensitive comparator, use an optimized search specifically for the known encodings.
87+
*/
88+
private static SerializerEncoding checkForKnownEncoding(String contentType) {
89+
int length = contentType.length();
90+
91+
// Check the length of the content type first as it is a quick check.
92+
if (length != 8 && length != 9 && length != 10 && length != 15 && length != 16) {
93+
return null;
94+
}
95+
96+
if ("text/".regionMatches(true, 0, contentType, 0, 5)) {
97+
if (length == 8) {
98+
if ("xml".regionMatches(true, 0, contentType, 5, 3)) {
99+
return XML;
100+
} else if ("csv".regionMatches(true, 0, contentType, 5, 3)) {
101+
return TEXT;
102+
} else if ("css".regionMatches(true, 0, contentType, 5, 3)) {
103+
return TEXT;
104+
}
105+
} else if (length == 9 && "html".regionMatches(true, 0, contentType, 5, 4)) {
106+
return TEXT;
107+
} else if (length == 10 && "plain".regionMatches(true, 0, contentType, 5, 5)) {
108+
return TEXT;
109+
} else if (length == 15 && "javascript".regionMatches(true, 0, contentType, 5, 10)) {
110+
return TEXT;
111+
}
112+
} else if ("application/".regionMatches(true, 0, contentType, 0, 12)) {
113+
if (length == 16 && "json".regionMatches(true, 0, contentType, 12, 4)) {
114+
return JSON;
115+
} else if (length == 15 && "xml".regionMatches(true, 0, contentType, 12, 3)) {
116+
return XML;
117+
}
118+
}
119+
120+
return null;
121+
}
100122
}

0 commit comments

Comments
 (0)