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.