|
| 1 | +using BenchmarkDotNet.Attributes; |
| 2 | +using Microsoft.Extensions.Logging.Abstractions; |
| 3 | +using NeoReports.Abstractions; |
| 4 | +using NeoReports.Core; |
| 5 | +using NeoReports.Core.Building; |
| 6 | +using NeoReports.Core.Pipeline; |
| 7 | +using static NeoReports.Core.Building.ReportColumns; |
| 8 | + |
| 9 | +namespace NeoReports.Benchmarks; |
| 10 | + |
| 11 | +/// <summary> |
| 12 | +/// Proves CA-3 (constant memory). The benchmark runs the full pipeline over a synthetic source of |
| 13 | +/// <see cref="RowCount"/> rows. With <c>MemoryDiagnoser</c>, the key signal is that the |
| 14 | +/// <b>Allocated</b> column scales <i>linearly</i> with <see cref="RowCount"/> (constant |
| 15 | +/// allocation per row, no super-linear growth) and that the working set does not grow with the |
| 16 | +/// total — i.e. nothing buffers the whole report. CSV is fully streaming; XLSX is included for |
| 17 | +/// contrast (ClosedXML builds the workbook in memory by design — see ADR D14). |
| 18 | +/// </summary> |
| 19 | +[MemoryDiagnoser] |
| 20 | +public class ReportMemoryBenchmark |
| 21 | +{ |
| 22 | + private static readonly IServiceProvider Services = new EmptyServiceProvider(); |
| 23 | + |
| 24 | + /// <summary>Row counts spanning an order of magnitude so per-row allocation is observable.</summary> |
| 25 | + [Params(100_000L, 1_000_000L)] |
| 26 | + public long RowCount { get; set; } |
| 27 | + |
| 28 | + private CompiledReport _csvReport = null!; |
| 29 | + private CompiledReport _xlsxReport = null!; |
| 30 | + |
| 31 | + [GlobalSetup] |
| 32 | + public void Setup() |
| 33 | + { |
| 34 | + _csvReport = BuildReport(Formats.Csv.Format.Csv(o => o.Delimiter(';'))); |
| 35 | + _xlsxReport = BuildReport(Formats.Xlsx.Format.Xlsx(o => o.SheetName("Vendas"))); |
| 36 | + } |
| 37 | + |
| 38 | + private CompiledReport BuildReport(OutputSpec output) |
| 39 | + { |
| 40 | + // The schema the synthetic source declares is not consumed by the pipeline (the builder's |
| 41 | + // columns drive projection), so a placeholder is fine. |
| 42 | + var schema = new ReportSchema(new[] { new ReportColumn("Id", ColumnType.Integer) }); |
| 43 | + var source = new SyntheticSource(RowCount, schema); |
| 44 | + |
| 45 | + return new ReportBuilder<Venda>("bench") |
| 46 | + .From(source) |
| 47 | + .WithPageSize(1000) |
| 48 | + .Columns( |
| 49 | + Col<Venda, long>(v => v.Id, "ID Venda"), |
| 50 | + Col<Venda, string>(v => v.Cliente, "Cliente"), |
| 51 | + Col<Venda, decimal>(v => v.Valor, "Valor", format: "C2", culture: "pt-BR"), |
| 52 | + Col<Venda, DateTime>(v => v.Data, "Data Venda", format: "yyyy-MM-dd")) |
| 53 | + .To(output) |
| 54 | + .Build(); |
| 55 | + } |
| 56 | + |
| 57 | + [Benchmark(Description = "CSV (streaming)")] |
| 58 | + public async Task<long> Csv() |
| 59 | + { |
| 60 | + var result = await RunAsync(_csvReport).ConfigureAwait(false); |
| 61 | + return result.Stats.RecordsWritten; |
| 62 | + } |
| 63 | + |
| 64 | + [Benchmark(Description = "XLSX (ClosedXML, in-memory)")] |
| 65 | + public async Task<long> Xlsx() |
| 66 | + { |
| 67 | + var result = await RunAsync(_xlsxReport).ConfigureAwait(false); |
| 68 | + return result.Stats.RecordsWritten; |
| 69 | + } |
| 70 | + |
| 71 | + private static Task<ReportRunResult> RunAsync(CompiledReport report) |
| 72 | + { |
| 73 | + var execution = new ReportExecutionContext( |
| 74 | + Guid.NewGuid().ToString("N"), report.Name, null, NullLogger.Instance, CancellationToken.None); |
| 75 | + return ReportRunner.ExecuteAsync(report, execution, Services, CancellationToken.None); |
| 76 | + } |
| 77 | + |
| 78 | + private sealed class EmptyServiceProvider : IServiceProvider |
| 79 | + { |
| 80 | + public object? GetService(Type serviceType) => null; |
| 81 | + } |
| 82 | +} |
0 commit comments