committed by
GitHub
93 changed files with 1775 additions and 1402 deletions
@ -0,0 +1,28 @@ |
|||
using System.Collections.Generic; |
|||
|
|||
namespace Avalonia.Data.Core.Plugins |
|||
{ |
|||
/// <summary>
|
|||
/// Holds a registry of plugins used for bindings.
|
|||
/// </summary>
|
|||
public static class BindingPlugins |
|||
{ |
|||
/// <summary>
|
|||
/// An ordered collection of property accessor plugins that can be used to customize
|
|||
/// the reading and subscription of property values on a type.
|
|||
/// </summary>
|
|||
public static IList<IPropertyAccessorPlugin> PropertyAccessors => ExpressionObserver.PropertyAccessors; |
|||
|
|||
/// <summary>
|
|||
/// An ordered collection of validation checker plugins that can be used to customize
|
|||
/// the validation of view model and model data.
|
|||
/// </summary>
|
|||
public static IList<IDataValidationPlugin> DataValidators => ExpressionObserver.DataValidators; |
|||
|
|||
/// <summary>
|
|||
/// An ordered collection of stream plugins that can be used to customize the behavior
|
|||
/// of the '^' stream binding operator.
|
|||
/// </summary>
|
|||
public static IList<IStreamPlugin> StreamHandlers => ExpressionObserver.StreamHandlers; |
|||
} |
|||
} |
|||
@ -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