@@ -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 (
0 commit comments