Browse Source

Don't keep the text layout buffers around if they're too large

pull/10013/head
Julien Lebosquain 3 years ago
parent
commit
89a78f557b
  1. 4
      src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs
  2. 62
      src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs
  3. 2
      src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs
  4. 3
      src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs
  5. 53
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  6. 22
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs
  7. 29
      src/Avalonia.Base/Utilities/BidiDictionary.cs
  8. 8
      tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs

4
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);
}
}

62
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<T>(ref ArrayBuilder<T> arrayBuilder)
{
arrayBuilder.Clear();
if (IsBufferTooLarge<T>(arrayBuilder.Capacity))
{
arrayBuilder = default;
}
}
public static void ClearThenResetIfTooLarge<T>(List<T> list)
{
list.Clear();
if (IsBufferTooLarge<T>(list.Capacity))
{
list.TrimExcess();
}
}
public static void ClearThenResetIfTooLarge<T>(Stack<T> stack)
{
stack.Clear();
if (IsBufferTooLarge<T>(stack.Count))
{
stack.TrimExcess();
}
}
public static void ClearThenResetIfTooLarge<TKey, TValue>(ref Dictionary<TKey, TValue> 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<KeyValuePair<TKey, TValue>>(dictionary.Count))
{
#if NET6_0_OR_GREATER
dictionary.TrimExcess();
#else
dictionary = new Dictionary<TKey, TValue>();
#endif
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsBufferTooLarge<T>(int length)
=> (long)Unsafe.SizeOf<T>() * length > MaxKeptBufferSizeInBytes;
}
}

2
src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs

@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting
}
--_pendingReturnCount;
rentedList.Clear();
FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList);
if (_size < MaxSize)
{

3
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++)

53
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@ -28,6 +28,11 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </remarks>
internal sealed class BidiAlgorithm
{
/// <summary>
/// Whether the state is clean and can be reused without a reset.
/// </summary>
private bool _hasCleanState = true;
/// <summary>
/// The original BiDiClass classes as provided by the caller
/// </summary>
@ -226,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode
ArraySlice<sbyte>? 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
}
}
/// <summary>
/// Resets the bidi algorithm to a clean state.
/// </summary>
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;
}
/// <summary>
/// Hold the start and end index of a pair of brackets
/// </summary>

22
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs

@ -14,6 +14,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
internal sealed class BidiData
{
private bool _hasCleanState = true;
private ArrayBuilder<BidiClass> _classes;
private ArrayBuilder<BidiPairedBracketType> _pairedBracketTypes;
private ArrayBuilder<int> _pairedBracketValues;
@ -62,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <param name="text">The text to process.</param>
public void Append(ReadOnlySpan<char> text)
{
_hasCleanState = false;
_classes.Add(text.Length);
_pairedBracketTypes.Add(text.Length);
_pairedBracketValues.Add(text.Length);
@ -183,12 +186,17 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// </summary>
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;
}
}
}

29
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
/// <typeparam name="T2">Value type</typeparam>
internal sealed class BidiDictionary<T1, T2> where T1 : notnull where T2 : notnull
{
public Dictionary<T1, T2> Forward { get; } = new Dictionary<T1, T2>();
private Dictionary<T1, T2> _forward = new();
private Dictionary<T2, T1> _reverse = new();
public Dictionary<T2, T1> Reverse { get; } = new Dictionary<T2, T1>();
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);
}
}

8
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();

Loading…
Cancel
Save