Browse Source

Table: minor optimizations + performance test

MarcinZiabek 4 years ago
parent
commit
01a80032b0

+ 4 - 3
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 =>
                                 {

+ 1 - 0
QuestPDF.Examples/QuestPDF.Examples.csproj

@@ -6,6 +6,7 @@
     </PropertyGroup>
 
     <ItemGroup>
+        <PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
         <PackageReference Include="microcharts" Version="0.9.5.9" />
         <PackageReference Include="nunit" Version="3.13.2" />
         <PackageReference Include="NUnit3TestAdapter" Version="4.0.0" />

+ 117 - 51
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<IContainer> 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");
             };
         }
     }

+ 113 - 0
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<TablePerformanceTest>(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<IContainer> 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");
+            };
+        }
+    }
+}

+ 2 - 0
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);

+ 37 - 15
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<TableCell> Children { get; } = new List<TableCell>();
         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<Element?> 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<TableCell>())
+                    .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);
-        }
     }
 }

+ 1 - 1
QuestPDF/QuestPDF.csproj

@@ -7,7 +7,7 @@
         <Version>2021.12.0</Version>
         <PackageDescription>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.</PackageDescription>
         <PackageReleaseNotes>$([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/Resources/ReleaseNotes.txt"))</PackageReleaseNotes>
-        <LangVersion>8</LangVersion>
+        <LangVersion>10</LangVersion>
         <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
         <PackageIcon>Logo.png</PackageIcon>
         <PackageIconUrl>https://www.questpdf.com/images/package-logo.png</PackageIconUrl>