Browse Source

Reduced memory usage by reusing SkParagraphBuilder

Marcin Ziąbek 1 year ago
parent
commit
94edfca98b

+ 3 - 5
Source/QuestPDF/Drawing/FontManager.cs

@@ -18,11 +18,9 @@ namespace QuestPDF.Drawing
     {
     {
         internal static SkTypefaceProvider TypefaceProvider { get; } = new();
         internal static SkTypefaceProvider TypefaceProvider { get; } = new();
         
         
-        private static SkFontCollection LocalFontCollection { get; } = SkFontCollection.Create(TypefaceProvider, SkFontManager.Empty);
-        private static SkFontCollection GlobalFontCollection { get; } = SkFontCollection.Create(TypefaceProvider, SkFontManager.Global);
-        
-        internal static SkFontCollection FontCollection => Settings.UseEnvironmentFonts ? GlobalFontCollection : LocalFontCollection;
-        
+        internal static SkFontCollection LocalFontCollection { get; } = SkFontCollection.Create(TypefaceProvider, SkFontManager.Empty);
+        internal static SkFontCollection GlobalFontCollection { get; } = SkFontCollection.Create(TypefaceProvider, SkFontManager.Global);
+
         static FontManager()
         static FontManager()
         {
         {
             NativeDependencyCompatibilityChecker.Test();
             NativeDependencyCompatibilityChecker.Test();

+ 39 - 0
Source/QuestPDF/Elements/Text/SkParagraphBuilderPoolManager.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Concurrent;
+using QuestPDF.Drawing;
+using QuestPDF.Skia.Text;
+
+namespace QuestPDF.Elements.Text;
+
+internal static class SkParagraphBuilderPoolManager
+{
+    private static ConcurrentDictionary<(ParagraphStyleConfiguration, bool useEnvironmentFonts), ConcurrentBag<SkParagraphBuilder>> ObjectPool { get; } = new();
+
+    public static SkParagraphBuilder Get(ParagraphStyleConfiguration configuration)
+    {
+        var specificPool = GetPool(configuration);
+        
+        if (specificPool.TryTake(out var builder))
+            return builder;
+
+        var fontCollection = Settings.UseEnvironmentFonts
+            ? FontManager.GlobalFontCollection
+            : FontManager.LocalFontCollection;
+        
+        return SkParagraphBuilder.Create(configuration, fontCollection);
+    }
+
+    public static void Return(SkParagraphBuilder builder)
+    {
+        builder.Reset();
+        
+        var specificPool = GetPool(builder.Configuration);
+        specificPool.Add(builder);
+    }
+
+    static ConcurrentBag<SkParagraphBuilder> GetPool(ParagraphStyleConfiguration configuration)
+    {
+        var key = (configuration, Settings.UseEnvironmentFonts);
+        return ObjectPool.GetOrAdd(key, _ => new ConcurrentBag<SkParagraphBuilder>());
+    }
+}

+ 48 - 36
Source/QuestPDF/Elements/Text/TextBlock.cs

@@ -238,45 +238,17 @@ namespace QuestPDF.Elements.Text
                 MaxLinesVisible = LineClamp ?? 1_000_000
                 MaxLinesVisible = LineClamp ?? 1_000_000
             };
             };
             
             
-            using var paragraphBuilder = SkParagraphBuilder.Create(paragraphStyle, FontManager.FontCollection);
-            var currentTextIndex = 0;
-            
-            foreach (var textBlockItem in Items)
-            {
-                if (textBlockItem is TextBlockSpan textBlockSpan)
-                {
-                    if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
-                        textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
-
-                    else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
-                        textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
+            var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
 
 
-                    else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
-                        textBlockPageNumber.UpdatePageNumberText(PageContext);
-                
-                    var textStyle = textBlockSpan.Style.GetSkTextStyle();
-                    paragraphBuilder.AddText(textBlockSpan.Text, textStyle);
-                    currentTextIndex += textBlockSpan.Text.Length;
-                }
-                else if (textBlockItem is TextBlockElement textBlockElement)
-                {
-                    textBlockElement.ConfigureElement(PageContext, Canvas);
-                    textBlockElement.UpdateElementSize();
-                    
-                    paragraphBuilder.AddPlaceholder(new SkPlaceholderStyle
-                    {
-                        Width = textBlockElement.ElementSize.Width,
-                        Height = textBlockElement.ElementSize.Height,
-                        Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
-                        Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
-                        BaselineOffset = 0
-                    });
-                }
+            try
+            {
+                Paragraph = CreateParagraph(builder);
+            }
+            finally
+            {
+                SkParagraphBuilderPoolManager.Return(builder);
             }
             }
 
 
-            Paragraph?.Dispose();
-            Paragraph = paragraphBuilder.CreateParagraph();
-            
             static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
             static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
             {
             {
                 return alignment switch
                 return alignment switch
@@ -313,6 +285,46 @@ namespace QuestPDF.Elements.Text
                     _ => throw new Exception()
                     _ => throw new Exception()
                 };
                 };
             }
             }
