using System.Runtime.CompilerServices; using FluentAssertions; using Workflow.Application.BuildFlow.Common.Services; using Xunit; using Xunit.Abstractions; namespace BuildFlow.Workflow.Tests.Performance; /// OpenXML 顺序导出的托管内存峰值门禁。 public sealed class OpenXmlMemoryProfileTests(ITestOutputHelper output) { private const int ExportRows = 50_000; private const long MaxPeakDeltaBytes = 128L * 1024 * 1024; [Fact] public async Task LargeAsyncExportKeepsManagedMemoryBounded() { var filePath = Path.Combine( Path.GetTempPath(), $"buildflow-openxml-memory-{Guid.NewGuid():N}.xlsx"); GC.Collect(); GC.WaitForPendingFinalizers(); GC.Collect(); var baselineBytes = GC.GetTotalMemory(true); var peakBytes = baselineBytes; void SampleManagedMemory() { peakBytes = Math.Max(peakBytes, GC.GetTotalMemory(false)); } try { var service = new FormSchemaExcelService(); await using var file = new FileStream( filePath, FileMode.CreateNew, FileAccess.ReadWrite, FileShare.None, 81_920, FileOptions.Asynchronous | FileOptions.SequentialScan); await service.WriteExportAsync( SchemaJson, "内存门禁", CreateRowsAsync(ExportRows, SampleManagedMemory), file, CancellationToken.None); await file.FlushAsync(CancellationToken.None); SampleManagedMemory(); new FileInfo(filePath).Length.Should().BeGreaterThan(0); var peakDeltaBytes = Math.Max(0, peakBytes - baselineBytes); peakDeltaBytes.Should().BeLessThan( MaxPeakDeltaBytes, "OpenXML 导出应按行顺序写入,不能把全部 JSON 行或工作表加载到内存"); output.WriteLine($"SOP_METRIC excel_export_peak_managed_bytes={peakDeltaBytes}"); output.WriteLine($"SOP_METRIC excel_export_rows={ExportRows}"); } finally { System.IO.File.Delete(filePath); } } private static async IAsyncEnumerable CreateRowsAsync( int count, Action sampleManagedMemory, [EnumeratorCancellation] CancellationToken ct = default) { for (var index = 0; index < count; index++) { ct.ThrowIfCancellationRequested(); if (index % 128 == 0) { sampleManagedMemory(); } yield return $$"""{"id":{{index}},"name":"row-{{index}}","active":{{(index % 2 == 0).ToString().ToLowerInvariant()}}}"""; if (index % 1024 == 0) { await Task.Yield(); } } sampleManagedMemory(); } private const string SchemaJson = """ { "type": "object", "properties": { "id": { "type": "number", "title": "编号", "x-component": "InputNumber" }, "name": { "type": "string", "title": "名称", "x-component": "Input" }, "active": { "type": "boolean", "title": "启用", "x-component": "Switch" } } } """; }