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