committed by
GitHub
49 changed files with 1569 additions and 1273 deletions
@ -0,0 +1,268 @@ |
|||||
|
using System; |
||||
|
using System.Diagnostics; |
||||
|
using Avalonia.Utilities; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Reorders text runs according to their bidi level.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>To avoid allocations, this class is designed to be reused.</remarks>
|
||||
|
internal sealed class BidiReorderer |
||||
|
{ |
||||
|
[ThreadStatic] private static BidiReorderer? t_instance; |
||||
|
|
||||
|
private ArrayBuilder<OrderedBidiRun> _runs; |
||||
|
private ArrayBuilder<BidiRange> _ranges; |
||||
|
|
||||
|
public static BidiReorderer Instance |
||||
|
=> t_instance ??= new(); |
||||
|
|
||||
|
public void BidiReorder(Span<TextRun> textRuns, FlowDirection flowDirection) |
||||
|
{ |
||||
|
Debug.Assert(_runs.Length == 0); |
||||
|
Debug.Assert(_ranges.Length == 0); |
||||
|
|
||||
|
if (textRuns.IsEmpty) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
_runs.Add(textRuns.Length); |
||||
|
|
||||
|
// Build up the collection of ordered runs.
|
||||
|
for (var i = 0; i < textRuns.Length; i++) |
||||
|
{ |
||||
|
var textRun = textRuns[i]; |
||||
|
_runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection)); |
||||
|
|
||||
|
if (i > 0) |
||||
|
{ |
||||
|
_runs[i - 1].NextRunIndex = i; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Reorder them into visual order.
|
||||
|
var firstIndex = LinearReorder(); |
||||
|
|
||||
|
// Now perform a recursive reversal of each run.
|
||||
|
// From the highest level found in the text to the lowest odd level on each line, including intermediate levels
|
||||
|
// not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
|
||||
|
// https://unicode.org/reports/tr9/#L2
|
||||
|
sbyte max = 0; |
||||
|
var min = sbyte.MaxValue; |
||||
|
|
||||
|
for (var i = 0; i < textRuns.Length; i++) |
||||
|
{ |
||||
|
var level = GetRunBidiLevel(textRuns[i], flowDirection); |
||||
|
if (level > max) |
||||
|
{ |
||||
|
max = level; |
||||
|
} |
||||
|
|
||||
|
if ((level & 1) != 0 && level < min) |
||||
|
{ |
||||
|
min = level; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (min > max) |
||||
|
{ |
||||
|
min = max; |
||||
|
} |
||||
|
|
||||
|
if (max == 0 || (min == max && (max & 1) == 0)) |
||||
|
{ |
||||
|
// Nothing to reverse.
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// Now apply the reversal and replace the original contents.
|
||||
|
var minLevelToReverse = max; |
||||
|
int currentIndex; |
||||
|
|
||||
|
while (minLevelToReverse >= min) |
||||
|
{ |
||||
|
currentIndex = firstIndex; |
||||
|
|
||||
|
while (currentIndex >= 0) |
||||
|
{ |
||||
|
ref var current = ref _runs[currentIndex]; |
||||
|
if (current.Level >= minLevelToReverse && current.Level % 2 != 0) |
||||
|
{ |
||||
|
if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) |
||||
|
{ |
||||
|
shapedTextCharacters.Reverse(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
currentIndex = current.NextRunIndex; |
||||
|
} |
||||
|
|
||||
|
minLevelToReverse--; |
||||
|
} |
||||
|
|
||||
|
var index = 0; |
||||
|
|
||||
|
currentIndex = firstIndex; |
||||
|
while (currentIndex >= 0) |
||||
|
{ |
||||
|
ref var current = ref _runs[currentIndex]; |
||||
|
textRuns[index++] = current.Run; |
||||
|
|
||||
|
currentIndex = current.NextRunIndex; |
||||
|
} |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs); |
||||
|
FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) |
||||
|
{ |
||||
|
if (run is ShapedTextRun shapedTextRun) |
||||
|
{ |
||||
|
return shapedTextRun.BidiLevel; |
||||
|
} |
||||
|
|
||||
|
var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; |
||||
|
return (sbyte)defaultLevel; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Reorders the runs from logical to visual order.
|
||||
|
/// <see href="https://github.com/fribidi/linear-reorder/blob/f2f872257d4d8b8e137fcf831f254d6d4db79d3c/linear-reorder.c"/>
|
||||
|
/// </summary>
|
||||
|
/// <returns>The first run index in visual order.</returns>
|
||||
|
private int LinearReorder() |
||||
|
{ |
||||
|
var runIndex = 0; |
||||
|
var rangeIndex = -1; |
||||
|
|
||||
|
while (runIndex >= 0) |
||||
|
{ |
||||
|
ref var run = ref _runs[runIndex]; |
||||
|
var nextRunIndex = run.NextRunIndex; |
||||
|
|
||||
|
while (rangeIndex >= 0 |
||||
|
&& _ranges[rangeIndex].Level > run.Level |
||||
|
&& _ranges[rangeIndex].PreviousRangeIndex >= 0 |
||||
|
&& _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level) |
||||
|
{ |
||||
|
|
||||
|
rangeIndex = MergeRangeWithPrevious(rangeIndex); |
||||
|
} |
||||
|
|
||||
|
if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level) |
||||
|
{ |
||||
|
// Attach run to the range.
|
||||
|
if ((run.Level & 1) != 0) |
||||
|
{ |
||||
|
// Odd, range goes to the right of run.
|
||||
|
run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex; |
||||
|
_ranges[rangeIndex].LeftRunIndex = runIndex; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Even, range goes to the left of run.
|
||||
|
_runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex; |
||||
|
_ranges[rangeIndex].RightRunIndex = runIndex; |
||||
|
} |
||||
|
|
||||
|
_ranges[rangeIndex].Level = run.Level; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex); |
||||
|
_ranges.AddItem(r); |
||||
|
rangeIndex = _ranges.Length - 1; |
||||
|
} |
||||
|
|
||||
|
runIndex = nextRunIndex; |
||||
|
} |
||||
|
|
||||
|
while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0) |
||||
|
{ |
||||
|
rangeIndex = MergeRangeWithPrevious(rangeIndex); |
||||
|
} |
||||
|
|
||||
|
// Terminate.
|
||||
|
_runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1; |
||||
|
|
||||
|
return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex; |
||||
|
} |
||||
|
|
||||
|
private int MergeRangeWithPrevious(int index) |
||||
|
{ |
||||
|
var previousIndex = _ranges[index].PreviousRangeIndex; |
||||
|
ref var previous = ref _ranges[previousIndex]; |
||||
|
|
||||
|
int leftIndex; |
||||
|
int rightIndex; |
||||
|
|
||||
|
if ((previous.Level & 1) != 0) |
||||
|
{ |
||||
|
// Odd, previous goes to the right of range.
|
||||
|
leftIndex = index; |
||||
|
rightIndex = previousIndex; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
// Even, previous goes to the left of range.
|
||||
|
leftIndex = previousIndex; |
||||
|
rightIndex = index; |
||||
|
} |
||||
|
|
||||
|
// Stitch them
|
||||
|
ref var left = ref _ranges[leftIndex]; |
||||
|
ref var right = ref _ranges[rightIndex]; |
||||
|
_runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex; |
||||
|
previous.LeftRunIndex = left.LeftRunIndex; |
||||
|
previous.RightRunIndex = right.RightRunIndex; |
||||
|
|
||||
|
return previousIndex; |
||||
|
} |
||||
|
|
||||
|
private struct OrderedBidiRun |
||||
|
{ |
||||
|
public OrderedBidiRun(int runIndex, TextRun run, sbyte level) |
||||
|
{ |
||||
|
RunIndex = runIndex; |
||||
|
Run = run; |
||||
|
Level = level; |
||||
|
NextRunIndex = -1; |
||||
|
} |
||||
|
|
||||
|
public int RunIndex { get; } |
||||
|
|
||||
|
public sbyte Level { get; } |
||||
|
|
||||
|
public TextRun Run { get; } |
||||
|
|
||||
|
public int NextRunIndex { get; set; } // -1 if none
|
||||
|
} |
||||
|
|
||||
|
private struct BidiRange |
||||
|
{ |
||||
|
public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex) |
||||
|
{ |
||||
|
Level = level; |
||||
|
LeftRunIndex = leftRunIndex; |
||||
|
RightRunIndex = rightRunIndex; |
||||
|
PreviousRangeIndex = previousRangeIndex; |
||||
|
} |
||||
|
|
||||
|
public sbyte Level { get; set; } |
||||
|
|
||||
|
public int LeftRunIndex { get; set; } |
||||
|
|
||||
|
public int RightRunIndex { get; set; } |
||||
|
|
||||
|
public int PreviousRangeIndex { get; } // -1 if none
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,84 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using System.Numerics; |
||||
|
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>((uint) arrayBuilder.Capacity)) |
||||
|
{ |
||||
|
arrayBuilder = default; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void ClearThenResetIfTooLarge<T>(List<T> list) |
||||
|
{ |
||||
|
list.Clear(); |
||||
|
|
||||
|
if (IsBufferTooLarge<T>((uint) list.Capacity)) |
||||
|
{ |
||||
|
list.TrimExcess(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void ClearThenResetIfTooLarge<T>(Stack<T> stack) |
||||
|
{ |
||||
|
var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count); |
||||
|
|
||||
|
stack.Clear(); |
||||
|
|
||||
|
if (IsBufferTooLarge<T>(approximateCapacity)) |
||||
|
{ |
||||
|
stack.TrimExcess(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static void ClearThenResetIfTooLarge<TKey, TValue>(ref Dictionary<TKey, TValue> dictionary) |
||||
|
where TKey : notnull |
||||
|
{ |
||||
|
var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count); |
||||
|
|
||||
|
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>>(approximateCapacity)) |
||||
|
{ |
||||
|
#if NET6_0_OR_GREATER
|
||||
|
dictionary.TrimExcess(); |
||||
|
#else
|
||||
|
dictionary = new Dictionary<TKey, TValue>(); |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private static bool IsBufferTooLarge<T>(uint capacity) |
||||
|
=> (long) (uint) Unsafe.SizeOf<T>() * capacity > MaxKeptBufferSizeInBytes; |
||||
|
|
||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||
|
private static uint RoundUpToPowerOf2(uint value) |
||||
|
{ |
||||
|
#if NET6_0_OR_GREATER
|
||||
|
return BitOperations.RoundUpToPowerOf2(value); |
||||
|
#else
|
||||
|
// Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
|
||||
|
--value; |
||||
|
value |= value >> 1; |
||||
|
value |= value >> 2; |
||||
|
value |= value >> 4; |
||||
|
value |= value >> 8; |
||||
|
value |= value >> 16; |
||||
|
return value + 1; |
||||
|
#endif
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,135 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// <para>Contains various list pools that are commonly used during text layout.</para>
|
||||
|
/// <para>
|
||||
|
/// This class provides an instance per thread.
|
||||
|
/// In most applications, there'll be only one instance: on the UI thread, which is responsible for layout.
|
||||
|
/// </para>
|
||||
|
/// </summary>
|
||||
|
/// <seealso cref="RentedList{T}"/>
|
||||
|
internal sealed class FormattingObjectPool |
||||
|
{ |
||||
|
[ThreadStatic] private static FormattingObjectPool? t_instance; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets an instance of this class for the current thread.
|
||||
|
/// </summary>
|
||||
|
/// <remarks>
|
||||
|
/// Since this is backed by a thread static field which is slower than a normal static field,
|
||||
|
/// prefer passing the instance around when possible instead of calling this property each time.
|
||||
|
/// </remarks>
|
||||
|
public static FormattingObjectPool Instance |
||||
|
=> t_instance ??= new(); |
||||
|
|
||||
|
public ListPool<TextRun> TextRunLists { get; } = new(); |
||||
|
|
||||
|
public ListPool<UnshapedTextRun> UnshapedTextRunLists { get; } = new(); |
||||
|
|
||||
|
public ListPool<TextLine> TextLines { get; } = new(); |
||||
|
|
||||
|
[Conditional("DEBUG")] |
||||
|
public void VerifyAllReturned() |
||||
|
{ |
||||
|
TextRunLists.VerifyAllReturned(); |
||||
|
UnshapedTextRunLists.VerifyAllReturned(); |
||||
|
TextLines.VerifyAllReturned(); |
||||
|
} |
||||
|
|
||||
|
internal sealed class ListPool<T> |
||||
|
{ |
||||
|
// we don't need a big number here, these are for temporary usages only which should quickly be returned
|
||||
|
private const int MaxSize = 16; |
||||
|
|
||||
|
private readonly RentedList<T>[] _lists = new RentedList<T>[MaxSize]; |
||||
|
private int _size; |
||||
|
private int _pendingReturnCount; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Rents a list.
|
||||
|
/// See <see cref="RentedList{T}"/> for the intended usages.
|
||||
|
/// </summary>
|
||||
|
/// <returns>A rented list instance that must be returned to the pool.</returns>
|
||||
|
/// <seealso cref="RentedList{T}"/>
|
||||
|
public RentedList<T> Rent() |
||||
|
{ |
||||
|
var list = _size > 0 ? _lists[--_size] : new RentedList<T>(); |
||||
|
|
||||
|
Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!"); |
||||
|
|
||||
|
++_pendingReturnCount; |
||||
|
return list; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Returns a rented list to the pool.
|
||||
|
/// </summary>
|
||||
|
/// <param name="rentedList">
|
||||
|
/// On input, the list to return.
|
||||
|
/// On output, the reference is set to null to avoid misuse.
|
||||
|
/// </param>
|
||||
|
public void Return(ref RentedList<T>? rentedList) |
||||
|
{ |
||||
|
if (rentedList is null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
--_pendingReturnCount; |
||||
|
FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList); |
||||
|
|
||||
|
if (_size < MaxSize) |
||||
|
{ |
||||
|
_lists[_size++] = rentedList; |
||||
|
} |
||||
|
|
||||
|
rentedList = null; |
||||
|
} |
||||
|
|
||||
|
[Conditional("DEBUG")] |
||||
|
public void VerifyAllReturned() |
||||
|
{ |
||||
|
if (_pendingReturnCount > 0) |
||||
|
{ |
||||
|
throw new InvalidOperationException( |
||||
|
$"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!"); |
||||
|
} |
||||
|
|
||||
|
if (_pendingReturnCount < 0) |
||||
|
{ |
||||
|
throw new InvalidOperationException( |
||||
|
$"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!"); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// <para>Represents a list that has been rented through <see cref="FormattingObjectPool"/>.</para>
|
||||
|
/// <para>
|
||||
|
/// This class can be used when a temporary list is needed to store some items during text layout.
|
||||
|
/// It can also be used as a reusable array builder by calling <see cref="List{T}.ToArray"/> when done.
|
||||
|
/// </para>
|
||||
|
/// <list type="bullet">
|
||||
|
/// <item>NEVER use an instance of this type after it's been returned to the pool.</item>
|
||||
|
/// <item>AVOID storing an instance of this type into a field or property.</item>
|
||||
|
/// <item>AVOID casting an instance of this type to another type.</item>
|
||||
|
/// <item>
|
||||
|
/// AVOID passing an instance of this type as an argument to a method expecting a standard list,
|
||||
|
/// unless you're absolutely sure it won't store it.
|
||||
|
/// </item>
|
||||
|
/// <item>
|
||||
|
/// If you call a method returning an instance of this type,
|
||||
|
/// you're now responsible for returning it to the pool.
|
||||
|
/// </item>
|
||||
|
/// </list>
|
||||
|
/// </summary>
|
||||
|
/// <typeparam name="T">The type of elements in the list.</typeparam>
|
||||
|
internal sealed class RentedList<T> : List<T> |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,36 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Media.TextFormatting |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a single glyph.
|
||||
|
/// </summary>
|
||||
|
public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default) |
||||
|
{ |
||||
|
internal static Comparer<GlyphInfo> ClusterAscendingComparer { get; } = |
||||
|
Comparer<GlyphInfo>.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster)); |
||||
|
|
||||
|
internal static Comparer<GlyphInfo> ClusterDescendingComparer { get; } = |
||||
|
Comparer<GlyphInfo>.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the glyph index.
|
||||
|
/// </summary>
|
||||
|
public ushort GlyphIndex { get; } = GlyphIndex; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the glyph cluster.
|
||||
|
/// </summary>
|
||||
|
public int GlyphCluster { get; } = GlyphCluster; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the glyph advance.
|
||||
|
/// </summary>
|
||||
|
public double GlyphAdvance { get; } = GlyphAdvance; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Get the glyph offset.
|
||||
|
/// </summary>
|
||||
|
public Vector GlyphOffset { get; } = GlyphOffset; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,151 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Reflection; |
||||
|
using Avalonia.Media.TextFormatting; |
||||
|
using Avalonia.Utilities; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Media.TextFormatting |
||||
|
{ |
||||
|
public class FormattingBufferHelperTests |
||||
|
{ |
||||
|
public static TheoryData<int> SmallSizes => new() { 1, 500, 10_000, 125_000 }; |
||||
|
public static TheoryData<int> LargeSizes => new() { 500_000, 1_000_000 }; |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(SmallSizes))] |
||||
|
public void Should_Keep_Small_Buffer_List(int itemCount) |
||||
|
{ |
||||
|
var capacity = FillAndClearList(itemCount); |
||||
|
|
||||
|
Assert.True(capacity >= itemCount); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(LargeSizes))] |
||||
|
public void Should_Reset_Large_Buffer_List(int itemCount) |
||||
|
{ |
||||
|
var capacity = FillAndClearList(itemCount); |
||||
|
|
||||
|
Assert.Equal(0, capacity); |
||||
|
} |
||||
|
|
||||
|
private static int FillAndClearList(int itemCount) |
||||
|
{ |
||||
|
var list = new List<int>(); |
||||
|
|
||||
|
for (var i = 0; i < itemCount; ++i) |
||||
|
{ |
||||
|
list.Add(i); |
||||
|
} |
||||
|
|
||||
|
FormattingBufferHelper.ClearThenResetIfTooLarge(list); |
||||
|
|
||||
|
return list.Capacity; |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(SmallSizes))] |
||||
|
public void Should_Keep_Small_Buffer_ArrayBuilder(int itemCount) |
||||
|
{ |
||||
|
var capacity = FillAndClearArrayBuilder(itemCount); |
||||
|
|
||||
|
Assert.True(capacity >= itemCount); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(LargeSizes))] |
||||
|
public void Should_Reset_Large_Buffer_ArrayBuilder(int itemCount) |
||||
|
{ |
||||
|
var capacity = FillAndClearArrayBuilder(itemCount); |
||||
|
|
||||
|
Assert.Equal(0, capacity); |
||||
|
} |
||||
|
|
||||
|
private static int FillAndClearArrayBuilder(int itemCount) |
||||
|
{ |
||||
|
var arrayBuilder = new ArrayBuilder<int>(); |
||||
|
|
||||
|
for (var i = 0; i < itemCount; ++i) |
||||
|
{ |
||||
|
arrayBuilder.AddItem(i); |
||||
|
} |
||||
|
|
||||
|
FormattingBufferHelper.ClearThenResetIfTooLarge(ref arrayBuilder); |
||||
|
|
||||
|
return arrayBuilder.Capacity; |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(SmallSizes))] |
||||
|
public void Should_Keep_Small_Buffer_Stack(int itemCount) |
||||
|
{ |
||||
|
var capacity = FillAndClearStack(itemCount); |
||||
|
|
||||
|
Assert.True(capacity >= itemCount); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(LargeSizes))] |
||||
|
public void Should_Reset_Large_Buffer_Stack(int itemCount) |
||||
|
{ |
||||
|
var capacity = FillAndClearStack(itemCount); |
||||
|
|
||||
|
Assert.Equal(0, capacity); |
||||
|
} |
||||
|
|
||||
|
private static int FillAndClearStack(int itemCount) |
||||
|
{ |
||||
|
var stack = new Stack<int>(); |
||||
|
|
||||
|
for (var i = 0; i < itemCount; ++i) |
||||
|
{ |
||||
|
stack.Push(i); |
||||
|
} |
||||
|
|
||||
|
FormattingBufferHelper.ClearThenResetIfTooLarge(stack); |
||||
|
|
||||
|
var array = (Array) stack.GetType() |
||||
|
.GetField("_array", BindingFlags.NonPublic | BindingFlags.Instance)! |
||||
|
.GetValue(stack)!; |
||||
|
|
||||
|
return array.Length; |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(SmallSizes))] |
||||
|
public void Should_Keep_Small_Buffer_Dictionary(int itemCount) |
||||
|
{ |
||||
|
var capacity = FillAndClearDictionary(itemCount); |
||||
|
|
||||
|
Assert.True(capacity >= itemCount); |
||||
|
} |
||||
|
|
||||
|
[Theory] |
||||
|
[MemberData(nameof(LargeSizes))] |
||||
|
public void Should_Reset_Large_Buffer_Dictionary(int itemCount) |
||||
|
{ |
||||
|
var capacity = FillAndClearDictionary(itemCount); |
||||
|
|
||||
|
Assert.True(capacity <= 3); // dictionary trims to the nearest prime starting with 3
|
||||
|
} |
||||
|
|
||||
|
private static int FillAndClearDictionary(int itemCount) |
||||
|
{ |
||||
|
var dictionary = new Dictionary<int, int>(); |
||||
|
|
||||
|
for (var i = 0; i < itemCount; ++i) |
||||
|
{ |
||||
|
dictionary.Add(i, i); |
||||
|
} |
||||
|
|
||||
|
FormattingBufferHelper.ClearThenResetIfTooLarge(ref dictionary); |
||||
|
|
||||
|
var array = (Array) dictionary.GetType() |
||||
|
.GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)! |
||||
|
.GetValue(dictionary)!; |
||||
|
|
||||
|
return array.Length; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
Loading…
Reference in new issue