From efdc8903a5e76dac3c2fee768b196f07868dc453 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Sun, 26 Dec 2021 17:04:59 +0100 Subject: [PATCH] Table: minor optimizations + performance test --- QuestPDF.Examples/FrameExample.cs | 7 +- QuestPDF.Examples/QuestPDF.Examples.csproj | 1 + QuestPDF.Examples/TableExamples.cs | 168 ++++++++++++++------- QuestPDF.Examples/TablePerformanceTest.cs | 113 ++++++++++++++ QuestPDF.ReportSample/Tests.cs | 2 + QuestPDF/Elements/Table/Table.cs | 52 +++++-- QuestPDF/QuestPDF.csproj | 2 +- 7 files changed, 275 insertions(+), 70 deletions(-) create mode 100644 QuestPDF.Examples/TablePerformanceTest.cs diff --git a/QuestPDF.Examples/FrameExample.cs b/QuestPDF.Examples/FrameExample.cs index 614aa54..ae058ce 100644 --- a/QuestPDF.Examples/FrameExample.cs +++ b/QuestPDF.Examples/FrameExample.cs @@ -13,11 +13,12 @@ namespace QuestPDF.Examples return container .Border(1) .Background(dark ? Colors.Grey.Lighten2 : Colors.White) - .Padding(10); + .Padding(5); } - public static void LabelCell(this IContainer container, string text) => container.Cell(true).Text(text, TextStyle.Default.Medium()); + public static void LabelCell(this IContainer container, string text) => container.Cell(true).Text(text, TextStyle.Default.SemiBold()); public static IContainer ValueCell(this IContainer container) => container.Cell(false); + public static void ValueCell(this IContainer container, string text) => container.ValueCell().Text(text, TextStyle.Default); } public class FrameExample @@ -36,7 +37,7 @@ namespace QuestPDF.Examples .Padding(25) .Stack(stack => { - for(var i=1; i<=4; i++) + for(var i = 1; i <= 4; i++) { stack.Item().Row(row => { diff --git a/QuestPDF.Examples/QuestPDF.Examples.csproj b/QuestPDF.Examples/QuestPDF.Examples.csproj index 12247a8..13b26ba 100644 --- a/QuestPDF.Examples/QuestPDF.Examples.csproj +++ b/QuestPDF.Examples/QuestPDF.Examples.csproj @@ -6,6 +6,7 @@ + diff --git a/QuestPDF.Examples/TableExamples.cs b/QuestPDF.Examples/TableExamples.cs index 1e671b8..67d3949 100644 --- a/QuestPDF.Examples/TableExamples.cs +++ b/QuestPDF.Examples/TableExamples.cs @@ -1,17 +1,21 @@ using System; +using System.ComponentModel; +using System.Diagnostics; using System.Linq; using NUnit.Framework; +using QuestPDF.Drawing; using QuestPDF.Examples.Engine; using QuestPDF.Fluent; using QuestPDF.Helpers; using QuestPDF.Infrastructure; +using IContainer = QuestPDF.Infrastructure.IContainer; namespace QuestPDF.Examples { public class TableExamples { public static Random Random { get; } = new Random(); - + [Test] public void Example() { @@ -51,86 +55,148 @@ namespace QuestPDF.Examples table.Cell().RowSpan(2).Element(CreateBox("K")); table.Cell().ColumnSpan(2).Element(CreateBox("L")); table.Cell().Element(CreateBox("M")); - - // table.Cell().Row(1).Column(1).ColumnSpan(2).Element(CreateBox("A")); - // table.Cell().Row(1).Column(3).Element(CreateBox("B")); - // table.Cell().Row(1).Column(4).Element(CreateBox("C")); - // - // table.Cell().Row(2).Column(1).Element(CreateBox("D")); - // table.Cell().Row(2).RowSpan(2).Column(2).Element(CreateBox("E")); - // table.Cell().Row(2).RowSpan(3).Column(3).ColumnSpan(2).Element(CreateBox("F")); - // - // table.Cell().Row(3).RowSpan(2).Column(1).Element(CreateBox("G")); - // table.Cell().Row(4).RowSpan(2).Column(2).Element(CreateBox("H")); - // table.Cell().Row(5).Column(3).Element(CreateBox("I")); - // table.Cell().Row(5).Column(4).Element(CreateBox("J")); - // table.Cell().Row(5).RowSpan(2).Column(1).Element(CreateBox("K")); - // table.Cell().Row(6).Column(2).ColumnSpan(2).Element(CreateBox("L")); - // table.Cell().Row(6).Column(4).Element(CreateBox("M")); }); }); } [Test] - public void PerformanceTest() + public void TreeTable() { RenderingTest .Create() .ProducePdf() - .PageSize(1002, 2002) - .MaxPages(1000) - .EnableCaching() - .EnableDebugging(false) + .PageSize(PageSizes.A4) .ShowResults() .Render(container => { container - .Padding(1) + .Padding(25) + .Box() + .Border(2) .Table(table => { table.ColumnsDefinition(columns => { - foreach (var size in Enumerable.Range(0, 10)) - columns.ConstantColumn(100); + columns.RelativeColumn(100); + columns.RelativeColumn(100); + columns.RelativeColumn(100); }); - foreach (var i in Enumerable.Range(1, 10_000)) - { - table - .Cell() - .RowSpan((uint)Random.Next(1, 5)) - .ColumnSpan((uint)Random.Next(1, 5)) - .Element(CreateBox(i.ToString())); - } + table.Cell().RowSpan(4).Element(CreateBox("A")); + + table.Cell().RowSpan(2).Element(CreateBox("B")); + table.Cell().Element(CreateBox("C")); + table.Cell().Element(CreateBox("D")); + + table.Cell().RowSpan(2).Element(CreateBox("E")); + table.Cell().Element(CreateBox("F")); + table.Cell().Element(CreateBox("G")); }); }); } + [Test] + public void TemperatureReport() + { + RenderingTest + .Create() + .ProducePdf() + .PageSize(PageSizes.A4) + .ShowResults() + .Render(container => GeneratePerformanceStructure(container, 100)); + } + + [Test] + public void TemperatureReport_PerformanceTest() + { + RenderingTest + .Create() + .ProducePdf() + .PageSize(PageSizes.A4) + .MaxPages(10000) + .EnableCaching() + .EnableDebugging(false) + .ShowResults() + .Render(container => GeneratePerformanceStructure(container, 1000)); + } + + public static void GeneratePerformanceStructure(IContainer container, int repeats) + { + container + .Padding(25) + .Box() + .Border(2) + .Table(table => + { + table.ColumnsDefinition(columns => + { + columns.ConstantColumn(100); + columns.RelativeColumn(); + columns.ConstantColumn(100); + columns.RelativeColumn(); + }); + + foreach (var _ in Enumerable.Range(0, repeats)) + { + table.Cell().RowSpan(3).LabelCell("Project"); + table.Cell().RowSpan(3).ValueCell(Placeholders.Sentence()); + + table.Cell().LabelCell("Date"); + table.Cell().ValueCell(Placeholders.ShortDate()); + + table.Cell().LabelCell("Report number"); + table.Cell().ValueCell(Placeholders.Integer()); + + table.Cell().LabelCell("Inspector"); + table.Cell().ValueCell("Marcin Ziąbek"); + + table.Cell().ColumnSpan(2).LabelCell("Morning weather"); + table.Cell().ColumnSpan(2).LabelCell("Evening weather"); + + table.Cell().ValueCell("Time"); + table.Cell().ValueCell("7:13"); + + table.Cell().ValueCell("Time"); + table.Cell().ValueCell("18:25"); + + table.Cell().ValueCell("Description"); + table.Cell().ValueCell("Sunny"); + + table.Cell().ValueCell("Description"); + table.Cell().ValueCell("Windy"); + + table.Cell().ValueCell("Wind"); + table.Cell().ValueCell("Mild"); + + table.Cell().ValueCell("Wind"); + table.Cell().ValueCell("Strong"); + + table.Cell().ValueCell("Temperature"); + table.Cell().ValueCell("17°C"); + + table.Cell().ValueCell("Temperature"); + table.Cell().ValueCell("32°C"); + + table.Cell().LabelCell("Remarks"); + table.Cell().ColumnSpan(3).ValueCell(Placeholders.Paragraph()); + + table.Cell().ColumnSpan(4).BorderBottom(2); + } + }); + } + private Action CreateBox(string label) { return container => { - var height = Random.Next(2, 7) * 25; + var height = Random.Next(2, 6) * 10; container - .Border(2) .Background(Placeholders.BackgroundColor()) - .Layers(layers => - { - layers - .PrimaryLayer() - .AlignCenter() - .AlignMiddle() - .Height(height) - .Width(80) - .Border(1); - - layers - .Layer() - .AlignCenter() - .AlignMiddle() - .Text($"{label}: {height}px"); - }); + // .AlignCenter() + // .AlignMiddle() + .Height(height); + // .Text($"{label}: {height}px"); }; } } diff --git a/QuestPDF.Examples/TablePerformanceTest.cs b/QuestPDF.Examples/TablePerformanceTest.cs new file mode 100644 index 0000000..e483ee1 --- /dev/null +++ b/QuestPDF.Examples/TablePerformanceTest.cs @@ -0,0 +1,113 @@ +using System; +using System.Diagnostics; +using System.Linq; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Engines; +using BenchmarkDotNet.Running; +using NUnit.Framework; +using QuestPDF.Drawing; +using QuestPDF.Drawing.Proxy; +using QuestPDF.Elements; +using QuestPDF.Fluent; +using QuestPDF.Helpers; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Examples +{ + [SimpleJob(RunStrategy.Monitoring, launchCount: 0, warmupCount: 1, targetCount: 16)] + [MinColumn, MaxColumn, MeanColumn, MedianColumn] + public class TablePerformanceTest + { + private static Random Random { get; } = new Random(); + + private PageContext PageContext { get; set; } + private DocumentMetadata Metadata { get; set; } + private Container Content { get; set; } + + [Test] + public void Run() + { + var configuration = ManualConfig + .Create(DefaultConfig.Instance) + .WithOptions(ConfigOptions.DisableOptimizationsValidator); + + BenchmarkRunner.Run(configuration); + } + + [IterationSetup] + [SetUp] + public void GenerateReportData() + { + Metadata = new DocumentMetadata() + { + DocumentLayoutExceptionThreshold = 1000 + }; + + var documentContainer = new DocumentContainer(); + + documentContainer.Page(page => + { + page.Size(PageSizes.A3); + GeneratePerformanceStructure(page.Content(), 10_000); + }); + + Content = documentContainer.Compose(); + + PageContext = new PageContext(); + DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null); + + Content.HandleVisitor(x => + { + if (x is ICacheable) + x.CreateProxy(y => new CacheProxy(y)); + }); + } + + [Benchmark] + [Test] + public void GenerationTest() + { + DocumentGenerator.RenderPass(PageContext, new FreeCanvas(), Content, Metadata, null); + } + + void GeneratePerformanceStructure(IContainer container, int itemsCount) + { + container + .Padding(25) + .Table(table => + { + table.ColumnsDefinition(columns => + { + foreach (var size in Enumerable.Range(0, 10)) + columns.ConstantColumn(100); + }); + + foreach (var i in Enumerable.Range(1, itemsCount)) + { + table + .Cell() + .RowSpan((uint)Random.Next(1, 5)) + .ColumnSpan((uint)Random.Next(1, 5)) + .Element(CreateBox(i.ToString())); + } + }); + } + + private Action CreateBox(string label) + { + return container => + { + var height = Random.Next(2, 6) * 20; + + container + .Border(2) + .Background(Placeholders.BackgroundColor()) + .AlignCenter() + .AlignMiddle() + .Height(height) + .Text($"{label}: {height}px"); + }; + } + } +} \ No newline at end of file diff --git a/QuestPDF.ReportSample/Tests.cs b/QuestPDF.ReportSample/Tests.cs index f31ba6f..96b2f7f 100644 --- a/QuestPDF.ReportSample/Tests.cs +++ b/QuestPDF.ReportSample/Tests.cs @@ -24,6 +24,8 @@ namespace QuestPDF.ReportSample [Test] public void GenerateAndShowPdf() { + ImagePlaceholder.Solid = true; + var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, $"test_result.pdf"); Report.GeneratePdf(path); Process.Start("explorer.exe", path); diff --git a/QuestPDF/Elements/Table/Table.cs b/QuestPDF/Elements/Table/Table.cs index 7732377..04ac5a3 100644 --- a/QuestPDF/Elements/Table/Table.cs +++ b/QuestPDF/Elements/Table/Table.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using QuestPDF.Drawing; +using QuestPDF.Fluent; using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Table @@ -12,7 +13,9 @@ namespace QuestPDF.Elements.Table public List Children { get; } = new List(); public float Spacing { get; set; } - public int CurrentRow { get; set; } + private TableCell[][] OrderedChildren { get; set; } + private int RowsCount { get; set; } + private int CurrentRow { get; set; } internal override void HandleVisitor(Action visit) { @@ -22,6 +25,21 @@ namespace QuestPDF.Elements.Table public void ResetState() { + if (RowsCount == default) + RowsCount = Children.Max(x => x.Row + x.RowSpan); + + if (OrderedChildren == default) + { + var groups = Children + .GroupBy(x => x.Row) + .ToDictionary(x => x.Key, x => x.OrderBy(y => y.Column).ToArray()); + + OrderedChildren = Enumerable + .Range(0, RowsCount) + .Select(x => groups.TryGetValue(x, out var output) ? output : Array.Empty()) + .ToArray(); + } + CurrentRow = 1; } @@ -31,7 +49,7 @@ namespace QuestPDF.Elements.Table var layout = PlanLayout(availableSpace); - return layout.MaxRowRendered < GetRowsCount() + return layout.MaxRowRendered < RowsCount ? SpacePlan.PartialRender(layout.Size) : SpacePlan.FullRender(layout.Size); } @@ -75,23 +93,30 @@ namespace QuestPDF.Elements.Table .ToList() .ForEach(x => cellOffsets[x] = Columns[x - 1].Width + cellOffsets[x - 1]); + + // update row heights - var rowsCount = GetRowsCount(); + var rowsCount = RowsCount; var rowBottomOffsets = new float[rowsCount]; - var childrenToTry = Children.Where(x => x.Row >= CurrentRow).OrderBy(x => x.Row); + var childrenToTry = Enumerable.Range(CurrentRow - 1, RowsCount - CurrentRow).SelectMany(x => OrderedChildren[x]); var currentRow = CurrentRow; foreach (var child in childrenToTry) { - if (currentRow < child.Row) + if (child.Row > currentRow) { - rowBottomOffsets[currentRow] = Math.Max(rowBottomOffsets[currentRow], rowBottomOffsets[currentRow-1]); + if (rowBottomOffsets[currentRow - 1] > availableSpace.Height + Single.Epsilon) + break; + currentRow = child.Row; } - + var rowIndex = child.Row - 1; + if (rowIndex > 1) + rowBottomOffsets[rowIndex] = Math.Max(rowBottomOffsets[rowIndex], rowBottomOffsets[rowIndex-1]); + var topOffset = 0f; if (rowIndex > 0) @@ -102,11 +127,13 @@ namespace QuestPDF.Elements.Table var targetRowId = child.Row + child.RowSpan - 2; // -1 because indexing starts at 0, -1 because rowSpan starts at 1 rowBottomOffsets[targetRowId] = Math.Max(rowBottomOffsets[targetRowId], cellBottomOffset); - - //if (targetRowId > 1 && rowBottomOffsets[targetRowId - 1] > availableSpace.Height) - // break; } + Enumerable + .Range(1, rowsCount - 1) + .ToList() + .ForEach(x => rowBottomOffsets[x] = Math.Max(rowBottomOffsets[x], rowBottomOffsets[x-1])); + var rowHeights = new float[rowsCount]; rowHeights[0] = rowBottomOffsets[0]; @@ -166,10 +193,5 @@ namespace QuestPDF.Elements.Table return measurement; } } - - int GetRowsCount() - { - return Children.Max(x => x.Row + x.RowSpan); - } } } \ No newline at end of file diff --git a/QuestPDF/QuestPDF.csproj b/QuestPDF/QuestPDF.csproj index 86cd77a..ec7954c 100644 --- a/QuestPDF/QuestPDF.csproj +++ b/QuestPDF/QuestPDF.csproj @@ -7,7 +7,7 @@ 2021.12.0 QuestPDF is an open-source, modern and battle-tested library that can help you with generating PDF documents by offering friendly, discoverable and predictable C# fluent API. $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt")) - 8 + 10 true Logo.png https://www.questpdf.com/images/package-logo.png