Skip to content

Commit 01f35e4

Browse files
committed
Squashed commit of the following:
commit b766f4abd51dfa6ccfa0d9505fca88502d18408d Author: robert engels <robaho@users.noreply.github.com> Date: Thu Jan 2 12:59:32 2025 -0600 further optimizations commit 201100fe9f7107e83aa4699bd0c6420be437f38b Author: robert engels <robaho@users.noreply.github.com> Date: Thu Jan 2 12:59:24 2025 -0600 update timings commit f8045e28c2a7dd4d53f97d478ecd949a514cbd75 Author: robert engels <robaho@users.noreply.github.com> Date: Thu Jan 2 12:34:51 2025 -0600 write header and data frames directly to stream commit f25470098225aa9ec30fb5c54590b38e56122afb Author: robert engels <robaho@users.noreply.github.com> Date: Thu Jan 2 10:48:51 2025 -0600 many micro optimizations, improved performance by 20-30 percent commit dda364aeb0ebd19efb14d17761177cf3303fc1b8 Author: robert engels <robaho@users.noreply.github.com> Date: Wed Jan 1 15:39:28 2025 -0600 remove locking commit 17fad8afbae47670ed4c4af09c3c2e4046d035b2 Author: robert engels <robaho@users.noreply.github.com> Date: Tue Dec 31 15:46:36 2024 -0600 flushes are only 3 per second, so something else is the issue commit 3e3d26e82045eb783a7598216bf0b8a5183fd015 Author: robert engels <robaho@users.noreply.github.com> Date: Tue Dec 31 13:38:45 2024 -0600 try enqueuing outbound frames to avoid contention
1 parent f90f49a commit 01f35e4

35 files changed

+998
-356
lines changed

