Table: minor optimizations + performance test

This commit is contained in:
MarcinZiabek
2021-12-26 17:04:59 +01:00
committed by Marcin Ziąbek
parent 3e5e3843d2
commit 01a80032b0
7 changed files with 275 additions and 70 deletions
+4 -3
View File
@@ -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 =>
{
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>