From 4743768e23648d699b05ad69ab93705a6210cfc6 Mon Sep 17 00:00:00 2001 From: MarcinZiabek Date: Mon, 12 Sep 2022 17:09:07 +0200 Subject: [PATCH] Implemented simple object cache, and updated some fluent api invocations --- QuestPDF.Examples/Engine/RenderingTest.cs | 4 +- QuestPDF.Examples/TableBenchmark.cs | 106 ++++++++++++++------ QuestPDF/Drawing/DocumentGenerator.cs | 2 + QuestPDF/Drawing/ElementCacheManager.cs | 76 ++++++++++++++ QuestPDF/Elements/Border.cs | 10 ++ QuestPDF/Elements/Column.cs | 7 +- QuestPDF/Elements/Container.cs | 2 +- QuestPDF/Elements/Padding.cs | 12 ++- QuestPDF/Elements/Row.cs | 7 +- QuestPDF/Elements/Text/TextBlock.cs | 7 +- QuestPDF/Fluent/BorderExtensions.cs | 3 +- QuestPDF/Fluent/ColumnExtensions.cs | 18 ++-- QuestPDF/Fluent/ElementExtensions.cs | 11 +- QuestPDF/Fluent/PaddingExtensions.cs | 3 +- QuestPDF/Fluent/RowExtensions.cs | 20 ++-- QuestPDF/Infrastructure/ContainerElement.cs | 7 +- QuestPDF/Infrastructure/ICollectable.cs | 7 ++ 17 files changed, 238 insertions(+), 64 deletions(-) create mode 100644 QuestPDF/Drawing/ElementCacheManager.cs create mode 100644 QuestPDF/Infrastructure/ICollectable.cs diff --git a/QuestPDF.Examples/Engine/RenderingTest.cs b/QuestPDF.Examples/Engine/RenderingTest.cs index fadab3f..edf90f8 100644 --- a/QuestPDF.Examples/Engine/RenderingTest.cs +++ b/QuestPDF.Examples/Engine/RenderingTest.cs @@ -65,9 +65,9 @@ namespace QuestPDF.Examples.Engine return this; } - public RenderingTest ShowResults() + public RenderingTest ShowResults(bool value = true) { - ShowResult = true; + ShowResult = value; return this; } diff --git a/QuestPDF.Examples/TableBenchmark.cs b/QuestPDF.Examples/TableBenchmark.cs index 9cba9f3..a1919be 100644 --- a/QuestPDF.Examples/TableBenchmark.cs +++ b/QuestPDF.Examples/TableBenchmark.cs @@ -1,8 +1,13 @@ -using System.Linq; +using System; +using System.Diagnostics; +using System.Linq; using NUnit.Framework; +using QuestPDF.Drawing; +using QuestPDF.Elements; using QuestPDF.Examples.Engine; using QuestPDF.Fluent; using QuestPDF.Helpers; +using QuestPDF.Infrastructure; namespace QuestPDF.Examples { @@ -11,36 +16,77 @@ namespace QuestPDF.Examples [Test] public void Benchmark() { - RenderingTest - .Create() - .ProducePdf() - .PageSize(PageSizes.A4) - .ShowResults() - .MaxPages(10_000) - .EnableCaching(true) - .EnableDebugging(false) - .Render(container => - { - container - .Padding(10) - .MinimalBox() - .Border(1) - .Table(table => - { - const int numberOfRows = 100_000; - const int numberOfColumns = 10; - - table.ColumnsDefinition(columns => - { - foreach (var _ in Enumerable.Range(0, numberOfColumns)) - columns.RelativeColumn(); - }); + GenerateAndCollect(); - foreach (var row in Enumerable.Range(0, numberOfRows)) - foreach (var column in Enumerable.Range(0, numberOfColumns)) - table.Cell().Background(Placeholders.BackgroundColor()).Padding(5).Text($"{row}_{column}"); - }); - }); + var stopwatch = new Stopwatch(); + stopwatch.Start(); + + foreach (var _ in Enumerable.Range(0, 10000)) + { + GenerateAndCollect(); + } + + stopwatch.Stop(); + + Console.WriteLine($"Execution time: {stopwatch.Elapsed:g}"); + + void GenerateAndCollect() + { + var container = new Container(); + + container + .Padding(10) + .MinimalBox() + .Border(1) + .Column(column => + { + const int numberOfRows = 100; + const int numberOfColumns = 10; + + for (var y = 0; y < numberOfRows; y++) + { + column.Item().Row(row => + { + for (var x = 0; x < numberOfColumns; x++) + { + row.RelativeItem() + + .Background(Colors.Red.Lighten5) + .Padding(3) + + .Background(Colors.Red.Lighten4) + .Padding(3) + + .Background(Colors.Red.Lighten3) + .Padding(3) + + .Background(Colors.Red.Lighten2) + .Padding(3) + + .Background(Colors.Red.Lighten1) + .Padding(3) + + .Background(Colors.Red.Medium) + .Padding(3) + + .Background(Colors.Red.Darken1) + .Padding(3) + + .Background(Colors.Red.Darken2) + .Padding(3) + + .Background(Colors.Red.Darken3) + .Padding(3) + + .Background(Colors.Red.Darken4) + .Height(3); + } + }); + } + }); + + ElementCacheManager.Collect(container); + } } } } \ No newline at end of file diff --git a/QuestPDF/Drawing/DocumentGenerator.cs b/QuestPDF/Drawing/DocumentGenerator.cs index 84b0b3d..c04af57 100644 --- a/QuestPDF/Drawing/DocumentGenerator.cs +++ b/QuestPDF/Drawing/DocumentGenerator.cs @@ -74,6 +74,8 @@ namespace QuestPDF.Drawing var pageContext = new PageContext(); RenderPass(pageContext, new FreeCanvas(), content, debuggingState); RenderPass(pageContext, canvas, content, debuggingState); + + ElementCacheManager.Collect(content); } internal static void RenderPass(PageContext pageContext, TCanvas canvas, Container content, DebuggingState? debuggingState) diff --git a/QuestPDF/Drawing/ElementCacheManager.cs b/QuestPDF/Drawing/ElementCacheManager.cs new file mode 100644 index 0000000..4c829ba --- /dev/null +++ b/QuestPDF/Drawing/ElementCacheManager.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Concurrent; +using System.Runtime.CompilerServices; +using QuestPDF.Infrastructure; + +namespace QuestPDF.Drawing +{ + internal class CircularBuffer + { + private const int BufferSize = 11_000; + + private object[] Buffer = new object[BufferSize]; + private int WriteIndex { get; set; } = 0; + private int ReadIndex { get; set; } = 0; + + public T Get() where T : class, new() + { + lock (this) + { + if (ReadIndex == WriteIndex) + return new T(); + + var index = ReadIndex; + ReadIndex = (ReadIndex + 1) % BufferSize; + + var result = Buffer[index] as T; + Buffer[index] = null; + + return result; + } + } + + public void Store(object value) + { + lock (this) + { + Buffer[WriteIndex] = value; + WriteIndex = (WriteIndex + 1) % BufferSize; + } + } + } + + // performance analysis: + // without: 115s + // ConcurrentQueue: 28s + // ConcurrentBag: 38s + // CircularBuffer: 30s + internal static class ElementCacheManager + { + private static ConcurrentDictionary> Cache { get; } = new(); + + public static T Get() where T : class, new() + { + var buffer = Cache.GetOrAdd(typeof(T), _=> new ConcurrentQueue()); + return buffer.TryDequeue(out var result) ? result as T : new T(); + } + + public static void Store(T element) + { + var buffer = Cache.GetOrAdd(element.GetType(), _=> new ConcurrentQueue()); + buffer.Enqueue(element); + } + + public static void Collect(Element element) + { + foreach (var child in element.GetChildren()) + Collect(child); + + if (element is ICollectable collectable) + { + collectable.Collect(); + Store(element); + } + } + } +} \ No newline at end of file diff --git a/QuestPDF/Elements/Border.cs b/QuestPDF/Elements/Border.cs index cbb6c8b..8f215e1 100644 --- a/QuestPDF/Elements/Border.cs +++ b/QuestPDF/Elements/Border.cs @@ -41,5 +41,15 @@ namespace QuestPDF.Elements { return $"Border: Top({Top}) Right({Right}) Bottom({Bottom}) Left({Left}) Color({Color})"; } + + public override void Collect() + { + base.Collect(); + + Left = 0; + Right = 0; + Bottom = 0; + Top = 0; + } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Column.cs b/QuestPDF/Elements/Column.cs index 1579d6b..21caea0 100644 --- a/QuestPDF/Elements/Column.cs +++ b/QuestPDF/Elements/Column.cs @@ -19,7 +19,7 @@ namespace QuestPDF.Elements public Position Offset { get; set; } } - internal class Column : Element, ICacheable, IStateResettable + internal class Column : Element, ICacheable, IStateResettable, ICollectable { internal List Items { get; } = new(); internal float Spacing { get; set; } @@ -124,5 +124,10 @@ namespace QuestPDF.Elements return commands; } + + public void Collect() + { + Items.Clear(); + } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Container.cs b/QuestPDF/Elements/Container.cs index 2a83863..dad473e 100644 --- a/QuestPDF/Elements/Container.cs +++ b/QuestPDF/Elements/Container.cs @@ -4,7 +4,7 @@ namespace QuestPDF.Elements { internal class Container : ContainerElement { - internal Container() + public Container() { } diff --git a/QuestPDF/Elements/Padding.cs b/QuestPDF/Elements/Padding.cs index 9007a87..53b66ef 100644 --- a/QuestPDF/Elements/Padding.cs +++ b/QuestPDF/Elements/Padding.cs @@ -4,7 +4,7 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements { - internal class Padding : ContainerElement, ICacheable + internal class Padding : ContainerElement, ICacheable, ICollectable { public float Top { get; set; } public float Right { get; set; } @@ -62,5 +62,15 @@ namespace QuestPDF.Elements { return $"Padding: Top({Top}) Right({Right}) Bottom({Bottom}) Left({Left})"; } + + public override void Collect() + { + base.Collect(); + + Left = 0; + Right = 0; + Bottom = 0; + Top = 0; + } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Row.cs b/QuestPDF/Elements/Row.cs index cdf6a63..9ceafad 100644 --- a/QuestPDF/Elements/Row.cs +++ b/QuestPDF/Elements/Row.cs @@ -31,7 +31,7 @@ namespace QuestPDF.Elements public Position Offset { get; set; } } - internal class Row : Element, ICacheable, IStateResettable + internal class Row : Element, ICacheable, IStateResettable, ICollectable { internal List Items { get; } = new(); internal float Spacing { get; set; } @@ -156,5 +156,10 @@ namespace QuestPDF.Elements return renderingCommands; } + + public void Collect() + { + Items.Clear(); + } } } \ No newline at end of file diff --git a/QuestPDF/Elements/Text/TextBlock.cs b/QuestPDF/Elements/Text/TextBlock.cs index 6420a7d..2af43ee 100644 --- a/QuestPDF/Elements/Text/TextBlock.cs +++ b/QuestPDF/Elements/Text/TextBlock.cs @@ -8,7 +8,7 @@ using QuestPDF.Infrastructure; namespace QuestPDF.Elements.Text { - internal class TextBlock : Element, IStateResettable + internal class TextBlock : Element, IStateResettable, ICollectable { public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left; public List Items { get; set; } = new List(); @@ -38,6 +38,11 @@ namespace QuestPDF.Elements.Text RenderingQueue.Enqueue(item); } } + + public void Collect() + { + Items.Clear(); + } internal override SpacePlan Measure(Size availableSpace) { diff --git a/QuestPDF/Fluent/BorderExtensions.cs b/QuestPDF/Fluent/BorderExtensions.cs index 32e94f1..f9e2434 100644 --- a/QuestPDF/Fluent/BorderExtensions.cs +++ b/QuestPDF/Fluent/BorderExtensions.cs @@ -1,4 +1,5 @@ using System; +using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; @@ -8,7 +9,7 @@ namespace QuestPDF.Fluent { private static IContainer Border(this IContainer element, Action handler) { - var border = element as Border ?? new Border(); + var border = element as Border ?? ElementCacheManager.Get(); handler(border); return element.Element(border); diff --git a/QuestPDF/Fluent/ColumnExtensions.cs b/QuestPDF/Fluent/ColumnExtensions.cs index 9c10b20..c370b3f 100644 --- a/QuestPDF/Fluent/ColumnExtensions.cs +++ b/QuestPDF/Fluent/ColumnExtensions.cs @@ -1,12 +1,14 @@ using System; +using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; +using Container = System.ComponentModel.Container; namespace QuestPDF.Fluent { public class ColumnDescriptor { - internal Column Column { get; } = new(); + internal Column Column { get; set; } public void Spacing(float value, Unit unit = Unit.Point) { @@ -15,14 +17,9 @@ namespace QuestPDF.Fluent public IContainer Item() { - var container = new Container(); - - Column.Items.Add(new ColumnItem - { - Child = container - }); - - return container; + var columnItem = ElementCacheManager.Get(); + Column.Items.Add(columnItem); + return columnItem; } } @@ -36,7 +33,8 @@ namespace QuestPDF.Fluent public static void Column(this IContainer element, Action handler) { - var descriptor = new ColumnDescriptor(); + var descriptor = ElementCacheManager.Get(); + descriptor.Column = ElementCacheManager.Get(); handler(descriptor); element.Element(descriptor.Column); } diff --git a/QuestPDF/Fluent/ElementExtensions.cs b/QuestPDF/Fluent/ElementExtensions.cs index 804dc84..ff22400 100644 --- a/QuestPDF/Fluent/ElementExtensions.cs +++ b/QuestPDF/Fluent/ElementExtensions.cs @@ -1,4 +1,5 @@ using System; +using QuestPDF.Drawing; using QuestPDF.Drawing.Exceptions; using QuestPDF.Elements; using QuestPDF.Infrastructure; @@ -52,10 +53,10 @@ namespace QuestPDF.Fluent public static IContainer Background(this IContainer element, string color) { - return element.Element(new Background - { - Color = color - }); + var background = ElementCacheManager.Get(); + background.Color = color; + + return element.Element(background); } public static void Placeholder(this IContainer element, string? text = null) @@ -162,7 +163,7 @@ namespace QuestPDF.Fluent public static IContainer MinimalBox(this IContainer element) { - return element.Element(new MinimalBox()); + return element.Element(ElementCacheManager.Get()); } public static IContainer Unconstrained(this IContainer element) diff --git a/QuestPDF/Fluent/PaddingExtensions.cs b/QuestPDF/Fluent/PaddingExtensions.cs index 33d0b46..c94f28b 100644 --- a/QuestPDF/Fluent/PaddingExtensions.cs +++ b/QuestPDF/Fluent/PaddingExtensions.cs @@ -1,4 +1,5 @@ using System; +using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; @@ -8,7 +9,7 @@ namespace QuestPDF.Fluent { private static IContainer Padding(this IContainer element, Action handler) { - var padding = element as Padding ?? new Padding(); + var padding = element as Padding ?? ElementCacheManager.Get(); handler(padding); return element.Element(padding); diff --git a/QuestPDF/Fluent/RowExtensions.cs b/QuestPDF/Fluent/RowExtensions.cs index 2aa5f69..8f7b82f 100644 --- a/QuestPDF/Fluent/RowExtensions.cs +++ b/QuestPDF/Fluent/RowExtensions.cs @@ -1,4 +1,5 @@ using System; +using QuestPDF.Drawing; using QuestPDF.Elements; using QuestPDF.Infrastructure; @@ -6,7 +7,7 @@ namespace QuestPDF.Fluent { public class RowDescriptor { - internal Row Row { get; } = new(); + internal Row Row { get; set; } public void Spacing(float value) { @@ -15,14 +16,12 @@ namespace QuestPDF.Fluent private IContainer Item(RowItemType type, float size = 0) { - var element = new RowItem - { - Type = type, - Size = size - }; + var rowItem = ElementCacheManager.Get(); + rowItem.Type = type; + rowItem.Size = size; - Row.Items.Add(element); - return element; + Row.Items.Add(rowItem); + return rowItem; } [Obsolete("This element has been renamed since version 2022.2. Please use the RelativeItem method.")] @@ -57,9 +56,12 @@ namespace QuestPDF.Fluent { public static void Row(this IContainer element, Action handler) { - var descriptor = new RowDescriptor(); + var descriptor = ElementCacheManager.Get(); + descriptor.Row = ElementCacheManager.Get(); + handler(descriptor); element.Element(descriptor.Row); + ElementCacheManager.Store(descriptor); } } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/ContainerElement.cs b/QuestPDF/Infrastructure/ContainerElement.cs index 794c3d2..878ea69 100644 --- a/QuestPDF/Infrastructure/ContainerElement.cs +++ b/QuestPDF/Infrastructure/ContainerElement.cs @@ -5,7 +5,7 @@ using QuestPDF.Elements; namespace QuestPDF.Infrastructure { - internal abstract class ContainerElement : Element, IContainer + internal abstract class ContainerElement : Element, IContainer, ICollectable { internal Element? Child { get; set; } = Empty.Instance; @@ -34,5 +34,10 @@ namespace QuestPDF.Infrastructure { Child?.Draw(availableSpace); } + + public virtual void Collect() + { + Child = default; + } } } \ No newline at end of file diff --git a/QuestPDF/Infrastructure/ICollectable.cs b/QuestPDF/Infrastructure/ICollectable.cs new file mode 100644 index 0000000..41b0e9b --- /dev/null +++ b/QuestPDF/Infrastructure/ICollectable.cs @@ -0,0 +1,7 @@ +namespace QuestPDF.Infrastructure +{ + public interface ICollectable + { + void Collect(); + } +} \ No newline at end of file