README.md

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -181,20 +181,20 @@ The http2 implementation passes all specification tests in [h2spec](https://gith
181181

182182
## Http2 performance
183183

184-
Http2 performance has not yet been optimized, but an unscientific test shows the http2 implementation to have greater than 50% better throughput than the Javalin/Jetty 11 version.
184+
Http2 performance has not yet been optimized, but an unscientific test shows the http2 implementation to have greater than 2x better throughput than the Javalin/Jetty 11 version.
185185

186-
The Javalin/Jetty project is available [here](https://github.com/robaho/javalin-http2-example)
186+
Still, the http2 version is almost 3x slower than the http1 version. I expect this to be the case with most http2 implementations.
187187

188-
TODO: outbound headers are only minimally compressed/indexed.
188+
The Javalin/Jetty project is available [here](https://github.com/robaho/javalin-http2-example)
189189

190190
<details>
191191
<summary>performance details</summary>
192192

193193
All tests were run on the same hardware with the same JDK23 version.
194194

195-
Using `h2load -n 1000000 -m 1000 -c 16 http://localhost:<port>`
195+
Using `h2load -n 1000000 -m 1000 -c 16 [--h1] http://localhost:<port>`
196196

197-
Jetty 11
197+
Jetty 11 http2
198198
```
199199
starting benchmark...
200200
spawning thread #0: 16 total client(s). 1000000 total requests
@@ -210,20 +210,52 @@ time to 1st byte: 11.16ms 33.62ms 20.95ms 9.28ms 50.00%
210210
req/s : 11894.25 12051.63 11957.08 58.94 56.25%
211211
```
212212

213+
Jetty 11 http1
214+
```
215+
starting benchmark...
216+
spawning thread #0: 16 total client(s). 1000000 total requests
217+
Application protocol: http/1.1
218+
finished in 3.67s, 272138.02 req/s, 35.56MB/s
219+
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
220+
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
221+
traffic: 130.65MB (137000000) total, 86.78MB (91000000) headers (space savings 0.00%), 10.49MB (11000000) data
222+
min max mean sd +/- sd
223+
time for request: 831us 189.78ms 57.30ms 21.98ms 71.20%
224+
time for connect: 152us 4.21ms 2.19ms 1.24ms 62.50%
225+
time to 1st byte: 4.85ms 11.73ms 7.11ms 2.29ms 81.25%
226+
req/s : 17010.42 17843.23 17334.96 260.43 50.00%
227+
```
228+
213229
robaho http2
214230
```
215231
starting benchmark...
216232
spawning thread #0: 16 total client(s). 1000000 total requests
217233
Application protocol: h2c
218-
finished in 2.97s, 336884.32 req/s, 14.14MB/s
234+
finished in 2.20s, 453632.21 req/s, 19.04MB/s
219235
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
220236
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
221237
traffic: 41.96MB (44000480) total, 5.72MB (6000000) headers (space savings 76.92%), 10.49MB (11000000) data
222238
min max mean sd +/- sd
223-
time for request: 406us 83.67ms 25.15ms 13.16ms 67.28%
224-
time for connect: 188us 11.70ms 5.99ms 3.69ms 56.25%
225-
time to 1st byte: 14.13ms 31.81ms 22.80ms 6.61ms 43.75%
226-
req/s : 21059.44 21271.63 21141.16 75.01 68.75%
239+
time for request: 347us 51.17ms 16.98ms 10.52ms 59.21%
240+
time for connect: 228us 8.77ms 4.02ms 2.44ms 62.50%
241+
time to 1st byte: 9.46ms 22.61ms 12.61ms 4.81ms 81.25%
242+
req/s : 28353.29 29288.55 28542.35 229.27 87.50%
243+
```
244+
245+
robaho http1
246+
```
247+
starting benchmark...
248+
spawning thread #0: 16 total client(s). 1000000 total requests
249+
Application protocol: http/1.1
250+
finished in 802.36ms, 1246317.13 req/s, 103.41MB/s
251+
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
252+
status codes: 1001066 2xx, 0 3xx, 0 4xx, 0 5xx
253+
traffic: 82.97MB (87000000) total, 46.73MB (49000000) headers (space savings 0.00%), 10.49MB (11000000) data
254+
min max mean sd +/- sd
255+
time for request: 860us 35.46ms 12.61ms 3.33ms 75.21%
256+
time for connect: 92us 4.06ms 2.06ms 1.21ms 62.50%
257+
time to 1st byte: 4.68ms 18.67ms 10.85ms 4.88ms 50.00%
258+
req/s : 77913.01 80438.10 78458.60 721.68 81.25%
227259
```
228260

229261
</details>

build.gradle

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ tasks.withType(Test) {
2525
jvmArgs += "--add-opens=jdk.httpserver/com.sun.net.httpserver=ALL-UNNAMED"
2626
systemProperty("java.util.logging.config.file","logging.properties")
2727
systemProperty("com.sun.net.httpserver.HttpServerProvider","robaho.net.httpserver.DefaultHttpServerProvider")
28+
systemProperty("robaho.net.httpserver.http2OverSSL","true")
2829
systemProperty("robaho.net.httpserver.http2OverNonSSL","true")
2930
// systemProperty("robaho.net.httpserver.http2MaxConcurrentStreams","5000")
3031
// systemProperty("robaho.net.httpserver.http2DisableFlushDelay","true")
@@ -37,10 +38,6 @@ tasks.withType(JavaExec) {
3738
systemProperty("com.sun.net.httpserver.HttpServerProvider","robaho.net.httpserver.DefaultHttpServerProvider")
3839
}
3940

40-
tasks.withType(JavaExec).configureEach {
41-
javaLauncher.set(javaToolchains.launcherFor(java.toolchain))
42-
}
43-
4441
dependencies {
4542
testImplementation 'org.testng:testng:7.8.0'
4643
}
@@ -122,7 +119,11 @@ task testSingleTest(type: Test) {
122119
}
123120
}
124121

122+
125123
task runSimpleFileServer(type: Test) {
124+
javaLauncher = javaToolchains.launcherFor {
125+
languageVersion = JavaLanguageVersion.of(23)
126+
}
126127
dependsOn testClasses
127128
doLast {
128129
def props = systemProperties

src/main/java/robaho/net/httpserver/FixedLengthInputStream.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class FixedLengthInputStream extends LeftOverInputStream {
3939
FixedLengthInputStream(ExchangeImpl t, InputStream src, long len) {
4040
super(t, src);
4141
if (len < 0) {
42-
throw new IllegalArgumentException("Content-Length: " + len);
42+
throw new IllegalArgumentException("Content-length: " + len);
4343
}
4444
this.remaining = len;
4545
}

src/main/java/robaho/net/httpserver/FixedLengthOutputStream.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ class FixedLengthOutputStream extends FilterOutputStream {
4444
FixedLengthOutputStream(ExchangeImpl t, OutputStream src, long len) {
4545
super(src);
4646
if (len < 0) {
47-
throw new IllegalArgumentException("Content-Length: " + len);
47+
throw new IllegalArgumentException("Content-length: " + len);
4848
}
4949
this.t = t;
5050
this.remaining = len;

src/main/java/robaho/net/httpserver/Http2ExchangeImpl.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void close() {
7979
@Override
8080
public void sendResponseHeaders(int rCode, long responseLength) throws IOException {
8181
if(responseLength>0) {
82-
response.set("Content-Length", Long.toString(responseLength));
82+
response.set("Content-length", Long.toString(responseLength));
8383
} else if(responseLength==0) {
8484
// no chunked encoding so just ignore
8585
} else {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package robaho.net.httpserver;
2+
3+
import java.util.ArrayList;
4+
import java.util.Arrays;
5+
import java.util.Collections;
6+
import java.util.HashSet;
7+
import java.util.List;
8+
import java.util.Map;
9+
import java.util.Set;
10+
import java.util.function.Function;
11+
12+
public class OpenAddressIntMap<T> {
13+
14+
private static class Entry {
15+
int key;
16+
Object value;
17+
18+
Entry(int key, Object value) {
19+
this.key = key;
20+
this.value = value;
21+
}
22+
}
23+
24+
private int capacity;
25+
private int mask;
26+
private Entry[] entries;
27+
private int size;
28+
private int used;
29+
30+
public OpenAddressIntMap(int capacity) {
31+
// round up to next power of 2
32+
capacity--;
33+
capacity |= capacity >> 1;
34+
capacity |= capacity >> 2;
35+
capacity |= capacity >> 4;
36+
capacity |= capacity >> 8;
37+
capacity |= capacity >> 16;
38+
capacity++;
39+
40+
this.capacity = capacity;
41+
this.mask = capacity - 1;
42+
this.entries = new Entry[capacity];
43+
}
44+
45+
public synchronized T put(int key, T value) {
46+
if(used>=capacity/2) {
47+
resize();
48+
}
49+
int index = key & mask;
50+
int start = index;
51+
int sentinel = -1;
52+
Entry entry;
53+
while ((entry = entries[index]) != null) {
54+
if (entry.key==key) {
55+
T oldValue = (T)entry.value;
56+
entry.value = value;
57+
if(value==null) {
58+
size--;
59+
}
60+
return oldValue;
61+
} else if (entry.value==null) {
62+
sentinel = index;
63+
}
64+
index = (index + 1) & mask;
65+
if (index == start) {
66+
resize();
67+
index = key & mask;
68+
start = index;
69+
sentinel = -1;
70+
}
71+
}
72+
if(value==null) {
73+
return null;
74+
}
75+
entries[sentinel==-1 ? index : sentinel] = new Entry(key, value);
76+
size++; used++;
77+
return null;
78+
}
79+
80+
private void resize() {
81+
OpenAddressIntMap newMap = new OpenAddressIntMap(capacity << 1);
82+
for (Entry entry : entries) {
83+
if (entry != null && entry.value != null) {
84+
newMap.put(entry.key, entry.value);
85+
}
86+
}
87+
this.entries = newMap.entries;
88+
this.capacity = newMap.capacity;
89+
this.mask = newMap.mask;
90+
this.size = newMap.size;
91+
this.used = newMap.used;
92+
}
93+
94+
public T get(int key) {
95+
int index = key & mask;
96+
int start = index;
97+
Entry entry;
98+
while ((entry = entries[index]) != null) {
99+
if (entry.key==key) {
100+
return (T)entry.value;
101+
}
102+
index = (index + 1) & mask;
103+
if(index==start) {
104+
break;
105+
}
106+
}
107+
return null;
108+
}
109+
110+
public T getOrDefault(int key, T defaultValue) {
111+
T value = get(key);
112+
return value != null ? value : defaultValue;
113+
}
114+
115+
public int size() {
116+
return size;
117+
}
118+
119+
public void clear() {
120+
Arrays.fill(entries, null);
121+
}
122+
123+
public Iterable<T> values() {
124+
List<T> result = new ArrayList<>();
125+
for (Entry entry : entries) {
126+
if (entry != null && entry.value != null) {
127+
result.add((T)entry.value);
128+
}
129+
}
130+
return Collections.unmodifiableList(result);
131+
}
132+
133+
public <T2> Set<Map.Entry<Integer, T2>> entrySet(Function<T, T2> valueMapper) {
134+
Set<Map.Entry<Integer, T2>> result = new HashSet<>();
135+
for (Entry entry : entries) {
136+
if (entry != null) {
137+
result.add(Map.entry(entry.key, valueMapper != null ? valueMapper.apply((T)entry.value) : (T2) entry.value));
138+
}
139+
}
140+
return Collections.unmodifiableSet(result);
141+
}
142+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package robaho.net.httpserver;
2+
3+
import java.util.Arrays;
4+
import java.util.function.BiConsumer;
5+
6+
public class OpenAddressMap {
7+
8+
private static class Entry {
9+
10+
String key;
11+
Object value;
12+
13+
Entry(String key, Object value) {
14+
this.key = key;
15+
this.value = value;
16+
}
17+
}
18+
19+
private int capacity;
20+
private int mask;
21+
private int size;
22+
private Entry[] entries;
23+
24+
public OpenAddressMap(int capacity) {
25+
// round up to next power of 2
26+
capacity--;
27+
capacity |= capacity >> 1;
28+
capacity |= capacity >> 2;
29+
capacity |= capacity >> 4;
30+
capacity |= capacity >> 8;
31+
capacity |= capacity >> 16;
32+
capacity++;
33+
34+
this.capacity = capacity;
35+
this.mask = capacity - 1;
36+
this.entries = new Entry[capacity];
37+
}
38+
39+
public Object put(String key, Object value) {
40+
int index = key.hashCode() & mask;
41+
int start = index;
42+
int sentinel = -1;
43+
Entry entry;
44+
while ((entry = entries[index]) != null) {
45+
if (entry.key.equals(key)) {
46+
Object oldValue = entry.value;
47+
entry.value = value;
48+
if (value == null) {
49+
size--;
50+
}
51+
return oldValue;
52+
} else if (entry.value == null) {
53+
sentinel = index;
54+
}
55+
index = (index + 1) & mask;
56+
if (index == start) {
57+
resize();
58+
index = key.hashCode() & mask;
59+
start = index;
60+
}
61+
}
62+
entries[sentinel==-1 ? index : sentinel] = new Entry(key, value);
63+
size++;
64+
return null;
65+
}
66+
67+
private void resize() {
68+
OpenAddressMap newMap = new OpenAddressMap(capacity << 1);
69+
for (Entry entry : entries) {
70+
if (entry != null) {
71+
newMap.put(entry.key, entry.value);
72+
}
73+
}
74+
this.entries = newMap.entries;
75+
this.capacity = newMap.capacity;
76+
this.mask = newMap.mask;
77+
}
78+
79+
public Object get(String key) {
80+
int index = key.hashCode() & mask;
81+
int start = index;
82+
Entry entry;
83+
while ((entry = entries[index]) != null) {
84+
if (entry.key.equals(key)) {
85+
return entry.value;
86+
}
87+
index = (index + 1) & mask;
88+
if(index==start) {
89+
break;
90+
}
91+
}
92+
return null;
93+
}
94+
95+
public int size() {
96+
return size;
97+
}
98+
99+
public void clear() {
100+
Arrays.fill(entries, null);
101+
}
102+
103+
public void forEach(BiConsumer<String,Object> action) {
104+
for (Entry entry : entries) {
105+
if (entry != null && entry.value != null) {
106+
action.accept(entry.key,entry.value);
107+
}
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)