com.sun.net.httpserver.Headers issues & optimizations
robert engels
rengels at ix.netcom.com
Mon Jan 6 18:35:26 UTC 2025
Hi,
Although, having the JDK specify an API for an http server has been awesome, but I think an unfortunate design decision was to make Headers a concrete class instead of an interface. I don’t think this can be easily changed now without serious backwards compatibility issues.
With a high performance http server, often the most expensive element is the encoding/decoding/processing of the “headers” when accessing cached resources, etc. Garbage generation can be a real problem for low-latency high volume servers. Since the headers survive for the duration of the request, the are subject to not being quickly cleaned in the new generation.
A couple of proposals:
1. The first is to change the implementation of ’normalize’ in Headers. Currently, the cost is paid on every put and every get - even if the developer is aware of the ’normalized’ format and uses that, e.g. getFirst(“Content-length”)
By changing the code to:
public static String normalizeOption8(String key) {
int len = key.length();
if (len == 0) {
return key;
}
int i=0;
char c = key.charAt(0);
if (c == '\r' || c == '\n')
throw new IllegalArgumentException("illegal character in key");
if(c>='a' && c<='z') {
// start with lowercase
} else {
i++;
for(;i<len;i++) {
c = key.charAt(i);
if (c == '\r' || c == '\n')
throw new IllegalArgumentException("illegal character in key");
else if(c >= 'A' && c<='Z') {
break;
}
}
}
if(i==len) return key;
char[] b = key.toCharArray();
if(i==0) {
b[0] = (char)(b[0] - ('a' - 'A'));
i++;
}
for (;i<len; i++) {
c = b[i];
if(c>='A' && c<='Z') {
b[i] = (char) (c+('a'-'A'));
} else if (c == '\r' || c == '\n')
throw new IllegalArgumentException("illegal character in key");
}
return new String(b);
}
You can avoid the overhead in the common case (which is creating a new char[] twice - once to access the characters and a second time when the String is created).
The jmh performance tests:
Benchmark (testString) Mode Cnt Score Error Units
NormalizerJMH.benchmarkNormalizeJDK CONTENT_LENGTH avgt 3 0.021 ± 0.002 us/op
NormalizerJMH.benchmarkNormalizeJDK:gc.alloc.rate CONTENT_LENGTH avgt 3 4724.968 ± 380.497 MB/sec
NormalizerJMH.benchmarkNormalizeJDK:gc.alloc.rate.norm CONTENT_LENGTH avgt 3 104.000 ± 0.001 B/op
NormalizerJMH.benchmarkNormalizeJDK:gc.count CONTENT_LENGTH avgt 3 118.000 counts
NormalizerJMH.benchmarkNormalizeJDK:gc.time CONTENT_LENGTH avgt 3 109.000 ms
NormalizerJMH.benchmarkNormalizeJDK Content-length avgt 3 0.022 ± 0.012 us/op
NormalizerJMH.benchmarkNormalizeJDK:gc.alloc.rate Content-length avgt 3 4562.486 ± 2509.142 MB/sec
NormalizerJMH.benchmarkNormalizeJDK:gc.alloc.rate.norm Content-length avgt 3 104.000 ± 0.001 B/op
NormalizerJMH.benchmarkNormalizeJDK:gc.count Content-length avgt 3 135.000 counts
NormalizerJMH.benchmarkNormalizeJDK:gc.time Content-length avgt 3 122.000 ms
NormalizerJMH.benchmarkNormalizeJDK Content-Length avgt 3 0.022 ± 0.001 us/op
NormalizerJMH.benchmarkNormalizeJDK:gc.alloc.rate Content-Length avgt 3 4482.320 ± 279.049 MB/sec
NormalizerJMH.benchmarkNormalizeJDK:gc.alloc.rate.norm Content-Length avgt 3 104.000 ± 0.001 B/op
NormalizerJMH.benchmarkNormalizeJDK:gc.count Content-Length avgt 3 149.000 counts
NormalizerJMH.benchmarkNormalizeJDK:gc.time Content-Length avgt 3 118.000 ms
NormalizerJMH.benchmarkNormalizeOption8 CONTENT_LENGTH avgt 5 0.022 ± 0.005 us/op
NormalizerJMH.benchmarkNormalizeOption8:gc.alloc.rate CONTENT_LENGTH avgt 5 4453.114 ± 984.009 MB/sec
NormalizerJMH.benchmarkNormalizeOption8:gc.alloc.rate.norm CONTENT_LENGTH avgt 5 104.000 ± 0.001 B/op
NormalizerJMH.benchmarkNormalizeOption8:gc.count CONTENT_LENGTH avgt 5 214.000 counts
NormalizerJMH.benchmarkNormalizeOption8:gc.time CONTENT_LENGTH avgt 5 188.000 ms
NormalizerJMH.benchmarkNormalizeOption8 Content-length avgt 5 0.010 ± 0.001 us/op
NormalizerJMH.benchmarkNormalizeOption8:gc.alloc.rate Content-length avgt 5 0.001 ± 0.001 MB/sec
NormalizerJMH.benchmarkNormalizeOption8:gc.alloc.rate.norm Content-length avgt 5 ≈ 10⁻⁵ B/op
NormalizerJMH.benchmarkNormalizeOption8:gc.count Content-length avgt 5 ≈ 0 counts
NormalizerJMH.benchmarkNormalizeOption8 Content-Length avgt 5 0.025 ± 0.002 us/op
NormalizerJMH.benchmarkNormalizeOption8:gc.alloc.rate Content-Length avgt 5 3927.226 ± 241.881 MB/sec
NormalizerJMH.benchmarkNormalizeOption8:gc.alloc.rate.norm Content-Length avgt 5 104.000 ± 0.001 B/op
NormalizerJMH.benchmarkNormalizeOption8:gc.count Content-Length avgt 5 190.000 counts
NormalizerJMH.benchmarkNormalizeOption8:gc.time Content-Length avgt 5 159.000 ms
You can review the project and the various options attempted here https://github.com/robaho/normalize_test/tree/main
2. Another proposed change to Headers would be to add a protected constructor that allowed you to pass in the Map implementation (or null), allowing specialized map data structures that are better suited to http headers storage, especially when being converted to/from http2 headers.
Although it is possible to subclass Headers to provide your own map, you still pay a penalty because the base class will instantiate a map, and the code is quite ugly as you must override every method in the Headers class. You can see a sample implementation here https://github.com/robaho/httpserver/blob/main/src/main/java/robaho/net/httpserver/OptimizedHeaders.java
The subclass does offer the ability to bypass the normalization completely with a package level method to be used internally by the server when it has already guaranteed the key is normalized.
3. The public static of() methods in Headers should be changed/removed, as they create a dependency on a sun internal class (sun.net.httpserver.UnmodifiableHeaders) from a public “api” class. These could be changed to instead wrap the map with an unmodifiable map.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/net-dev/attachments/20250106/2b21192c/attachment-0001.htm>
More information about the net-dev
mailing list