Skip to content

Commit 3e3bc2f

Browse files
committed
broken
1 parent a558035 commit 3e3bc2f

18 files changed

Lines changed: 426 additions & 572 deletions

src/TurboHttp.Benchmarks/Internal/BenchmarkComparisonReport.cs

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,18 @@ public static class BenchmarkComparisonReport
2929

3030
/// <summary>
3131
/// Generates a markdown report string comparing <paramref name="httpClientResults"/>
32-
/// against <paramref name="turboHttpResults"/>.
32+
/// against <paramref name="turboHttpResults"/>, with an optional concurrent-benchmark section.
3333
/// </summary>
34-
/// <param name="httpClientResults">Baseline HttpClient benchmark results.</param>
35-
/// <param name="turboHttpResults">TurboHttp benchmark results for the same scenarios.</param>
34+
/// <param name="httpClientResults">Baseline HttpClient single-request benchmark results.</param>
35+
/// <param name="turboHttpResults">TurboHttp single-request benchmark results.</param>
36+
/// <param name="httpClientConcurrentResults">Baseline HttpClient concurrent benchmark results.</param>
37+
/// <param name="turboHttpConcurrentResults">TurboHttp concurrent benchmark results.</param>
3638
/// <returns>A markdown-formatted comparison report.</returns>
3739
public static string GenerateReport(
3840
IReadOnlyList<BenchmarkResult> httpClientResults,
39-
IReadOnlyList<BenchmarkResult> turboHttpResults)
41+
IReadOnlyList<BenchmarkResult> turboHttpResults,
42+
IReadOnlyList<BenchmarkResult> httpClientConcurrentResults,
43+
IReadOnlyList<BenchmarkResult> turboHttpConcurrentResults)
4044
{
4145
var sb = new StringBuilder();
4246
var now = DateTime.UtcNow;
@@ -45,6 +49,7 @@ public static string GenerateReport(
4549
AppendThroughputTable(sb, httpClientResults, turboHttpResults);
4650
AppendLatencyTable(sb, httpClientResults, turboHttpResults);
4751
AppendMemoryTable(sb, httpClientResults, turboHttpResults);
52+
AppendConcurrentSections(sb, httpClientConcurrentResults, turboHttpConcurrentResults);
4853
AppendNotes(sb);
4954

5055
return sb.ToString();
@@ -190,6 +195,106 @@ private static void AppendMemoryTable(
190195
sb.AppendLine();
191196
}
192197

198+
private static void AppendConcurrentSections(
199+
StringBuilder sb,
200+
IReadOnlyList<BenchmarkResult> httpResults,
201+
IReadOnlyList<BenchmarkResult> turboResults)
202+
{
203+
sb.AppendLine("---");
204+
sb.AppendLine();
205+
sb.AppendLine("## Concurrent Benchmarks");
206+
sb.AppendLine();
207+
sb.AppendLine("> N requests are fired simultaneously via `Task.WhenAll`.");
208+
sb.AppendLine("> **Throughput** = N / Mean (total req/sec across all parallel slots).");
209+
sb.AppendLine("> **Latency** = elapsed wall-time until all N complete (lower is better).");
210+
sb.AppendLine();
211+
212+
AppendConcurrentThroughputTable(sb, httpResults, turboResults);
213+
AppendConcurrentLatencyTable(sb, httpResults, turboResults);
214+
AppendConcurrentMemoryTable(sb, httpResults, turboResults);
215+
}
216+
217+
private static void AppendConcurrentThroughputTable(
218+
StringBuilder sb,
219+
IReadOnlyList<BenchmarkResult> httpResults,
220+
IReadOnlyList<BenchmarkResult> turboResults)
221+
{
222+
sb.AppendLine("### Concurrent Throughput (Req/sec — higher is better)");
223+
sb.AppendLine();
224+
sb.AppendLine("| Scenario | HttpClient | TurboHttp | Delta% | |");
225+
sb.AppendLine("|---|---:|---:|---:|:---:|");
226+
227+
foreach (var row in MatchRows(httpResults, turboResults))
228+
{
229+
var cl = ParseConcurrencyLevel(row.Name);
230+
var httpRps = ConcurrentNsToRps(row.Http.MeanNanoseconds, cl);
231+
var turboRps = ConcurrentNsToRps(row.Turbo.MeanNanoseconds, cl);
232+
233+
var delta = ComputeDelta(httpRps, turboRps);
234+
var indicator = ThroughputIndicator(delta);
235+
236+
sb.AppendLine(
237+
$"| {row.Name} | {httpRps:N0} | {turboRps:N0} | {delta:+0.0;-0.0;0.0}% | {indicator} |");
238+
}
239+
240+
sb.AppendLine();
241+
}
242+
243+
private static void AppendConcurrentLatencyTable(
244+
StringBuilder sb,
245+
IReadOnlyList<BenchmarkResult> httpResults,
246+
IReadOnlyList<BenchmarkResult> turboResults)
247+
{
248+
sb.AppendLine("### Concurrent Latency (ns — lower is better)");
249+
sb.AppendLine();
250+
251+
sb.AppendLine("#### p50 (Median)");
252+
sb.AppendLine();
253+
sb.AppendLine("| Scenario | HttpClient | TurboHttp | Delta% | |");
254+
sb.AppendLine("|---|---:|---:|---:|:---:|");
255+
AppendLatencyRows(sb, httpResults, turboResults, r => r.P50Nanoseconds);
256+
sb.AppendLine();
257+
258+
sb.AppendLine("#### p95");
259+
sb.AppendLine();
260+
sb.AppendLine("| Scenario | HttpClient | TurboHttp | Delta% | |");
261+
sb.AppendLine("|---|---:|---:|---:|:---:|");
262+
AppendLatencyRows(sb, httpResults, turboResults, r => r.P95Nanoseconds);
263+
sb.AppendLine();
264+
265+
sb.AppendLine("#### p99");
266+
sb.AppendLine();
267+
sb.AppendLine("| Scenario | HttpClient | TurboHttp | Delta% | |");
268+
sb.AppendLine("|---|---:|---:|---:|:---:|");
269+
AppendLatencyRows(sb, httpResults, turboResults, r => r.P99Nanoseconds);
270+
sb.AppendLine();
271+
}
272+
273+
private static void AppendConcurrentMemoryTable(
274+
StringBuilder sb,
275+
IReadOnlyList<BenchmarkResult> httpResults,
276+
IReadOnlyList<BenchmarkResult> turboResults)
277+
{
278+
sb.AppendLine("### Concurrent Memory (Allocated bytes/op — lower is better)");
279+
sb.AppendLine();
280+
sb.AppendLine("| Scenario | HttpClient | TurboHttp | Delta% | |");
281+
sb.AppendLine("|---|---:|---:|---:|:---:|");
282+
283+
foreach (var row in MatchRows(httpResults, turboResults))
284+
{
285+
double httpBytes = row.Http.AllocatedBytes;
286+
double turboBytes = row.Turbo.AllocatedBytes;
287+
288+
var delta = ComputeLatencyDelta(httpBytes, turboBytes);
289+
var indicator = ThroughputIndicator(delta);
290+
291+
sb.AppendLine(
292+
$"| {row.Name} | {row.Http.AllocatedBytes:N0} B | {row.Turbo.AllocatedBytes:N0} B | {delta:+0.0;-0.0;0.0}% | {indicator} |");
293+
}
294+
295+
sb.AppendLine();
296+
}
297+
193298
private static void AppendNotes(StringBuilder sb)
194299
{
195300
sb.AppendLine("## Notes");
@@ -253,6 +358,39 @@ public static string ThroughputIndicator(double deltaPercent)
253358
};
254359
}
255360

361+
/// <summary>
362+
/// Converts nanoseconds-per-batch to requests per second, scaling by
363+
/// <paramref name="concurrencyLevel"/> because each batch completes N requests.
364+
/// </summary>
365+
public static double ConcurrentNsToRps(double meanNanoseconds, int concurrencyLevel)
366+
{
367+
if (meanNanoseconds <= 0)
368+
{
369+
return 0;
370+
}
371+
372+
return concurrencyLevel * 1_000_000_000.0 / meanNanoseconds;
373+
}
374+
375+
/// <summary>
376+
/// Parses the concurrency level from a scenario name built by
377+
/// <see cref="SummaryExtractor"/> (e.g. <c>"ConcurrentRequests_Light / CL=16 / …"</c>).
378+
/// Returns 1 when no <c>CL=</c> token is found.
379+
/// </summary>
380+
public static int ParseConcurrencyLevel(string name)
381+
{
382+
var clIdx = name.IndexOf("CL=", StringComparison.Ordinal);
383+
if (clIdx < 0)
384+
{
385+
return 1;
386+
}
387+
388+
var start = clIdx + 3;
389+
var spaceIdx = name.IndexOf(' ', start);
390+
var slice = spaceIdx < 0 ? name.AsSpan(start) : name.AsSpan(start, spaceIdx - start);
391+
return int.TryParse(slice, out var cl) ? cl : 1;
392+
}
393+
256394
// Row matching
257395

258396
private static IReadOnlyList<(string Name, BenchmarkResult Http, BenchmarkResult Turbo)> MatchRows(

src/TurboHttp.Benchmarks/Program.cs

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,26 @@
55
var summaries = BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
66

77
var enumerable = summaries.ToList();
8-
var httpSummary = enumerable.FirstOrDefault(s => s.HasBenchmarksOf<HttpClientSingleRequestBenchmarks>());
9-
var turboSummary = enumerable.FirstOrDefault(s => s.HasBenchmarksOf<TurboHttpSingleRequestBenchmarks>());
8+
var httpSingleSummary = enumerable.FirstOrDefault(s => s.HasBenchmarksOf<HttpClientSingleRequestBenchmarks>());
9+
var turboSingleSummary = enumerable.FirstOrDefault(s => s.HasBenchmarksOf<TurboHttpSingleRequestBenchmarks>());
10+
var httpConcurrentSummary = enumerable.FirstOrDefault(s => s.HasBenchmarksOf<HttpClientConcurrentBenchmarks>());
11+
var turboConcurrentSummary = enumerable.FirstOrDefault(s => s.HasBenchmarksOf<TurboHttpConcurrentBenchmarks>());
1012

11-
if (httpSummary is not null && turboSummary is not null)
13+
if (httpSingleSummary is not null
14+
&& turboSingleSummary is not null
15+
&& httpConcurrentSummary is not null
16+
&& turboConcurrentSummary is not null)
1217
{
13-
var httpResults = SummaryExtractor.Extract(httpSummary);
14-
var turboResults = SummaryExtractor.Extract(turboSummary);
15-
var markdown = BenchmarkComparisonReport.GenerateReport(httpResults, turboResults);
18+
var httpResults = SummaryExtractor.Extract(httpSingleSummary);
19+
var turboResults = SummaryExtractor.Extract(turboSingleSummary);
20+
var httpConcurrentResults = SummaryExtractor.Extract(httpConcurrentSummary);
21+
var turboConcurrentResults = SummaryExtractor.Extract(turboConcurrentSummary);
22+
23+
var markdown = BenchmarkComparisonReport.GenerateReport(
24+
httpResults,
25+
turboResults,
26+
httpConcurrentResults,
27+
turboConcurrentResults);
1628

1729
if (markdown.Contains("NaN") || markdown.Contains("Infinity") || markdown.Contains("Inf%"))
1830
{
@@ -21,4 +33,9 @@
2133

2234
var path = BenchmarkComparisonReport.WriteReportToFile(markdown);
2335
Console.WriteLine($"Comparison report: {path}");
36+
}
37+
else
38+
{
39+
Console.WriteLine("Comparison report skipped — not all 4 benchmark suites ran " +
40+
"(HttpClientSingleRequest, TurboHttpSingleRequest, HttpClientConcurrent, TurboHttpConcurrent).");
2441
}

src/TurboHttp.IntegrationTests/Shared/ActorSystemFixture.cs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Akka.Actor;
2+
using Akka.Actor.Setup;
23
using Akka.Configuration;
34
using Akka.DependencyInjection;
45
using Microsoft.Extensions.DependencyInjection;
@@ -20,18 +21,6 @@ public ValueTask InitializeAsync()
2021
var diSetup = DependencyResolverSetup.Create(services.BuildServiceProvider());
2122
var bootstrap = BootstrapSetup.Create();
2223

23-
// When TURBO_DEBUG=1 is set, load akka.debug.conf for verbose pipeline logging.
24-
if (Environment.GetEnvironmentVariable("TURBO_DEBUG") == "1")
25-
{
26-
var configFile = Path.Combine(AppContext.BaseDirectory, "akka.debug.conf");
27-
if (File.Exists(configFile))
28-
{
29-
var hocon = File.ReadAllText(configFile);
30-
var debugConfig = ConfigurationFactory.ParseString(hocon);
31-
bootstrap = bootstrap.WithConfig(debugConfig);
32-
}
33-
}
34-
3524
var setup = bootstrap.And(diSetup);
3625
System = ActorSystem.Create($"turbohttp-shared-{Guid.NewGuid()}", setup);
3726
return ValueTask.CompletedTask;

src/TurboHttp.IntegrationTests/TurboHttp.IntegrationTests.csproj

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,6 @@
2727
<Content Include="xunit.runner.json">
2828
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
2929
</Content>
30-
<Content Include="akka.debug.conf">
31-
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
32-
</Content>
3330
</ItemGroup>
3431

3532
</Project>

src/TurboHttp.IntegrationTests/akka.debug.conf

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/TurboHttp.StreamTests/Http2/Encoding/Http2BatchEncodingSpec.cs

Lines changed: 10 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,8 @@ public async Task Http2BatchEncoding_should_pass_through_unchanged_when_single_f
7272
var body = new byte[] { 0x01, 0x02, 0x03 };
7373
var frame = new DataFrame(streamId: 1, data: body, endStream: true);
7474

75-
var item = await Source.Single((Http2Frame)frame)
75+
var item = await Source.Single(new List<Http2Frame> { frame })
7676
.Via(Flow.FromGraph(new Http20EncoderStage()))
77-
.Via(Flow.Create<IOutputItem>()
78-
.BatchWeighted(
79-
Http20Engine.MaxBatchWeight,
80-
x => x is NetworkBuffer d ? d.Length : 0L,
81-
x => x,
82-
Http20Engine.BatchConsolidate))
8377
.RunWith(Sink.First<IOutputItem>(), Materializer);
8478

8579
var dataItem = Assert.IsAssignableFrom<NetworkBuffer>(item);
@@ -98,34 +92,21 @@ public void Http2BatchEncoding_should_expose_max_weight_as_64kb_constant()
9892
}
9993

10094
[Fact(Timeout = 10_000)]
101-
public async Task Http2BatchEncoding_should_batch_multiple_frames_when_stream_has_multiple_small_frames()
95+
public async Task Http2BatchEncoding_should_encode_multiple_frames_into_single_buffer()
10296
{
103-
// Create several small frames that should be batched together
97+
// 5 PING frames passed as one batch → encoder writes them into a single NetworkBuffer.
10498
var frames = Enumerable.Range(1, 5)
10599
.Select(i => (Http2Frame)new PingFrame(data: BitConverter.GetBytes((long)i)))
106100
.ToList();
107101

108-
var items = await Source.From(frames)
102+
var item = await Source.Single(frames)
109103
.Via(Flow.FromGraph(new Http20EncoderStage()))
110-
.Via(Flow.Create<IOutputItem>()
111-
.BatchWeighted(
112-
Http20Engine.MaxBatchWeight,
113-
x => x is NetworkBuffer d ? d.Length : 0L,
114-
x => x,
115-
Http20Engine.BatchConsolidate))
116-
.RunWith(Sink.Seq<IOutputItem>(), Materializer);
117-
118-
var totalBytes = items.OfType<NetworkBuffer>().Sum(d => d.Length);
119-
var expectedSize = frames.Sum(f => f.SerializedSize);
120-
Assert.Equal(expectedSize, totalBytes);
121-
122-
// Verify concatenated bytes are valid by checking each frame is 17 bytes (9 header + 8 ping data)
123-
Assert.Equal(17 * 5, totalBytes);
124-
125-
foreach (var item in items.OfType<NetworkBuffer>())
126-
{
127-
item.Dispose();
128-
}
104+
.RunWith(Sink.First<IOutputItem>(), Materializer);
105+
106+
var buffer = Assert.IsAssignableFrom<NetworkBuffer>(item);
107+
// Each PING frame = 9-byte header + 8-byte data = 17 bytes
108+
Assert.Equal(17 * 5, buffer.Length);
109+
buffer.Dispose();
129110
}
130111

131112
[Fact(Timeout = 5_000)]

src/TurboHttp.StreamTests/Http2/Encoding/Http2EncoderFrameSerializationSpec.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ public sealed class Http2EncoderFrameSerializationSpec : StreamTestBase
1313
{
1414
private async Task<byte[]> EncodeAsync(Http2Frame frame)
1515
{
16-
var item = await Source.Single(frame)
16+
var item = await Source.Single(new List<Http2Frame> { frame })
1717
.Via(Flow.FromGraph(new Http20EncoderStage()))
1818
.RunWith(Sink.First<IOutputItem>(), Materializer);
1919

src/TurboHttp.StreamTests/Http2/Encoding/Http2EncoderSpec.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public sealed class Http2EncoderSpec : StreamTestBase
2323

2424
private async Task<byte[]> EncodeAsync(Http2Frame frame)
2525
{
26-
var item = await Source.Single(frame)
26+
var item = await Source.Single(new List<Http2Frame> { frame })
2727
.Via(Flow.FromGraph(new Http20EncoderStage()))
2828
.RunWith(Sink.First<IOutputItem>(), Materializer);
2929

@@ -33,9 +33,10 @@ private async Task<byte[]> EncodeAsync(Http2Frame frame)
3333
return bytes;
3434
}
3535

36+
// Encodes each frame as a separate single-element batch, preserving per-frame output semantics.
3637
private async Task<List<NetworkBuffer>> EncodeMultipleAsync(params Http2Frame[] frames)
3738
{
38-
var items = await Source.From(frames)
39+
var items = await Source.From(frames.Select(f => new List<Http2Frame> { f }))
3940
.Via(Flow.FromGraph(new Http20EncoderStage()))
4041
.RunWith(Sink.Seq<IOutputItem>(), Materializer);
4142

0 commit comments

Comments
 (0)