Skip to content

Commit 8ccbd5f

Browse files
committed
fix http2 compliance when server returns data for testing endpoint
1 parent 2edf0a6 commit 8ccbd5f

File tree

7 files changed

+99
-24
lines changed

7 files changed

+99
-24
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,50 @@ Use `-Drobaho.net.httpserver.http2OverNonSSL=true` to enable Http2 on Non-SSL co
178178
See the additional Http2 options in `ServerConfig.java`
179179

180180
The http2 implementation passes all specification tests in [h2spec](https://github.com/summerwind/h2spec)
181+
182+
## Http2 performance
183+
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.
185+
186+
The Javalin/Jetty project is available [here](https://github.com/robaho/javalin-http2-example)
187+
188+
TODO: outbound headers are only minimally compressed/indexed.
189+
190+
<details>
191+
<summary>performance details</summary>
192+
193+
All tests were run on the same hardware with the same JDK23 version.
194+
195+
Jetty 11
196+
```
197+
starting benchmark...
198+
spawning thread #0: 16 total client(s). 1000000 total requests
199+
Application protocol: h2c
200+
finished in 5.25s, 190298.69 req/s, 6.72MB/s
201+
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
202+
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
203+
traffic: 35.29MB (37003264) total, 7.63MB (8002384) headers (space savings 90.12%), 10.49MB (11000000) data
204+
min max mean sd +/- sd
205+
time for request: 160us 52.24ms 7.76ms 3.94ms 67.73%
206+
time for connect: 235us 8.82ms 4.73ms 2.68ms 62.50%
207+
time to 1st byte: 11.16ms 33.62ms 20.95ms 9.28ms 50.00%
208+
req/s : 11894.25 12051.63 11957.08 58.94 56.25%
209+
```
210+
211+
robaho http2
212+
```
213+
starting benchmark...
214+
spawning thread #0: 16 total client(s). 1000000 total requests
215+
Application protocol: h2c
216+
finished in 2.97s, 336884.32 req/s, 14.14MB/s
217+
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
218+
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
219+
traffic: 41.96MB (44000480) total, 5.72MB (6000000) headers (space savings 76.92%), 10.49MB (11000000) data
220+
min max mean sd +/- sd
221+
time for request: 406us 83.67ms 25.15ms 13.16ms 67.28%
222+
time for connect: 188us 11.70ms 5.99ms 3.69ms 56.25%
223+
time to 1st byte: 14.13ms 31.81ms 22.80ms 6.61ms 43.75%
224+
req/s : 21059.44 21271.63 21141.16 75.01 68.75%
225+
```
226+
227+
</details>

build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ 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.http2MaxConcurrentStreams","5000")
28+
systemProperty("robaho.net.httpserver.http2OverNonSSL","true")
29+
// systemProperty("robaho.net.httpserver.http2MaxConcurrentStreams","5000")
30+
// systemProperty("robaho.net.httpserver.http2DisableFlushDelay","true")
2931
// systemProperty("javax.net.debug","ssl:handshake:verbose:keymanager:trustmanager")
3032
}
3133

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ public class ServerConfig {
8080
private static int http2MaxFrameSize;
8181
private static int http2InitialWindowSize;
8282
private static int http2MaxConcurrentStreams;
83+
private static boolean http2DisableFlushDelay;
8384

8485
static {
8586
java.security.AccessController.doPrivileged(
@@ -143,6 +144,7 @@ public Void run() {
143144
http2InitialWindowSize = Integer.getInteger(pkg + ".http2InitialWindowSize", DEFAULT_HTTP2_INITIAL_WINDOW_SIZE);
144145

145146
http2MaxConcurrentStreams = Integer.getInteger(pkg + ".http2MaxConcurrentStreams", DEFAULT_HTTP2_MAX_CONCURRENT_STREAMS);
147+
http2DisableFlushDelay = Boolean.getBoolean(pkg + ".http2DisableFlushDelay");
146148

147149
return null;
148150
}
@@ -253,4 +255,12 @@ public static int http2InitialWindowSize() {
253255
public static int http2MaxConcurrentStreams() {
254256
return http2MaxConcurrentStreams;
255257
}
258+
/**
259+
* @return true if delaying flush is enabled. disabling the flush delay can improve
260+
* latency at the expense of throughput
261+
*/
262+
public static boolean http2DisableFlushDelay() {
263+
return http2DisableFlushDelay;
264+
}
265+
256266
}

src/main/java/robaho/net/httpserver/http2/HTTP2Connection.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public class HTTP2Connection {
7171

7272
final AtomicLong sendWindow = new AtomicLong(65535);
7373
final AtomicInteger receiveWindow = new AtomicInteger(65535);
74+
final AtomicInteger requestsInProgress = new AtomicInteger();
7475

7576
private int maxConcurrentStreams = -1;
7677
private int highNumberStreams = 0;
@@ -329,12 +330,18 @@ private void processFrames() throws Exception {
329330
}
330331

331332
public void updateRemoteSettings(SettingsFrame remoteSettingFrame) throws HTTP2Exception {
333+
logger.log(Level.TRACE,() -> "updating remote settings");
334+
332335
for (SettingParameter parameter : remoteSettingFrame.getSettingParameters()) {
336+
long oldInitialWindowSize = remoteSettings.getOrDefault(SettingIdentifier.SETTINGS_INITIAL_WINDOW_SIZE, SettingParameter.DEFAULT_INITIAL_WINDOWSIZE).value;
333337
if(parameter.identifier == SettingIdentifier.SETTINGS_INITIAL_WINDOW_SIZE) {
334338
if(parameter.value > 2147483647) {
335339
throw new HTTP2Exception(HTTP2ErrorCode.FLOW_CONTROL_ERROR,"Invalid value for SETTINGS_INITIAL_WINDOW_SIZE "+parameter.value);
336340
}
337341
logger.log(Level.DEBUG,() -> "received initial window size of "+parameter.value);
342+
for(var stream : http2Streams.values()) {
343+
stream.sendWindow.addAndGet(parameter.value-oldInitialWindowSize);
344+
}
338345
}
339346
if(parameter.identifier == SettingIdentifier.SETTINGS_MAX_FRAME_SIZE) {
340347
logger.log(Level.DEBUG,() -> "received max frame size "+parameter.value);
@@ -344,14 +351,15 @@ public void updateRemoteSettings(SettingsFrame remoteSettingFrame) throws HTTP2E
344351
}
345352

346353
public void sendSettingsAck() throws IOException {
354+
logger.log(Level.TRACE,() -> "sending Settings Ack");
347355
lock();
348356
try {
349357
SettingsFrame frame = new SettingsFrame();
350358
frame.writeTo(outputStream);
351359
outputStream.flush();
352360
} finally {
353361
unlock();
354-
logger.log(Level.TRACE,() -> "Sent Settings Ack");
362+
logger.log(Level.TRACE,() -> "sent Settings Ack");
355363
}
356364
}
357365
public void sendMySettings() throws IOException {

src/main/java/robaho/net/httpserver/http2/HTTP2Stream.java

Lines changed: 28 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import com.sun.net.httpserver.Headers;
1818

1919
import robaho.net.httpserver.NoSyncBufferedOutputStream;
20+
import robaho.net.httpserver.ServerConfig;
2021
import robaho.net.httpserver.http2.hpack.HPackContext;
2122
import robaho.net.httpserver.http2.frame.BaseFrame;
2223
import robaho.net.httpserver.http2.frame.DataFrame;
@@ -31,7 +32,8 @@ public class HTTP2Stream {
3132

3233
private final int streamId;
3334

34-
private final AtomicLong sendWindow = new AtomicLong(65535);
35+
// needs to be accessible for connection to adjust based on SettingsFrame
36+
final AtomicLong sendWindow = new AtomicLong(65535);
3537

3638
private final HTTP2Connection connection;
3739
private final Logger logger;
@@ -96,21 +98,11 @@ public void close() {
9698
try {
9799
pipe.close();
98100
outputStream.close();
99-
100-
connection.lock();
101-
try {
102-
// if stream was already closed, then ResetFrame was received, so do not send end of stream
103-
FrameHeader header = new FrameHeader(0, FrameType.DATA, EnumSet.of(FrameFlag.END_STREAM), streamId);
104-
header.writeTo(connection.outputStream);
105-
connection.outputStream.flush();
106-
} finally {
107-
connection.unlock();
108-
}
109101
if(thread!=null)
110102
thread.interrupt();
111103
} catch (IOException e) {
112104
if(!connection.isClosed()) {
113-
logger.log(Level.WARNING,"IOException closing http2 stream",e);
105+
logger.log(connection.httpConnection.requestCount.get()>0 ? Level.WARNING : Level.DEBUG,"IOException closing http2 stream",e);
114106
}
115107
} finally {
116108
}
@@ -175,6 +167,7 @@ public void processFrame(BaseFrame frame) throws HTTP2Exception, IOException {
175167

176168
private void performRequest(boolean halfClosed) throws IOException {
177169
connection.httpConnection.requestCount.incrementAndGet();
170+
connection.requestsInProgress.incrementAndGet();
178171

179172
InputStream in = halfClosed ? InputStream.nullInputStream() : pipe.getInputStream();
180173

@@ -200,7 +193,6 @@ public void writeResponseHeaders() throws IOException {
200193
return;
201194
}
202195
HPackContext.writeHeaderFrame(responseHeaders, connection.outputStream, streamId);
203-
connection.outputStream.flush();
204196
} finally {
205197
headersSent = true;
206198
connection.unlock();
@@ -219,6 +211,7 @@ class Http2OutputStream extends OutputStream {
219211
private final OutputStream outputStream = connection.outputStream;
220212
private final int max_frame_size;
221213
private boolean closed;
214+
private long pauses = 0;
222215

223216
public Http2OutputStream(int streamId) {
224217
this.streamId = streamId;
@@ -240,7 +233,7 @@ public void write(byte[] b) throws IOException {
240233
public void write(byte[] b, int off, int len) throws IOException {
241234
// test outside of lock so other streams can progress
242235
while(sendWindow.get()<=0 && !connection.isClosed()) {
243-
logger.log(Level.TRACE,() -> "sending stream window exhausted, pausing on stream "+streamId);
236+
pauses++;
244237
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
245238
}
246239
connection.lock();
@@ -251,12 +244,17 @@ public void write(byte[] b, int off, int len) throws IOException {
251244
while(len>0) {
252245
int _len = Math.min(Math.min(len,max_frame_size),(int)Math.min(connection.sendWindow.get(),sendWindow.get()));
253246
if(_len<=0) {
254-
logger.log(Level.TRACE,() -> "sending connection window exhausted, pausing on stream "+streamId);
255-
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
256-
if(connection.isClosed()) {
257-
throw new IOException("connection closed");
247+
try {
248+
connection.unlock();
249+
pauses++;
250+
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(16));
251+
if(connection.isClosed()) {
252+
throw new IOException("connection closed");
253+
}
254+
continue;
255+
} finally {
256+
connection.lock();
258257
}
259-
continue;
260258
}
261259
FrameHeader header = new FrameHeader(_len, FrameType.DATA, EnumSet.noneOf(FrameFlag.class), streamId);
262260
logger.log(Level.TRACE,() -> "sending data frame length "+_len+" on stream "+streamId);
@@ -265,7 +263,9 @@ public void write(byte[] b, int off, int len) throws IOException {
265263
off+=_len;
266264
len-=_len;
267265
connection.sendWindow.addAndGet(-_len);
268-
sendWindow.addAndGet(-_len);
266+
if(sendWindow.addAndGet(-_len)<=0) {
267+
outputStream.flush();
268+
}
269269
}
270270
} finally {
271271
connection.unlock();
@@ -283,6 +283,8 @@ public void flush() throws IOException {
283283
@Override
284284
public void close() throws IOException {
285285
if(closed) return;
286+
if(pauses>0)
287+
logger.log(Level.TRACE,() -> "sending stream window exhausted "+pauses+" on stream "+streamId);
286288
connection.lock();
287289
try {
288290
if(connection.isClosed()) {
@@ -294,7 +296,11 @@ public void close() throws IOException {
294296
if (!headersSent) {
295297
writeResponseHeaders();
296298
}
297-
outputStream.flush();
299+
FrameHeader header = new FrameHeader(0, FrameType.DATA, EnumSet.of(FrameFlag.END_STREAM), streamId);
300+
header.writeTo(connection.outputStream);
301+
if(ServerConfig.http2DisableFlushDelay() || connection.requestsInProgress.decrementAndGet()==0) {
302+
connection.outputStream.flush();
303+
}
298304
} finally {
299305
closed=true;
300306
connection.unlock();
@@ -303,6 +309,7 @@ public void close() throws IOException {
303309
}
304310
}
305311

312+
// custom Pipe implementation since JDK version still uses synchronized methods which are not optimal for virtual threads
306313
private static class Pipe {
307314
private final CustomPipedInputStream inputStream;
308315
private final CustomPipedOutputStream outputStream;

src/main/java/robaho/net/httpserver/http2/frame/FrameType.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ public static FrameType getEnum(int value) {
2828
if (e.getValue() == value)
2929
return e;
3030
}
31-
System.out.println("FrameType.getEnum: value not found: " + value);
3231
return FrameType.NOT_IMPLEMENTED;
3332
}
3433
}

src/main/java/robaho/net/httpserver/http2/frame/SettingParameter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ public class SettingParameter {
1313
public SettingIdentifier identifier;
1414
public long value;
1515

16+
public static SettingParameter DEFAULT_INITIAL_WINDOWSIZE = new SettingParameter(SettingIdentifier.SETTINGS_INITIAL_WINDOW_SIZE,65535);
17+
1618
public SettingParameter() {
1719
}
1820

0 commit comments

Comments
 (0)