Skip to content

Commit 2dc29b4

Browse files
committed
test(h3): replace placeholder assertions and update HTTP specs
1 parent 9eeb54a commit 2dc29b4

5 files changed

Lines changed: 225 additions & 7 deletions

File tree

src/TurboHTTP.AcceptanceTests/H2/RequestFormatSpec.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public async Task Get_request_should_contain_path_pseudo_header()
8181
var decoder = new HpackDecoder();
8282
var headers = decoder.Decode(headersFrame.HeaderBlockFragment.Span);
8383

84-
Assert.Contains(headers, h => h.Name == ":path" && h.Value == "/some/path");
84+
Assert.Contains(headers, h => h is { Name: ":path", Value: "/some/path" });
8585
}
8686

8787
[Fact(Timeout = 5000)]

src/TurboHTTP.StreamTests/Http3/Http30ConnectionStageSpec.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ public async Task Http30ConnectionStage_should_route_to_correct_quic_stream()
5959
appSubscription.SendNext(MakeRequest("/stream2"));
6060

6161
// After TransportConnected: PreStart items (3x OpenStream + preface) are flushed,
62-
// then request encoding emits ConnectTransport + OpenStream + MultiplexedData + CompleteWrites per request.
62+
// then request encoding emits ConnectTransport + OpenStream/MultiplexedData/CompleteWrites per request.
6363
// Verify we get at least the PreStart batch + first request items.
64-
for (var i = 0; i < 8; i++)
64+
for (var i = 0; i < 6; i++)
6565
{
6666
await networkSub.ExpectNextAsync(TestContext.Current.CancellationToken);
6767
}

src/TurboHTTP.Tests/Http3/Connection/Http3ConnectionStateEdgeCasesSpec.cs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -345,11 +345,15 @@ public void RecordPush_should_track_push_count()
345345

346346
for (var i = 0; i < 5; i++)
347347
{
348-
state.RecordPush(); // Should not throw
348+
state.RecordPush();
349+
}
350+
351+
for (var i = 5; i < 10; i++)
352+
{
353+
state.RecordPush();
349354
}
350355

351-
// Internal push count is incremented (no public way to verify, but no exception = success)
352-
Assert.True(true);
356+
Assert.Throws<Http3Exception>(() => state.RecordPush());
353357
}
354358

355359
[Fact(Timeout = 5000)]

src/TurboHTTP.Tests/Http3/Connection/Http3StateMachineSpec.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,6 @@ public void EncodeRequest_should_tag_outbound_frames_with_stream_id()
559559

560560
sm.EncodeRequest(CreateGetRequest());
561561

