From 89a78f557b13f61b06c6eb56915b7f1a077808e7 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 18:39:25 +0100 Subject: [PATCH] Don't keep the text layout buffers around if they're too large --- .../Media/TextFormatting/BidiReorderer.cs | 4 +- .../TextFormatting/FormattingBufferHelper.cs | 62 +++++++++++++++++++ .../TextFormatting/FormattingObjectPool.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 3 + .../TextFormatting/Unicode/BiDiAlgorithm.cs | 53 ++++++++++++++-- .../Media/TextFormatting/Unicode/BiDiData.cs | 22 +++++-- src/Avalonia.Base/Utilities/BidiDictionary.cs | 29 ++++----- .../Text/HugeTextLayout.cs | 8 +-- 8 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 2c6db4b753..4db55fae6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -117,8 +117,8 @@ namespace Avalonia.Media.TextFormatting } finally { - _runs.Clear(); - _ranges.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs new file mode 100644 index 0000000000..0341842cb6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + internal static class FormattingBufferHelper + { + // 1MB, arbitrary, that's 512K characters or 128K object references on x64 + private const long MaxKeptBufferSizeInBytes = 1024 * 1024; + + public static void ClearThenResetIfTooLarge(ref ArrayBuilder arrayBuilder) + { + arrayBuilder.Clear(); + + if (IsBufferTooLarge(arrayBuilder.Capacity)) + { + arrayBuilder = default; + } + } + + public static void ClearThenResetIfTooLarge(List list) + { + list.Clear(); + + if (IsBufferTooLarge(list.Capacity)) + { + list.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(Stack stack) + { + stack.Clear(); + + if (IsBufferTooLarge(stack.Count)) + { + stack.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) + where TKey : notnull + { + dictionary.Clear(); + + // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here + if (IsBufferTooLarge>(dictionary.Count)) + { +#if NET6_0_OR_GREATER + dictionary.TrimExcess(); +#else + dictionary = new Dictionary(); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsBufferTooLarge(int length) + => (long)Unsafe.SizeOf() * length > MaxKeptBufferSizeInBytes; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs index 0468d8f413..cb8168e693 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting } --_pendingReturnCount; - rentedList.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList); if (_size < MaxSize) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index c25d530472..bf9f6f77f8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -225,6 +225,9 @@ namespace Avalonia.Media.TextFormatting CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); + bidiData.Reset(); + bidiAlgorithm.Reset(); + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); for (var index = 0; index < processedRuns.Count; index++) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index e960a510a9..3a81784152 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -28,6 +28,11 @@ namespace Avalonia.Media.TextFormatting.Unicode /// internal sealed class BidiAlgorithm { + /// + /// Whether the state is clean and can be reused without a reset. + /// + private bool _hasCleanState = true; + /// /// The original BiDiClass classes as provided by the caller /// @@ -226,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs.Clear(); - _workingClassesBuffer.Clear(); - _levelRuns.Clear(); - _resolvedLevelsBuffer.Clear(); + Reset(); if (types.IsEmpty) { return; } + _hasCleanState = false; + // Setup original types and working types _originalClasses = types; _workingClasses = _workingClassesBuffer.Add(types); @@ -1639,6 +1643,47 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + /// + /// Resets the bidi algorithm to a clean state. + /// + public void Reset() + { + if (_hasCleanState) + { + return; + } + + _originalClasses = default; + _pairedBracketTypes = default; + _pairedBracketValues = default; + _hasBrackets = default; + _hasEmbeddings = default; + _hasIsolates = default; + _isolatePairs.ClearThenResetIfTooLarge(); + _workingClasses = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer); + _resolvedLevels = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer); + _paragraphEmbeddingLevel = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map); + FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings); + _runLevel = default; + _runDirection = default; + _runLength = default; + _runResolvedClasses = default; + _runOriginalClasses = default; + _runLevels = default; + _runBiDiPairedBracketTypes = default; + _runPairedBracketValues = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets); + + _hasCleanState = true; + } + /// /// Hold the start and end index of a pair of brackets /// diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 226e5ad6bd..8bd2171a41 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -14,6 +14,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// To avoid allocations, this class is designed to be reused. internal sealed class BidiData { + private bool _hasCleanState = true; private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; private ArrayBuilder _pairedBracketValues; @@ -62,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The text to process. public void Append(ReadOnlySpan text) { + _hasCleanState = false; + _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); _pairedBracketValues.Add(text.Length); @@ -183,12 +186,17 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void Reset() { - _classes.Clear(); - _pairedBracketTypes.Clear(); - _pairedBracketValues.Clear(); - _savedClasses.Clear(); - _savedPairedBracketTypes.Clear(); - _tempLevelBuffer.Clear(); + if (_hasCleanState) + { + return; + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer); ParagraphEmbeddingLevel = 0; HasBrackets = false; @@ -199,6 +207,8 @@ namespace Avalonia.Media.TextFormatting.Unicode Classes = default; PairedBracketTypes = default; PairedBracketValues = default; + + _hasCleanState = true; } } } diff --git a/src/Avalonia.Base/Utilities/BidiDictionary.cs b/src/Avalonia.Base/Utilities/BidiDictionary.cs index 654fbc9807..01af53ad89 100644 --- a/src/Avalonia.Base/Utilities/BidiDictionary.cs +++ b/src/Avalonia.Base/Utilities/BidiDictionary.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media.TextFormatting; namespace Avalonia.Utilities { @@ -9,32 +11,27 @@ namespace Avalonia.Utilities /// Value type internal sealed class BidiDictionary where T1 : notnull where T2 : notnull { - public Dictionary Forward { get; } = new Dictionary(); + private Dictionary _forward = new(); + private Dictionary _reverse = new(); - public Dictionary Reverse { get; } = new Dictionary(); - - public void Clear() + public void ClearThenResetIfTooLarge() { - Forward.Clear(); - Reverse.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _forward); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _reverse); } public void Add(T1 key, T2 value) { - Forward.Add(key, value); - Reverse.Add(value, key); + _forward.Add(key, value); + _reverse.Add(value, key); } -#pragma warning disable CS8601 - public bool TryGetValue(T1 key, out T2 value) => Forward.TryGetValue(key, out value); -#pragma warning restore CS8601 + public bool TryGetValue(T1 key, [MaybeNullWhen(false)] out T2 value) => _forward.TryGetValue(key, out value); -#pragma warning disable CS8601 - public bool TryGetKey(T2 value, out T1 key) => Reverse.TryGetValue(value, out key); -#pragma warning restore CS8601 + public bool TryGetKey(T2 value, [MaybeNullWhen(false)] out T1 key) => _reverse.TryGetValue(value, out key); - public bool ContainsKey(T1 key) => Forward.ContainsKey(key); + public bool ContainsKey(T1 key) => _forward.ContainsKey(key); - public bool ContainsValue(T2 value) => Reverse.ContainsKey(value); + public bool ContainsValue(T2 value) => _reverse.ContainsKey(value); } } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 8b23855fde..c96edbef5c 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -48,10 +48,10 @@ It may reveal how the matters of peculiar interest slowly the goals and objectiv In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; [Params(false, true)] - public bool UseWrapping { get; set; } + public bool Wrap { get; set; } [Params(false, true)] - public bool UseTrimming { get; set; } + public bool Trim { get; set; } [Benchmark] public TextLayout BuildTextLayout() => MakeLayout(Text); @@ -101,8 +101,8 @@ In respect that the structure of the sufficient amount poses problems and challe private TextLayout MakeLayout(string str) { - var wrapping = UseWrapping ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; - var trimming = UseTrimming ? TextTrimming.CharacterEllipsis : TextTrimming.None; + var wrapping = Wrap ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; + var trimming = Trim ? TextTrimming.CharacterEllipsis : TextTrimming.None; var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120, textTrimming: trimming, textWrapping: wrapping); layout.Dispose();