Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ace9cf2
perf: reduce HashMap/collection allocation overhead in gateway path
Apr 1, 2026
10d1b5e
docs: add benchmark comparison charts for hashmap-collection-allocation
Apr 1, 2026
86ee3c0
docs: update benchmark charts with corrected per-interval CPU rendering
Apr 1, 2026
5296c02
Merge branch 'upstream-main' into perf/hashmap-collection-allocation
xinlian12 Apr 5, 2026
d25746b
Address PR review: fix HashMap sizing, deduplicate JSON parsing, remo…
xinlian12 Apr 5, 2026
d834340
Fix HashMap sizing in HttpUtils.asMap() and extract helper method
xinlian12 Apr 5, 2026
ae30351
Address review: document HashMap(32) rationale, apply mapCapacityForS…
xinlian12 Apr 5, 2026
fa0ec32
Address review: extract parseResponsePayload to deduplicate constructors
xinlian12 Apr 6, 2026
afbe7b3
Remove unused HttpUtils.unescape methods and update ResponseUtils
xinlian12 Apr 6, 2026
bf3457b
Merge branch 'upstream-main' into perf/hashmap-collection-allocation
xinlian12 Apr 6, 2026
7f9a136
docs: update benchmark results with V3 analysis - main vs hashmap-alloc
Apr 8, 2026
d6d01d1
docs: clarify JFR allocation weight is cumulative alloc, not heap usage
Apr 8, 2026
2f45e9d
docs: simplify JFR presentation - show % not GB, add GC comparison
Apr 8, 2026
240fa86
docs: rewrite PR description with variance analysis and full JFR comp…
Apr 8, 2026
44bd72a
docs: fix JFR table - explain ValueIterator on request vs response path
Apr 8, 2026
ad7c046
Fix Cosmos Spark BannedDependencies enforcer failure by updating scal…
xinlian12 Apr 14, 2026
7677a27
Merge branch 'fix/cosmos-spark-jackson-version-mismatch' into upstrea…
xinlian12 Apr 14, 2026
d205163
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-java in…
xinlian12 Apr 14, 2026
73308ae
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-java in…
xinlian12 Apr 14, 2026
d6f0130
Merge branch 'upstream-main' into perf/hashmap-collection-allocation
xinlian12 Apr 14, 2026
05ea658
Merge branch 'perf/hashmap-collection-allocation' of https://github.c…
xinlian12 Apr 14, 2026
8743b22
change
xinlian12 Apr 14, 2026
572ddb8
remove test results
xinlian12 Apr 14, 2026
0b15550
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-java in…
xinlian12 Apr 15, 2026
a696f39
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-java in…
xinlian12 Apr 17, 2026
b46387f
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-java in…
xinlian12 Apr 17, 2026
9cb2f83
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-java in…
xinlian12 Apr 20, 2026
925ac82
Merge branch 'upstream-main' into perf/hashmap-collection-allocation
xinlian12 Apr 21, 2026
8ad7f20
Address PR review comments for hashmap-collection-allocation
xinlian12 Apr 21, 2026
c97d58c
Merge branch 'main' of https://github.com/Azure/azure-sdk-for-java in…
xinlian12 Apr 23, 2026
410e559
Merge branch 'upstream-main' into perf/hashmap-collection-allocation
xinlian12 Apr 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,5 @@ public void verifyConversionOfHttpResponseHeadersToMap() {
Entry<String, String> entry = resultHeadersSet.iterator().next();
assertThat(entry.getKey()).isEqualTo(HttpConstants.HttpHeaders.OWNER_FULL_NAME);
assertThat(entry.getValue()).isEqualTo(HttpUtils.urlDecode(OWNER_FULL_NAME_VALUE));

Map<String, String> resultHeaders = HttpUtils.unescape(httpResponseHeaders.toMap());
assertThat(resultHeaders.size()).isEqualTo(1);
entry = resultHeadersSet.iterator().next();
assertThat(entry.getKey()).isEqualTo(HttpConstants.HttpHeaders.OWNER_FULL_NAME);
assertThat(entry.getValue()).isEqualTo(HttpUtils.urlDecode(OWNER_FULL_NAME_VALUE));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

import java.util.HashMap;

import static com.azure.cosmos.implementation.Utils.getUTF8BytesOrNull;
import static org.assertj.core.api.Assertions.assertThat;

public class JsonNodeStorePayloadTests {
@Test(groups = {"unit"})
@Ignore("fallbackCharsetDecoder will only be initialized during the first time when JsonNodeStorePayload loaded," +
Expand Down Expand Up @@ -46,4 +49,47 @@ private static byte[] hexStringToByteArray(String hex) {

return data;
}

@Test(groups = {"unit"})
public void arrayHeaderConstructorParsesValidJson() {
String jsonContent = "{\"id\":\"test\",\"name\":\"value\"}";
String[] headerNames = {"content-type", "x-request-id"};
String[] headerValues = {"application/json", "req-123"};

ByteBuf buffer = getUTF8BytesOrNull(jsonContent);
JsonNodeStorePayload payload = new JsonNodeStorePayload(
new ByteBufInputStream(buffer, true), buffer.readableBytes(), headerNames, headerValues);

assertThat(payload.getPayload()).isNotNull();
assertThat(payload.getPayload().get("id").asText()).isEqualTo("test");
assertThat(payload.getPayload().get("name").asText()).isEqualTo("value");
assertThat(payload.getResponsePayloadSize()).isEqualTo(jsonContent.getBytes().length);
}

@Test(groups = {"unit"})
public void arrayHeaderConstructorWithEmptyStreamReturnsNull() {
String[] headerNames = {"content-type"};
String[] headerValues = {"application/json"};

ByteBuf buffer = Unpooled.EMPTY_BUFFER;
JsonNodeStorePayload payload = new JsonNodeStorePayload(
new ByteBufInputStream(buffer), 0, headerNames, headerValues);

assertThat(payload.getPayload()).isNull();
assertThat(payload.getResponsePayloadSize()).isEqualTo(0);
}

@Test(groups = {"unit"})
public void mapConstructorParsesValidJson() {
String jsonContent = "{\"id\":\"test\"}";
HashMap<String, String> headers = new HashMap<>();
headers.put("content-type", "application/json");

ByteBuf buffer = getUTF8BytesOrNull(jsonContent);
JsonNodeStorePayload payload = new JsonNodeStorePayload(
new ByteBufInputStream(buffer, true), buffer.readableBytes(), headers);

assertThat(payload.getPayload()).isNotNull();
assertThat(payload.getPayload().get("id").asText()).isEqualTo("test");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

package com.azure.cosmos.implementation.directconnectivity;

import com.azure.cosmos.implementation.HttpConstants;
import com.azure.cosmos.implementation.http.HttpHeaders;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufInputStream;
import org.testng.annotations.Test;
Expand Down Expand Up @@ -47,4 +49,84 @@ public void headerNamesAreCaseInsensitive() {
assertThat(sp.getHeaderValue("kEy2")).isEqualTo("value2");
assertThat(sp.getHeaderValue("KEY3")).isEqualTo("value3");
}

@Test(groups = { "unit" })
public void httpHeadersConstructorProducesSameResultAsMapConstructor() {
String jsonContent = "{\"id\":\"test\"}";
HashMap<String, String> headerMap = new HashMap<>();
headerMap.put("key1", "value1");
headerMap.put("Content-Type", "application/json");
headerMap.put("X-Custom-Header", "customValue");

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("key1", "value1");
httpHeaders.set("Content-Type", "application/json");
httpHeaders.set("X-Custom-Header", "customValue");

ByteBuf buffer1 = getUTF8BytesOrNull(jsonContent);
StoreResponse fromMap = new StoreResponse(
"endpoint1", 200, headerMap, new ByteBufInputStream(buffer1, true), buffer1.readableBytes());

ByteBuf buffer2 = getUTF8BytesOrNull(jsonContent);
StoreResponse fromHttpHeaders = new StoreResponse(
"endpoint1", 200, httpHeaders, new ByteBufInputStream(buffer2, true), buffer2.readableBytes());

assertThat(fromHttpHeaders.getStatus()).isEqualTo(fromMap.getStatus());
assertThat(fromHttpHeaders.getEndpoint()).isEqualTo(fromMap.getEndpoint());

// Verify all headers are accessible with case-insensitive lookup
assertThat(fromHttpHeaders.getHeaderValue("key1")).isEqualTo("value1");
assertThat(fromHttpHeaders.getHeaderValue("content-type")).isEqualTo("application/json");
assertThat(fromHttpHeaders.getHeaderValue("x-custom-header")).isEqualTo("customValue");

// HttpHeaders constructor stores lowercase names
String[] headerNames = fromHttpHeaders.getResponseHeaderNames();
for (String name : headerNames) {
assertThat(name).isEqualTo(name.toLowerCase());
}
}

@Test(groups = { "unit" })
public void httpHeadersConstructorWithNullEndpoint() {
String jsonContent = "{\"id\":\"test\"}";
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("key1", "value1");

ByteBuf buffer = getUTF8BytesOrNull(jsonContent);
StoreResponse sp = new StoreResponse(
null, 200, httpHeaders, new ByteBufInputStream(buffer, true), buffer.readableBytes());

assertThat(sp.getEndpoint()).isEqualTo("");
assertThat(sp.getHeaderValue("key1")).isEqualTo("value1");
}

@Test(groups = { "unit" })
public void httpHeadersConstructorWithNoContent() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("key1", "value1");

StoreResponse sp = new StoreResponse("endpoint", 204, httpHeaders, null, 0);

assertThat(sp.getStatus()).isEqualTo(204);
assertThat(sp.getResponseBodyAsJson()).isNull();
assertThat(sp.getHeaderValue("key1")).isEqualTo("value1");
}

@Test(groups = { "unit" })
public void httpHeadersConstructorDecodesOwnerFullName() {
// OWNER_FULL_NAME value with URL-encoded segments (e.g. spaces encoded as %20)
String encodedOwner = "dbs%2FmyDb%2Fcolls%2Fmy%20Collection";
String expectedDecoded = "dbs/myDb/colls/my Collection";

HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(HttpConstants.HttpHeaders.OWNER_FULL_NAME, encodedOwner);
httpHeaders.set("X-Other", "plain");

StoreResponse sp = new StoreResponse("endpoint", 200, httpHeaders, null, 0);

// The encoded OWNER_FULL_NAME should be URL-decoded when accessed via getHeaderValue
assertThat(sp.getHeaderValue(HttpConstants.HttpHeaders.OWNER_FULL_NAME)).isEqualTo(expectedDecoded);
// Other headers are left as-is
assertThat(sp.getHeaderValue("X-Other")).isEqualTo("plain");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@

import org.testng.annotations.Test;

import java.util.Locale;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class HttpHeadersTests {

Expand All @@ -26,4 +28,78 @@ public void caseInsensitiveToMap() {
assertThat(caseSensitiveMap.get(headerName.toLowerCase())).isNull();
assertThat(caseSensitiveMap.get(headerName)).isEqualTo(headerValue);
}

@Test(groups = "unit")
public void populateLowerCaseHeadersProducesLowercaseNames() {
HttpHeaders headers = new HttpHeaders();
headers.set("Content-Type", "application/json");
headers.set("X-Ms-Request-Id", "abc-123");
headers.set("ETag", "\"v1\"");

String[] names = new String[headers.size()];
String[] values = new String[headers.size()];
headers.populateLowerCaseHeaders(names, values);

// All names should be lowercase
for (String name : names) {
assertThat(name).isEqualTo(name.toLowerCase(Locale.ROOT));
}
Comment thread
xinlian12 marked this conversation as resolved.

// Verify values are present (order depends on HashMap iteration, so use containment)
Map<String, String> resultMap = new java.util.HashMap<>();
for (int i = 0; i < names.length; i++) {
resultMap.put(names[i], values[i]);
}

assertThat(resultMap).containsEntry("content-type", "application/json");
assertThat(resultMap).containsEntry("x-ms-request-id", "abc-123");
assertThat(resultMap).containsEntry("etag", "\"v1\"");
}

@Test(groups = "unit")
public void populateLowerCaseHeadersWithEmptyHeaders() {
HttpHeaders headers = new HttpHeaders();

String[] names = new String[0];
String[] values = new String[0];
headers.populateLowerCaseHeaders(names, values);

assertThat(names).isEmpty();
assertThat(values).isEmpty();
}

@Test(groups = "unit")
public void populateLowerCaseHeadersRejectsNullNames() {
HttpHeaders headers = new HttpHeaders();
headers.set("Key", "value");

assertThatThrownBy(() -> headers.populateLowerCaseHeaders(null, new String[1]))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("names");
}

@Test(groups = "unit")
public void populateLowerCaseHeadersRejectsNullValues() {
HttpHeaders headers = new HttpHeaders();
headers.set("Key", "value");

assertThatThrownBy(() -> headers.populateLowerCaseHeaders(new String[1], null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("values");
}

@Test(groups = "unit")
public void populateLowerCaseHeadersRejectsTooSmallArrays() {
HttpHeaders headers = new HttpHeaders();
headers.set("A", "1");
headers.set("B", "2");

assertThatThrownBy(() -> headers.populateLowerCaseHeaders(new String[1], new String[2]))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("names");

assertThatThrownBy(() -> headers.populateLowerCaseHeaders(new String[2], new String[1]))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("values");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,9 @@ private RxDocumentServiceRequest(DiagnosticsClientContext clientContext,
this.forceNameCacheRefresh = false;
this.resourceType = resourceType;
this.contentAsByteArray = toByteArray(byteBuffer);
this.headers = headers != null ? headers : new HashMap<>();
// Pre-size to 32 (threshold 24 at 0.75 load factor) to accommodate typical request
// headers (auth, content-type, consistency, session-token, partition-key, etc.) without resize.
this.headers = headers != null ? headers : new HashMap<>(32);
Comment thread
xinlian12 marked this conversation as resolved.
this.activityId = UUIDs.nonBlockingRandomUUID();
this.isFeed = false;
this.isNameBased = isNameBased;
Expand Down Expand Up @@ -225,7 +227,9 @@ private RxDocumentServiceRequest(DiagnosticsClientContext clientContext,
this.operationType = operationType;
this.resourceType = resourceType;
this.requestContext.sessionToken = null;
this.headers = headers != null ? headers : new HashMap<>();
// Pre-size to 32 (threshold 24 at 0.75 load factor) to accommodate typical request
// headers (auth, content-type, consistency, session-token, partition-key, etc.) without resize.
this.headers = headers != null ? headers : new HashMap<>(32);
Comment thread
xinlian12 marked this conversation as resolved.
this.activityId = UUIDs.nonBlockingRandomUUID();
this.isFeed = false;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ public StoreResponse unwrapToStoreResponse(
return new StoreResponse(
endpoint,
statusCode,
HttpUtils.unescape(headers.toLowerCaseMap()),
headers,
new ByteBufInputStream(retainedContent, true),
size);
} else {
Expand All @@ -252,7 +252,7 @@ public StoreResponse unwrapToStoreResponse(
return new StoreResponse(
endpoint,
statusCode,
HttpUtils.unescape(headers.toLowerCaseMap()),
headers,
null,
0);
}
Expand Down Expand Up @@ -347,7 +347,7 @@ private Mono<RxDocumentServiceResponse> performRequestInternalCore(RxDocumentSer
}

private HttpHeaders getHttpRequestHeaders(Map<String, String> headers) {
HttpHeaders httpHeaders = new HttpHeaders(this.defaultHeaders.size());
HttpHeaders httpHeaders = new HttpHeaders(HttpUtils.mapCapacityForSize(this.defaultHeaders.size() + headers.size()));
// Add default headers.
for (Entry<String, String> entry : this.defaultHeaders.entrySet()) {
if (!headers.containsKey(entry.getKey())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,16 @@
import com.azure.cosmos.implementation.HttpConstants;
import com.azure.cosmos.implementation.Strings;
import com.azure.cosmos.implementation.apachecommons.lang.StringUtils;
import com.azure.cosmos.implementation.http.HttpHeader;
import com.azure.cosmos.implementation.http.HttpHeaders;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Pattern;

public class HttpUtils {
Expand All @@ -29,6 +25,18 @@ public class HttpUtils {

private static final Pattern PLUS_SYMBOL_ESCAPE_PATTERN = Pattern.compile(UrlEncodingInfo.PLUS_SYMBOL_ESCAPED);

/**
* Returns the initial capacity for a HashMap that will hold {@code expectedSize} entries
* without resizing, accounting for the default load factor of 0.75.
*/
public static int mapCapacityForSize(int expectedSize) {
if (expectedSize <= 0) {
return 1;
}
long capacity = (long) expectedSize * 4 / 3 + 1;
return capacity >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) capacity;
}

public static String urlEncode(String url) {
try {
return PLUS_SYMBOL_ESCAPE_PATTERN.matcher(URLEncoder.encode(url, UrlEncodingInfo.UTF_8))
Expand All @@ -51,14 +59,14 @@ public static String urlDecode(String url) {

public static Map<String, String> asMap(HttpHeaders headers) {
if (headers == null) {
return new HashMap<>();
return new HashMap<>(4);
}
HashMap<String, String> map = new HashMap<>(headers.size());
for (Entry<String, String> entry : headers.toMap().entrySet()) {
if (entry.getKey().equals(HttpConstants.HttpHeaders.OWNER_FULL_NAME)) {
map.put(entry.getKey(), HttpUtils.urlDecode(entry.getValue()));
HashMap<String, String> map = new HashMap<>(mapCapacityForSize(headers.size()));
for (HttpHeader header : headers) {
if (header.name().equals(HttpConstants.HttpHeaders.OWNER_FULL_NAME)) {
map.put(header.name(), HttpUtils.urlDecode(header.value()));
} else {
map.put(entry.getKey(), entry.getValue());
map.put(header.name(), header.value());
}
}
return map;
Expand All @@ -78,24 +86,4 @@ public static String getDateHeader(Map<String, String> headerValues) {

return date != null ? date : StringUtils.EMPTY;
}

public static List<Entry<String, String>> unescape(Set<Entry<String, String>> headers) {
List<Entry<String, String>> result = new ArrayList<>(headers.size());
for (Entry<String, String> entry : headers) {
if (entry.getKey().equals(HttpConstants.HttpHeaders.OWNER_FULL_NAME)) {
String unescapedUrl = HttpUtils.urlDecode(entry.getValue());
entry = new AbstractMap.SimpleEntry<>(entry.getKey(), unescapedUrl);
}
result.add(entry);
}
return result;
}

public static Map<String, String> unescape(Map<String, String> headers) {
if (headers != null) {
headers.computeIfPresent(HttpConstants.HttpHeaders.OWNER_FULL_NAME,
(ownerKey, ownerValue) -> HttpUtils.urlDecode(ownerValue));
}
return headers;
}
}
Loading
Loading