562-
// All request frames should be tagged as MultiplexedData with stream ID 0
563562
var tagged = _ops.Outbound
564563
.OfType<MultiplexedData>()
565564
.ToList();
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
# TurboHttp vs HttpClient — Localhost Kestrel (Loopback)
2+
3+
| | |
4+
|---|---|
5+
| **Report date** | 2026-05-05 10:28 UTC |
6+
| **Server** | localhost Kestrel (127.0.0.1, dynamic port) |
7+
| **Protocol** | HTTP cleartext — HTTP/1.1 and HTTP/2 (h2c prior knowledge) |
8+
| **Light endpoint** | `GET /benchmark/simple` (~3 B text/plain) |
9+
| **Heavy endpoint** | `POST /benchmark/payload` (10 KB request body) |
10+
11+
> **Legend:**
12+
> - ✓ faster than HttpClient by >5%
13+
> - – within ±5% of HttpClient
14+
> - ✗ slower than HttpClient by >5%
15+
> - **Δ%** is relative to the HttpClient baseline (positive = faster/cheaper)
16+
17+
# HTTP/1.1
18+
19+
## Single Request — Throughput (Req/sec — higher is better)
20+
21+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
22+
|---|---:|---:|---:|---:|---:|
23+
| ConcurrentRequests_Light / CL=1 | 23,356 | 14,432 | -38.2% | 12,111 | -48.1% |
24+
| ConcurrentRequests_Heavy / CL=1 | 16,940 | 13,163 | -22.3% | 10,845 | -36.0% |
25+
26+
## Single Request — Latency (ns — lower is better)
27+
28+
### p50 (Median)
29+
30+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
31+
|---|---:|---:|---:|---:|---:|
32+
| ConcurrentRequests_Light / CL=1 | 42,322 ns | 68,625 ns | -62.1% | 75,980 ns | -79.5% |
33+
| ConcurrentRequests_Heavy / CL=1 | 57,455 ns | 75,834 ns | -32.0% | 93,086 ns | -62.0% |
34+
35+
### p95
36+
37+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
38+
|---|---:|---:|---:|---:|---:|
39+
| ConcurrentRequests_Light / CL=1 | 44,925 ns | 77,564 ns | -72.7% | 107,494 ns | -139.3% |
40+
| ConcurrentRequests_Heavy / CL=1 | 64,406 ns | 77,171 ns | -19.8% | 100,411 ns | -55.9% |
41+
42+
### p99
43+
44+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
45+
|---|---:|---:|---:|---:|---:|
46+
| ConcurrentRequests_Light / CL=1 | 45,356 ns | 78,078 ns | -72.1% | 110,010 ns | -142.5% |
47+
| ConcurrentRequests_Heavy / CL=1 | 65,776 ns | 77,440 ns | -17.7% | 102,475 ns | -55.8% |
48+
49+
## Single Request — Memory (Allocated bytes/op — lower is better)
50+
51+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
52+
|---|---:|---:|---:|---:|---:|
53+
| ConcurrentRequests_Light / CL=1 | 2,704 B | 5,237 B | -93.7% | 5,173 B | -91.3% |
54+
| ConcurrentRequests_Heavy / CL=1 | 41,618 B | 44,224 B | -6.3% | 44,000 B | -5.7% |
55+
56+
---
57+
58+
## Concurrent Benchmarks
59+
60+
> N requests are fired simultaneously (SendAsync: `Task.WhenAll`; Streaming: channel write-all, drain-all).
61+
> **Throughput** = N / Mean (aggregate req/sec across all parallel slots).
62+
> **Latency** = elapsed wall-time until all N complete (lower is better).
63+
64+
### Concurrent Throughput (Req/sec — higher is better)
65+
66+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
67+
|---|---:|---:|---:|---:|---:|
68+
| ConcurrentRequests_Light / CL=512 | 77,858 | 73,642 | -5.4% | 242,330 | +211.2% |
69+
| ConcurrentRequests_Heavy / CL=512 | 39,983 | 70,943 | +77.4% | 67,455 | +68.7% |
70+
| ConcurrentRequests_Light / CL=4096 | 79,490 | 117,319 | +47.6% | 300,845 | +278.5% |
71+
| ConcurrentRequests_Heavy / CL=4096 | 43,055 | 84,915 | +97.2% | 110,153 | +155.8% |
72+
73+
### Concurrent Latency (ns — lower is better)
74+
75+
#### p50 (Median)
76+
77+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
78+
|---|---:|---:|---:|---:|---:|
79+
| ConcurrentRequests_Light / CL=512 | 6,588,207 ns | 5,423,530 ns | +17.7% | 1,996,716 ns | +69.7% |
80+
| ConcurrentRequests_Heavy / CL=512 | 12,955,241 ns | 6,837,216 ns | +47.2% | 7,692,003 ns | +40.6% |
81+
| ConcurrentRequests_Light / CL=4096 | 49,478,700 ns | 33,977,661 ns | +31.3% | 14,228,775 ns | +71.2% |
82+
| ConcurrentRequests_Heavy / CL=4096 | 94,241,425 ns | 46,708,275 ns | +50.4% | 37,826,442 ns | +59.9% |
83+
84+
#### p95
85+
86+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
87+
|---|---:|---:|---:|---:|---:|
88+
| ConcurrentRequests_Light / CL=512 | 7,465,021 ns | 10,657,414 ns | -42.8% | 2,901,573 ns | +61.1% |
89+
| ConcurrentRequests_Heavy / CL=512 | 14,437,024 ns | 8,726,812 ns | +39.6% | 9,208,768 ns | +36.2% |
90+
| ConcurrentRequests_Light / CL=4096 | 57,498,041 ns | 42,218,934 ns | +26.6% | 17,150,301 ns | +70.2% |
91+
| ConcurrentRequests_Heavy / CL=4096 | 98,974,532 ns | 55,854,108 ns | +43.6% | 39,071,458 ns | +60.5% |
92+
93+
#### p99
94+
95+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
96+
|---|---:|---:|---:|---:|---:|
97+
| ConcurrentRequests_Light / CL=512 | 7,625,899 ns | 10,830,154 ns | -42.0% | 2,970,250 ns | +61.1% |
98+
| ConcurrentRequests_Heavy / CL=512 | 14,647,485 ns | 8,929,927 ns | +39.0% | 9,235,972 ns | +36.9% |
99+
| ConcurrentRequests_Light / CL=4096 | 58,533,905 ns | 42,442,978 ns | +27.5% | 17,457,507 ns | +70.2% |
100+
| ConcurrentRequests_Heavy / CL=4096 | 99,633,666 ns | 55,940,292 ns | +43.9% | 39,433,762 ns | +60.4% |
101+
102+
### Concurrent Memory (Allocated bytes/op — lower is better)
103+
104+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
105+
|---|---:|---:|---:|---:|---:|
106+
| ConcurrentRequests_Light / CL=512 | 1,351,445 B | 2,513,379 B | -86.0% | 1,642,918 B | -21.6% |
107+
| ConcurrentRequests_Heavy / CL=512 | 21,481,607 B | 22,144,062 B | -3.1% | 20,448,074 B | +4.8% |
108+
| ConcurrentRequests_Light / CL=4096 | 10,811,462 B | 19,821,198 B | -83.3% | 13,078,302 B | -21.0% |
109+
| ConcurrentRequests_Heavy / CL=4096 | 171,843,902 B | 175,716,993 B | -2.3% | 210,727,631 B | -22.6% |
110+
111+
# HTTP/2.0
112+
113+
## Single Request — Throughput (Req/sec — higher is better)
114+
115+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
116+
|---|---:|---:|---:|---:|---:|
117+
| ConcurrentRequests_Light / CL=1 | 18,548 | 13,925 | -24.9% | 12,243 | -34.0% |
118+
| ConcurrentRequests_Heavy / CL=1 | 15,914 | 9,803 | -38.4% | 9,255 | -41.8% |
119+
120+
## Single Request — Latency (ns — lower is better)
121+
122+
### p50 (Median)
123+
124+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
125+
|---|---:|---:|---:|---:|---:|
126+
| ConcurrentRequests_Light / CL=1 | 44,850 ns | 69,981 ns | -56.0% | 81,060 ns | -80.7% |
127+
| ConcurrentRequests_Heavy / CL=1 | 61,841 ns | 91,502 ns | -48.0% | 104,823 ns | -69.5% |
128+
129+
### p95
130+
131+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
132+
|---|---:|---:|---:|---:|---:|
133+
| ConcurrentRequests_Light / CL=1 | 80,171 ns | 78,889 ns | +1.6% | 85,076 ns | -6.1% |
134+
| ConcurrentRequests_Heavy / CL=1 | 66,651 ns | 133,972 ns | -101.0% | 120,776 ns | -81.2% |
135+
136+
### p99
137+
138+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
139+
|---|---:|---:|---:|---:|---:|
140+
| ConcurrentRequests_Light / CL=1 | 85,198 ns | 80,518 ns | +5.5% | 86,450 ns | -1.5% |
141+
| ConcurrentRequests_Heavy / CL=1 | 67,185 ns | 140,506 ns | -109.1% | 123,006 ns | -83.1% |
142+
143+
## Single Request — Memory (Allocated bytes/op — lower is better)
144+
145+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
146+
|---|---:|---:|---:|---:|---:|
147+
| ConcurrentRequests_Light / CL=1 | 3,385 B | 5,510 B | -62.8% | 5,294 B | -56.4% |
148+
| ConcurrentRequests_Heavy / CL=1 | 43,950 B | 44,807 B | -1.9% | 44,653 B | -1.6% |
149+
150+
---
151+
152+
## Concurrent Benchmarks
153+
154+
> N requests are fired simultaneously (SendAsync: `Task.WhenAll`; Streaming: channel write-all, drain-all).
155+
> **Throughput** = N / Mean (aggregate req/sec across all parallel slots).
156+
> **Latency** = elapsed wall-time until all N complete (lower is better).
157+
158+
### Concurrent Throughput (Req/sec — higher is better)
159+
160+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
161+
|---|---:|---:|---:|---:|---:|
162+
| ConcurrentRequests_Light / CL=512 | 130,294 | 201,153 | +54.4% | 266,409 | +104.5% |
163+
| ConcurrentRequests_Heavy / CL=512 | 48,801 | 56,287 | +15.3% | 56,143 | +15.0% |
164+
| ConcurrentRequests_Light / CL=4096 | 379,976 | 214,172 | -43.6% | 315,954 | -16.8% |
165+
| ConcurrentRequests_Heavy / CL=4096 | 113,657 | 80,337 | -29.3% | 156,437 | +37.6% |
166+
167+
### Concurrent Latency (ns — lower is better)
168+
169+
#### p50 (Median)
170+
171+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
172+
|---|---:|---:|---:|---:|---:|
173+
| ConcurrentRequests_Light / CL=512 | 4,023,312 ns | 2,287,904 ns | +43.1% | 1,937,218 ns | +51.9% |
174+
| ConcurrentRequests_Heavy / CL=512 | 10,052,945 ns | 9,474,868 ns | +5.8% | 9,177,161 ns | +8.7% |
175+
| ConcurrentRequests_Light / CL=4096 | 9,265,517 ns | 17,794,035 ns | -92.0% | 12,577,429 ns | -35.7% |
176+
| ConcurrentRequests_Heavy / CL=4096 | 36,229,975 ns | 50,741,381 ns | -40.1% | 25,815,110 ns | +28.7% |
177+
178+
#### p95
179+
180+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
181+
|---|---:|---:|---:|---:|---:|
182+
| ConcurrentRequests_Light / CL=512 | 4,459,141 ns | 3,547,133 ns | +20.5% | 2,299,195 ns | +48.4% |
183+
| ConcurrentRequests_Heavy / CL=512 | 15,131,136 ns | 10,173,815 ns | +32.8% | 10,507,704 ns | +30.6% |
184+
| ConcurrentRequests_Light / CL=4096 | 15,556,228 ns | 24,229,471 ns | -55.8% | 15,592,222 ns | -0.2% |
185+
| ConcurrentRequests_Heavy / CL=4096 | 39,385,431 ns | 56,815,354 ns | -44.3% | 33,660,218 ns | +14.5% |
186+
187+
#### p99
188+
189+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
190+
|---|---:|---:|---:|---:|---:|
191+
| ConcurrentRequests_Light / CL=512 | 4,468,015 ns | 3,574,316 ns | +20.0% | 2,329,021 ns | +47.9% |
192+
| ConcurrentRequests_Heavy / CL=512 | 15,288,171 ns | 10,264,576 ns | +32.9% | 10,852,481 ns | +29.0% |
193+
| ConcurrentRequests_Light / CL=4096 | 15,880,833 ns | 24,587,174 ns | -54.8% | 15,899,704 ns | -0.1% |
194+
| ConcurrentRequests_Heavy / CL=4096 | 40,633,526 ns | 57,059,601 ns | -40.4% | 34,142,172 ns | +16.0% |
195+
196+
### Concurrent Memory (Allocated bytes/op — lower is better)
197+
198+
| Scenario | HttpClient | SendAsync | Δ% | Streaming | Δ% |
199+
|---|---:|---:|---:|---:|---:|
200+
| ConcurrentRequests_Light / CL=512 | 1,707,043 B | 2,387,265 B | -39.8% | 1,873,426 B | -9.7% |
201+
| ConcurrentRequests_Heavy / CL=512 | 24,447,072 B | 17,730,029 B | +27.5% | 17,817,293 B | +27.1% |
202+
| ConcurrentRequests_Light / CL=4096 | 13,620,632 B | 18,714,322 B | -37.4% | 15,534,137 B | -14.0% |
203+
| ConcurrentRequests_Heavy / CL=4096 | 201,487,630 B | 98,045,236 B | +51.3% | 128,515,193 B | +36.2% |
204+
205+
## Notes
206+
207+
- All requests target a localhost Kestrel server over loopback (127.0.0.1).
208+
- No TLS overhead — HTTP cleartext for both HTTP/1.1 and HTTP/2 (h2c).
209+
- Light: `GET /benchmark/simple` returns `OK\n` (~3 B). Heavy: `POST /benchmark/payload` with 10 KB body.
210+
- HTTP/2 uses h2c prior knowledge on a dedicated listener port.
211+
- Loopback eliminates network jitter — results reflect pure client+server overhead.
212+
- Memory figures reflect managed allocations only; native/pooled buffers are not included.
213+
- **Streaming** uses the channel API (`Requests` writer / `Responses` reader).
214+
- **SendAsync** uses `Task.WhenAll` fan-out; each concurrent slot gets its own `Task<HttpResponseMessage>`.
215+

0 commit comments

Comments
 (0)