View Javadoc
1   /*
2    * Copyright 2019 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  package io.netty5.handler.codec.http;
17  
18  import io.netty5.microbench.util.AbstractMicrobenchmark;
19  import org.openjdk.jmh.annotations.Benchmark;
20  import org.openjdk.jmh.annotations.Measurement;
21  import org.openjdk.jmh.annotations.OutputTimeUnit;
22  import org.openjdk.jmh.annotations.Scope;
23  import org.openjdk.jmh.annotations.State;
24  import org.openjdk.jmh.annotations.Warmup;
25  import org.openjdk.jmh.infra.Blackhole;
26  
27  import java.util.HashMap;
28  import java.util.Map;
29  import java.util.concurrent.TimeUnit;
30  
31  import static io.netty5.handler.codec.http.HttpMethod.CONNECT;
32  import static io.netty5.handler.codec.http.HttpMethod.DELETE;
33  import static io.netty5.handler.codec.http.HttpMethod.GET;
34  import static io.netty5.handler.codec.http.HttpMethod.HEAD;
35  import static io.netty5.handler.codec.http.HttpMethod.OPTIONS;
36  import static io.netty5.handler.codec.http.HttpMethod.PATCH;
37  import static io.netty5.handler.codec.http.HttpMethod.POST;
38  import static io.netty5.handler.codec.http.HttpMethod.PUT;
39  import static io.netty5.handler.codec.http.HttpMethod.TRACE;
40  import static io.netty5.util.internal.MathUtil.findNextPositivePowerOfTwo;
41  
42  @State(Scope.Benchmark)
43  @Warmup(iterations = 5)
44  @Measurement(iterations = 8)
45  @OutputTimeUnit(TimeUnit.MICROSECONDS)
46  public class HttpMethodMapBenchmark extends AbstractMicrobenchmark {
47      private static final Map<String, HttpMethod> OLD_MAP = new HashMap<>();
48      private static final SimpleStringMap<HttpMethod> NEW_MAP;
49      private static final String[] KNOWN_METHODS;
50      private static final String[] MIXED_METHODS;
51      private static final String[] UNKNOWN_METHODS;
52  
53      static {
54          // We intentionally don't use HttpMethod.toString() here to avoid the equals(..) comparison method from being
55          // able to short circuit due to reference equality checks and being biased toward the new approach. This
56          // simulates the behavior of HttpObjectDecoder which will build new String objects during the decode operation.
57          KNOWN_METHODS = new String[] {
58                  "OPTIONS",
59                  "GET",
60                  "HEAD",
61                  "POST",
62                  "PUT",
63                  "PATCH",
64                  "DELETE",
65                  "TRACE",
66                  "CONNECT"
67          };
68          MIXED_METHODS = new String[] {
69                  "OPTIONS",
70                  "FAKEMETHOD",
71                  "GET",
72                  "HEAD",
73                  "POST",
74                  "UBERGET",
75                  "PUT",
76                  "PATCH",
77                  "MYMETHOD",
78                  "DELETE",
79                  "TRACE",
80                  "CONNECT",
81                  "WHATMETHOD"
82          };
83          UNKNOWN_METHODS = new String[] {
84                  "FAKEMETHOD",
85                  "UBERGET",
86                  "MYMETHOD",
87                  "TESTING",
88                  "WHATMETHOD",
89                  "UNKNOWN",
90                  "FOOBAR"
91          };
92          OLD_MAP.put(OPTIONS.toString(), OPTIONS);
93          OLD_MAP.put(GET.toString(), GET);
94          OLD_MAP.put(HEAD.toString(), HEAD);
95          OLD_MAP.put(POST.toString(), POST);
96          OLD_MAP.put(PUT.toString(), PUT);
97          OLD_MAP.put(PATCH.toString(), PATCH);
98          OLD_MAP.put(DELETE.toString(), DELETE);
99          OLD_MAP.put(TRACE.toString(), TRACE);
100         OLD_MAP.put(CONNECT.toString(), CONNECT);
101 
102         NEW_MAP = new SimpleStringMap<>(
103                 new SimpleStringMap.Node<>(OPTIONS.toString(), OPTIONS),
104                 new SimpleStringMap.Node<>(GET.toString(), GET),
105                 new SimpleStringMap.Node<>(HEAD.toString(), HEAD),
106                 new SimpleStringMap.Node<>(POST.toString(), POST),
107                 new SimpleStringMap.Node<>(PUT.toString(), PUT),
108                 new SimpleStringMap.Node<>(PATCH.toString(), PATCH),
109                 new SimpleStringMap.Node<>(DELETE.toString(), DELETE),
110                 new SimpleStringMap.Node<>(TRACE.toString(), TRACE),
111                 new SimpleStringMap.Node<>(CONNECT.toString(), CONNECT));
112     }
113 
114     private static final class SimpleStringMap<T> {
115         private final SimpleStringMap.Node<T>[] values;
116         private final int valuesMask;
117 
118         SimpleStringMap(SimpleStringMap.Node<T>... nodes) {
119             values = (SimpleStringMap.Node<T>[]) new SimpleStringMap.Node[findNextPositivePowerOfTwo(nodes.length)];
120             valuesMask = values.length - 1;
121             for (SimpleStringMap.Node<T> node : nodes) {
122                 int i = hashCode(node.key) & valuesMask;
123                 if (values[i] != null) {
124                     throw new IllegalArgumentException("index " + i + " collision between values: [" +
125                             values[i].key + ", " + node.key + "]");
126                 }
127                 values[i] = node;
128             }
129         }
130 
131         T get(String name) {
132             SimpleStringMap.Node<T> node = values[hashCode(name) & valuesMask];
133             return node == null || !node.key.equals(name) ? null : node.value;
134         }
135 
136         private static int hashCode(String name) {
137             // This hash code needs to produce a unique index for each HttpMethod. If new methods are added this
138             // algorithm will need to be adjusted. The goal is to have each enum name's hash value correlate to a unique
139             // index in the values array.
140             return name.hashCode() >>> 6;
141         }
142 
143         private static final class Node<T> {
144             final String key;
145             final T value;
146 
147             Node(String key, T value) {
148                 this.key = key;
149                 this.value = value;
150             }
151         }
152     }
153 
154     @Benchmark
155     public void oldMapKnownMethods(Blackhole bh) throws Exception {
156         for (int i = 0; i < KNOWN_METHODS.length; ++i) {
157             bh.consume(OLD_MAP.get(KNOWN_METHODS[i]));
158         }
159     }
160 
161     @Benchmark
162     public void newMapKnownMethods(Blackhole bh) throws Exception {
163         for (int i = 0; i < KNOWN_METHODS.length; ++i) {
164             bh.consume(NEW_MAP.get(KNOWN_METHODS[i]));
165         }
166     }
167 
168     @Benchmark
169     public void oldMapMixMethods(Blackhole bh) throws Exception {
170         for (int i = 0; i < MIXED_METHODS.length; ++i) {
171             HttpMethod method = OLD_MAP.get(MIXED_METHODS[i]);
172             if (method != null) {
173                 bh.consume(method);
174             }
175         }
176     }
177 
178     @Benchmark
179     public void newMapMixMethods(Blackhole bh) throws Exception {
180         for (int i = 0; i < MIXED_METHODS.length; ++i) {
181             HttpMethod method = NEW_MAP.get(MIXED_METHODS[i]);
182             if (method != null) {
183                 bh.consume(method);
184             }
185         }
186     }
187 
188     @Benchmark
189     public void oldMapUnknownMethods(Blackhole bh) throws Exception {
190         for (int i = 0; i < UNKNOWN_METHODS.length; ++i) {
191             HttpMethod method = OLD_MAP.get(UNKNOWN_METHODS[i]);
192             if (method != null) {
193                 bh.consume(method);
194             }
195         }
196     }
197 
198     @Benchmark
199     public void newMapUnknownMethods(Blackhole bh) throws Exception {
200         for (int i = 0; i < UNKNOWN_METHODS.length; ++i) {
201             HttpMethod method = NEW_MAP.get(UNKNOWN_METHODS[i]);
202             if (method != null) {
203                 bh.consume(method);
204             }
205         }
206     }
207 }