A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

149 lines
4.8 KiB

#nullable enable
using System;
using System.Linq;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.UnitTests;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Text;
/// <summary>
/// Micro-benchmark for the <see cref="ShapedBuffer"/> cluster-cache hot paths
/// (<see cref="ShapedBuffer.TotalGlyphAdvance"/>, <see cref="ShapedBuffer.FindLeadingCharCountWithinWidth"/>,
/// and the cached <see cref="ShapedBuffer.Split"/> chain). Compares the
/// simple-mode fast path (1 char per cluster) against complex clusters by
/// shaping random ASCII vs. random-from-extended-Latin so the buffers exercise
/// different code paths. Pair with <c>--memory</c> to see allocation impact of
/// the <see cref="System.Buffers.ArrayPool{T}"/>-backed cluster cache.
/// </summary>
[MemoryDiagnoser]
[MinIterationTime(150)]
[MaxWarmupCount(10)]
public class ShapedBufferOps : IDisposable
{
private readonly IDisposable _app;
private readonly TextShaperOptions _options;
private string _text = string.Empty;
private ShapedBuffer? _primed;
public ShapedBufferOps()
{
_app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface);
_options = new TextShaperOptions(Typeface.Default.GlyphTypeface);
}
[Params(8, 32, 128, 512, 2048)]
public int GlyphCount { get; set; }
[GlobalSetup]
public void Setup()
{
// ASCII text → simple-mode fast path (one glyph == one cluster == one char).
var rng = new Random(GlyphCount);
const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 ";
_text = new string(Enumerable.Range(0, GlyphCount).Select(_ => alphabet[rng.Next(alphabet.Length)]).ToArray());
_primed?.Dispose();
_primed = TextShaper.Current.ShapeText(_text, _options);
// Prime the cluster cache once so the per-call benchmarks measure only the lookup cost.
_ = _primed.TotalGlyphAdvance;
}
/// <summary>
/// Cost of shaping + building the cluster cache from scratch. Captures both
/// the HarfBuzz call and the prefix-sum allocation/initialisation.
/// </summary>
[Benchmark]
public double ShapeAndPrime()
{
using var buffer = TextShaper.Current.ShapeText(_text, _options);
return buffer.TotalGlyphAdvance;
}
/// <summary>
/// Repeated <see cref="ShapedBuffer.TotalGlyphAdvance"/> on a primed buffer.
/// Should be ~O(1) per call regardless of glyph count.
/// </summary>
[Benchmark]
public double TotalAdvance_Cached()
{
var sum = 0d;
for (var i = 0; i < 64; i++)
{
sum += _primed!.TotalGlyphAdvance;
}
return sum;
}
/// <summary>
/// Repeated <see cref="ShapedBuffer.FindLeadingCharCountWithinWidth"/> targeting
/// half the buffer's total width. Exercises the binary search across the
/// prefix table.
/// </summary>
[Benchmark]
public int MeasureFit_Cached()
{
var halfWidth = _primed!.TotalGlyphAdvance * 0.5;
var sum = 0;
for (var i = 0; i < 64; i++)
{
sum += _primed.FindLeadingCharCountWithinWidth(halfWidth);
}
return sum;
}
/// <summary>
/// Splits the primed buffer at three positions and queries the resulting
/// children — the workload the shared cluster cache is designed for. Each
/// child should reuse the parent cache in O(1)/O(log) instead of rebuilding.
/// </summary>
[Benchmark]
public double SplitChain()
{
var quarter = _primed!.Text.Length / 4;
ShapedBuffer? first = null;
ShapedBuffer? second = null;
ShapedBuffer? firstFirst = null;
ShapedBuffer? firstSecond = null;
ShapedBuffer? secondFirst = null;
ShapedBuffer? secondSecond = null;
try
{
var halves = _primed.Split(quarter * 2);
first = halves.First!;
second = halves.Second!;
var firstSplit = first.Split(quarter);
var secondSplit = second.Split(quarter);
firstFirst = firstSplit.First;
firstSecond = firstSplit.Second;
secondFirst = secondSplit.First;
secondSecond = secondSplit.Second;
var total = (firstFirst?.TotalGlyphAdvance ?? 0)
+ (firstSecond?.TotalGlyphAdvance ?? 0)
+ (secondFirst?.TotalGlyphAdvance ?? 0)
+ (secondSecond?.TotalGlyphAdvance ?? 0);
return total;
}
finally
{
firstFirst?.Dispose();
firstSecond?.Dispose();
secondFirst?.Dispose();
secondSecond?.Dispose();
first?.Dispose();
second?.Dispose();
}
}
public void Dispose()
{
_primed?.Dispose();
_app.Dispose();
}
}