+
+            SkParagraph CreateParagraph(SkParagraphBuilder builder)
+            {
+                var currentTextIndex = 0;
+            
+                foreach (var textBlockItem in Items)
+                {
+                    if (textBlockItem is TextBlockSpan textBlockSpan)
+                    {
+                        if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
+                            textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
+            
+                        else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
+                            textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
+            
+                        else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
+                            textBlockPageNumber.UpdatePageNumberText(PageContext);
+                
+                        var textStyle = textBlockSpan.Style.GetSkTextStyle();
+                        builder.AddText(textBlockSpan.Text, textStyle);
+                        currentTextIndex += textBlockSpan.Text.Length;
+                    }
+                    else if (textBlockItem is TextBlockElement textBlockElement)
+                    {
+                        textBlockElement.ConfigureElement(PageContext, Canvas);
+                        textBlockElement.UpdateElementSize();
+                    
+                        builder.AddPlaceholder(new SkPlaceholderStyle
+                        {
+                            Width = textBlockElement.ElementSize.Width,
+                            Height = textBlockElement.ElementSize.Height,
+                            Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
+                            Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
+                            BaselineOffset = 0
+                        });
+                    }
+                }
+
+                return builder.CreateParagraph();
+            }
         }
         }
         
         
         private void CalculateParagraphMetrics(Size availableSpace)
         private void CalculateParagraphMetrics(Size availableSpace)

BIN
Source/QuestPDF/Runtimes/osx-arm64/native/libQuestPdfSkia.dylib


BIN
Source/QuestPDF/Runtimes/osx-x64/native/libQuestPdfSkia.dylib


BIN
Source/QuestPDF/Runtimes/win-x64/native/QuestPdfSkia.dll


+ 9 - 7
Source/QuestPDF/Skia/Text/SkParagraphBuilder.cs

@@ -85,16 +85,18 @@ internal sealed class SkParagraphBuilder : IDisposable
 {
 {
     public IntPtr Instance { get; private set; }
     public IntPtr Instance { get; private set; }
     
     
-    public SkParagraphBuilder(IntPtr instance)
-    {
-        Instance = instance;
-        SkiaAPI.EnsureNotNull(Instance);
-    }
-    
+    public ParagraphStyleConfiguration Configuration { get; private set; }
+
     public static SkParagraphBuilder Create(ParagraphStyleConfiguration paragraphStyleConfiguration, SkFontCollection fontCollection)
     public static SkParagraphBuilder Create(ParagraphStyleConfiguration paragraphStyleConfiguration, SkFontCollection fontCollection)
     {
     {
         var instance = API.paragraph_builder_create(paragraphStyleConfiguration, fontCollection.Instance);
         var instance = API.paragraph_builder_create(paragraphStyleConfiguration, fontCollection.Instance);
-        return new SkParagraphBuilder(instance);
+        SkiaAPI.EnsureNotNull(instance);
+        
+        return new SkParagraphBuilder
+        {
+            Instance = instance,
+            Configuration = paragraphStyleConfiguration
+        };
     }
     }
     
     
     public void AddText(string text, SkTextStyle textStyle)
     public void AddText(string text, SkTextStyle textStyle)