diff --git a/CSharpMath.Avalonia/CSharpMath.Avalonia.csproj b/CSharpMath.Avalonia/CSharpMath.Avalonia.csproj index 79b5c4e5..9046f8bc 100644 --- a/CSharpMath.Avalonia/CSharpMath.Avalonia.csproj +++ b/CSharpMath.Avalonia/CSharpMath.Avalonia.csproj @@ -6,6 +6,7 @@ $(PackageTags) avalonia $(DefineConstants) Avalonia + diff --git a/CSharpMath.Maui/CSharpMath.Maui.csproj b/CSharpMath.Maui/CSharpMath.Maui.csproj index bed8856a..207cda7c 100644 --- a/CSharpMath.Maui/CSharpMath.Maui.csproj +++ b/CSharpMath.Maui/CSharpMath.Maui.csproj @@ -1,6 +1,6 @@  - + net10.0;net10.0-android $(TargetFrameworks);net10.0-ios;net10.0-maccatalyst $(TargetFrameworks);net10.0-windows10.0.19041.0 @@ -18,7 +18,8 @@ $(PackageTags) maui $(DefineConstants) Maui - + + diff --git a/CSharpMath.Rendering.Benchmarks/CSharpMath.Rendering.Benchmarks.csproj b/CSharpMath.Rendering.Benchmarks/CSharpMath.Rendering.Benchmarks.csproj index dfa64d71..7a1561d0 100644 --- a/CSharpMath.Rendering.Benchmarks/CSharpMath.Rendering.Benchmarks.csproj +++ b/CSharpMath.Rendering.Benchmarks/CSharpMath.Rendering.Benchmarks.csproj @@ -9,6 +9,9 @@ + + + - \ No newline at end of file + diff --git a/CSharpMath.Rendering.Benchmarks/Program.cs b/CSharpMath.Rendering.Benchmarks/Program.cs index cbd0371b..d6f39b29 100644 --- a/CSharpMath.Rendering.Benchmarks/Program.cs +++ b/CSharpMath.Rendering.Benchmarks/Program.cs @@ -1,4 +1,8 @@ +using Avalonia; +using Avalonia.Headless; using BenchmarkDotNet.Attributes; +using CSharpMath.Atom; +using CSharpMath.SkiaSharp; namespace CSharpMath.Rendering.Benchmarks { using SkiaSharp; using Data = Tests.TestRenderingMathData; @@ -41,4 +45,73 @@ static void Main(string[] args) { #endif } } -} \ No newline at end of file + + [MemoryDiagnoser] + public class ViewUpdateChurnBenchmarks { + const string Sample = @"x_1 = 1.234e50"; + [Params(100, 1000)] public int Iterations; + readonly MathPainter painter = new(); + readonly MathList content = new MathPainter { LaTeX = Sample }.Content!; + + [Benchmark] + public void LaTeXAssignment() { + for (var i = 0; i < Iterations; i++) { + painter.LaTeX = Sample; + painter.Measure(1000); + } + } + + [Benchmark] + public void ContentAssignment() { + for (var i = 0; i < Iterations; i++) { + painter.Content = content; + painter.Measure(1000); + } + } + + [Benchmark] + public void DirectPainterReuse() { + painter.LaTeX = Sample; + for (var i = 0; i < Iterations; i++) painter.Measure(1000); + } + } + + [MemoryDiagnoser] + public class AvaloniaViewUpdateChurnBenchmarks { + const string FirstSample = @"x_1 = 1.234e50"; + const string SecondSample = @"x_2 = 5.678e90"; + [Params(100, 1000)] public int Iterations; + static AvaloniaViewUpdateChurnBenchmarks() { + _ = CSharpMath.Rendering.BackEnd.Fonts.GlobalTypefaces.ToString(); + AppBuilder.Configure() + .UseHeadless(new AvaloniaHeadlessPlatformOptions()) + .SetupWithoutStarting(); + } + sealed class BenchmarkApplication : Application { } + readonly CSharpMath.Avalonia.MathView view = new(); + readonly MathList firstContent = new MathPainter { LaTeX = FirstSample }.Content!; + readonly MathList secondContent = new MathPainter { LaTeX = SecondSample }.Content!; + + [Benchmark] + public void ViewLaTeXAssignment() { + for (var i = 0; i < Iterations; i++) { + view.LaTeX = i % 2 == 0 ? FirstSample : SecondSample; + view.Measure(new Size(1000, 1000)); + } + } + + [Benchmark] + public void ViewContentAssignment() { + for (var i = 0; i < Iterations; i++) { + view.Content = i % 2 == 0 ? firstContent : secondContent; + view.Measure(new Size(1000, 1000)); + } + } + + [Benchmark] + public void DirectPainterReuse() { + view.Painter.LaTeX = FirstSample; + for (var i = 0; i < Iterations; i++) view.Painter.Measure(1000); + } + } +} diff --git a/CSharpMath.Rendering/FrontEnd/Painter.cs b/CSharpMath.Rendering/FrontEnd/Painter.cs index 02acaa6f..4518c503 100644 --- a/CSharpMath.Rendering/FrontEnd/Painter.cs +++ b/CSharpMath.Rendering/FrontEnd/Painter.cs @@ -50,7 +50,7 @@ public Painter() { Atom.LineStyle __style = Atom.LineStyle.Display; public Atom.LineStyle LineStyle { get => __style; set { __style = value; SetRedisplay(); } } TContent? __content; - public TContent? Content { get => __content; set { __content = value; SetRedisplay(); } } + public TContent? Content { get => __content; set { __content = value; ErrorMessage = null; SetRedisplay(); } } public string? LaTeX { get => Content is null ? "" : ContentToLaTeX(Content); set => (Content, ErrorMessage) = LaTeXToContent(value ?? ""); } #endregion Redisplaying properties @@ -132,4 +132,4 @@ GlyphBoxColor is var (glyph, textRun) ? Nullable((WrapColor(glyph), WrapColor(te public Painter ShallowClone() => (Painter)MemberwiseClone(); #endregion Methods } -} \ No newline at end of file +} diff --git a/CSharpMath.Uno/CSharpMath.Uno.csproj b/CSharpMath.Uno/CSharpMath.Uno.csproj index 50d7416f..8f77c4ae 100644 --- a/CSharpMath.Uno/CSharpMath.Uno.csproj +++ b/CSharpMath.Uno/CSharpMath.Uno.csproj @@ -10,6 +10,7 @@ $(DefineConstants) Uno + diff --git a/CSharpMath.Xaml.Tests/CSharpMath.Xaml.Tests.csproj b/CSharpMath.Xaml.Tests/CSharpMath.Xaml.Tests.csproj index 6d43aaf7..c2d39500 100644 --- a/CSharpMath.Xaml.Tests/CSharpMath.Xaml.Tests.csproj +++ b/CSharpMath.Xaml.Tests/CSharpMath.Xaml.Tests.csproj @@ -4,5 +4,6 @@ + - \ No newline at end of file + diff --git a/CSharpMath.Xaml.Tests/Test.cs b/CSharpMath.Xaml.Tests/Test.cs index e0689fdf..3cf83149 100644 --- a/CSharpMath.Xaml.Tests/Test.cs +++ b/CSharpMath.Xaml.Tests/Test.cs @@ -11,6 +11,10 @@ namespace CSharpMath.Xaml.Tests { using Rendering.BackEnd; using Rendering.FrontEnd; using Rendering.Text; + interface ITestDiagnostics { + void Reset(); + int Count(string name); + } public abstract class Test where TBindingMode : struct, Enum where TMathView : TBaseView, ICSharpMathAPI, new() @@ -32,8 +36,18 @@ void SetAndRaise(ref T field, T value, [System.Runtime.CompilerServices.Calle protected abstract TBindingMode OneWayToSource { get; } protected abstract TBindingMode TwoWay { get; } protected abstract TView ParseFromXaml(string xaml) where TView : TBaseView, new(); + protected abstract TBaseView CreateInstrumentedMathView(); protected abstract void SetBindingContext(TBaseView view, object viewModel); protected abstract IDisposable SetBinding(TBaseView view, TProperty property, string viewModelProperty, TBindingMode bindingMode); + static void AssertUpdateCounts(TBaseView view, bool contentSource, Action update) { + var diagnostics = Assert.IsAssignableFrom(view); + diagnostics.Reset(); + update(); + Assert.Equal(contentSource ? 1 : 0, diagnostics.Count("ContentToLaTeX")); + Assert.Equal(contentSource ? 0 : 1, diagnostics.Count("LaTeXToContent")); + Assert.Equal(1, diagnostics.Count("MeasureInvalidation")); + Assert.Equal(1, diagnostics.Count("VisualInvalidation")); + } IDisposable SetBinding(TView view, string propertyName, TBindingMode? bindingMode = null) where TView : TBaseView => SetBinding(view, (TProperty)typeof(TView) @@ -52,6 +66,9 @@ static void Test(TView view, TContent content) } Test(new TMathView(), new MathList(new Atom.Atoms.Number("1"))); Test(new TTextView(), (TextAtom)new TextAtom.Text("1")); + var countedView = CreateInstrumentedMathView(); + var counted = Assert.IsAssignableFrom>(countedView); + AssertUpdateCounts(countedView, false, () => counted.LaTeX = "1"); } [Fact] public void ContentUpdatesLaTeX() { @@ -64,6 +81,33 @@ static void Test(TView view, TContent content) } Test(new TMathView(), new MathList(new Atom.Atoms.Number("1"))); Test(new TTextView(), (TextAtom)new TextAtom.Text("1")); + var countedView = CreateInstrumentedMathView(); + var counted = Assert.IsAssignableFrom>(countedView); + AssertUpdateCounts(countedView, true, () => counted.Content = new MathList(new Atom.Atoms.Number("1"))); + } + [Fact] + public void RepeatedContentUpdatesRemainNormalized() { + var view = new TMathView(); + for (var i = 0; i < 8; i++) { + view.Content = new MathList(new Atom.Atoms.Number((i + 1).ToString())); + Assert.Equal((i + 1).ToString(), view.LaTeX); + Assert.Null(view.ErrorMessage); + } + var text = new TTextView(); + for (var i = 0; i < 8; i++) { + text.LaTeX = "a b"; + Assert.NotNull(text.Content); + Assert.Null(text.ErrorMessage); + } + } + [Fact] + public void ValidContentClearsPreviousError() { + var view = new TMathView(); + view.LaTeX = @"\missingcommand"; + Assert.NotNull(view.ErrorMessage); + view.Content = new MathList(new Atom.Atoms.Number("1")); + Assert.Null(view.ErrorMessage); + Assert.Equal("1", view.LaTeX); } [Fact] public void ContentIsBindable() { @@ -259,4 +303,4 @@ public void LaTeXIsContent() { Assert.Equal(new TextAtom.List(new TextAtom[] { new TextAtom.Text("a"), new TextAtom.ControlSpace(), new TextAtom.Text("b") }), text.Content); } } -} \ No newline at end of file +} diff --git a/CSharpMath.Xaml.Tests/TestAvalonia.cs b/CSharpMath.Xaml.Tests/TestAvalonia.cs index ed23d512..60419c22 100644 --- a/CSharpMath.Xaml.Tests/TestAvalonia.cs +++ b/CSharpMath.Xaml.Tests/TestAvalonia.cs @@ -9,6 +9,12 @@ namespace CSharpMath.Xaml.Tests { using Avalonia; + public class InstrumentedMathView : MathView, ITestDiagnostics { + readonly Dictionary counts = new(); + internal override void OnDiagnostic(string name) => counts[name] = counts.GetValueOrDefault(name) + 1; + public void Reset() => counts.Clear(); + public int Count(string name) => counts.GetValueOrDefault(name); + } public class TestAvalonia : Test { protected override Display.IDisplay GetDisplay(Control control) { @@ -27,10 +33,11 @@ public class TestAvalonia protected override BindingMode Default => BindingMode.Default; protected override BindingMode OneWayToSource => BindingMode.OneWayToSource; protected override BindingMode TwoWay => BindingMode.TwoWay; + protected override Control CreateInstrumentedMathView() => new InstrumentedMathView(); protected override TView ParseFromXaml(string xaml) => AvaloniaRuntimeXamlLoader.Parse(xaml); protected override IDisposable SetBinding(Control view, AvaloniaProperty property, string viewModelProperty, BindingMode bindingMode) => view.Bind(property, new Binding(viewModelProperty) { Mode = bindingMode }); protected override void SetBindingContext(Control view, object viewModel) => view.DataContext = viewModel; } -} \ No newline at end of file +} diff --git a/CSharpMath.Xaml.Tests/TestMaui.cs b/CSharpMath.Xaml.Tests/TestMaui.cs index addb21a4..f14667d1 100644 --- a/CSharpMath.Xaml.Tests/TestMaui.cs +++ b/CSharpMath.Xaml.Tests/TestMaui.cs @@ -9,6 +9,13 @@ namespace CSharpMath.Xaml.Tests { using Maui; using Microsoft.Maui.Controls.Xaml; + public class InstrumentedMauiMathView : MathView, ITestDiagnostics { + readonly Dictionary counts = new(); + internal override void OnDiagnostic(string name) => counts[name] = counts.GetValueOrDefault(name) + 1; + public void Reset() => counts.Clear(); + public int Count(string name) => counts.GetValueOrDefault(name); + } + public class TestMaui : Test { @@ -57,6 +64,7 @@ public bool DispatchDelayed(TimeSpan delay, Action action) { protected override BindingMode Default => BindingMode.Default; protected override BindingMode OneWayToSource => BindingMode.OneWayToSource; protected override BindingMode TwoWay => BindingMode.TwoWay; + protected override GraphicsView CreateInstrumentedMathView() => new InstrumentedMauiMathView(); protected override TView ParseFromXaml(string xaml) { var view = new TView(); view.LoadFromXaml(xaml); @@ -74,4 +82,4 @@ protected override IDisposable SetBinding(GraphicsView view, BindableProperty pr protected override void SetBindingContext(GraphicsView view, object viewModel) => view.BindingContext = viewModel; } -} \ No newline at end of file +} diff --git a/CSharpMath.Xaml.Tests/TestUno.cs b/CSharpMath.Xaml.Tests/TestUno.cs new file mode 100644 index 00000000..e9bfbe9b --- /dev/null +++ b/CSharpMath.Xaml.Tests/TestUno.cs @@ -0,0 +1,15 @@ +using System.Runtime.CompilerServices; +using Microsoft.UI.Xaml.Data; +using Xunit; + +namespace CSharpMath.Xaml.Tests { + public class TestUno { + [Fact] + public void BindingExpressionMarkerPreventsCounterpartAssignment() { + var binding = RuntimeHelpers.GetUninitializedObject(typeof(BindingExpression)); + Assert.False(Uno.UnoPropertySynchronization.ShouldAssignCounterpart(binding)); + Assert.True(Uno.UnoPropertySynchronization.ShouldAssignCounterpart(null)); + Assert.True(Uno.UnoPropertySynchronization.ShouldAssignCounterpart(new object())); + } + } +} diff --git a/CSharpMath.Xaml/Views.cs b/CSharpMath.Xaml/Views.cs index 09bdd917..e8a59a94 100644 --- a/CSharpMath.Xaml/Views.cs +++ b/CSharpMath.Xaml/Views.cs @@ -40,6 +40,12 @@ namespace CSharpMath.Maui { using MathPainter = CSharpMath.SkiaSharp.MathPainter; using TextPainter = CSharpMath.SkiaSharp.TextPainter; namespace CSharpMath.Uno { +#endif +#if Uno + internal static class UnoPropertySynchronization { + internal static bool ShouldAssignCounterpart(object? localValue) => + localValue is not Microsoft.UI.Xaml.Data.BindingExpression; + } [Microsoft.UI.Xaml.Markup.ContentProperty(Name = nameof(LaTeX))] #endif public partial class BaseView<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] TPainter, @@ -47,6 +53,9 @@ public partial class BaseView<[DynamicallyAccessedMembers(DynamicallyAccessedMem TContent> : XInheritControl, ICSharpMathAPI where TPainter : Painter, new() where TContent : class { public TPainter Painter { get; } = new TPainter(); + bool _synchronizingProperties; + // Internal test seam; production views do not carry diagnostic state. + internal virtual void OnDiagnostic(string _) { } protected static readonly TPainter staticPainter = new TPainter(); @@ -65,8 +74,8 @@ static BaseView() { GlyphBoxColorProperty = CreateProperty, (XColor glyph, XColor textRun)?>(nameof(GlyphBoxColor), false, p => p.GlyphBoxColor is var (glyph, textRun) ? Nullable((XCanvasColorToXColor(glyph), XCanvasColorToXColor(textRun))) : null, (p, v) => p.GlyphBoxColor = v is var (glyph, textRun) ? Nullable((XColorToXCanvasColor(glyph), XColorToXCanvasColor(textRun))) : null), - ContentProperty = CreateProperty, TContent?>(nameof(Content), true, p => p.Content, (p, v) => p.Content = v, (b, v) => { if (b.Painter.ErrorMessage == null) b.LaTeX = b.Painter.LaTeX; }), - LaTeXProperty = CreateProperty, string?>(nameof(LaTeX), true, p => p.LaTeX, (p, v) => p.LaTeX = v, (b, v) => (b.Content, b.ErrorMessage) = (b.Painter.Content, b.Painter.ErrorMessage)), + ContentProperty = CreateProperty, TContent?>(nameof(Content), true, p => p.Content, (p, v) => p.Content = v, SynchronizeFromContent), + LaTeXProperty = CreateProperty, string?>(nameof(LaTeX), true, p => p.LaTeX, (p, v) => p.LaTeX = v, SynchronizeFromLaTeX), DisplayErrorInlineProperty = CreateProperty, bool>(nameof(DisplayErrorInline), true, p => p.DisplayErrorInline, (p, v) => p.DisplayErrorInline = v), FontSizeProperty = CreateProperty, float>(nameof(FontSize), true, p => p.FontSize, (p, v) => p.FontSize = v), ErrorFontSizeProperty = CreateProperty, float?>(nameof(ErrorFontSize), true, p => p.ErrorFontSize, (p, v) => p.ErrorFontSize = v), @@ -97,13 +106,20 @@ static XProperty CreateProperty< where TThis : BaseView { var defaultValue = defaultValueGet(staticPainter); void PropertyChanged(TThis @this, object? newValue) { + // Counterpart updates only mirror the already-updated painter state; + // they must not parse/serialize it a second time or invalidate twice. + if (@this._synchronizingProperties) return; // We need to support nullable classes and structs, so we cannot forbid null here // So this use of the null-forgiving operator should be blamed on non-generic PropertyChanged handlers var @new = (TValue)newValue!; + @this.OnDiagnostic(propertyName == nameof(Content) ? "ContentToLaTeX" : propertyName == nameof(LaTeX) ? "LaTeXToContent" : propertyName); propertySet(@this.Painter, @new); - if (affectsMeasure) @this.InvalidateMeasure(); + if (affectsMeasure) @this.InvalidateMeasureTracked(); #if Avalonia - updateOtherProperty?.Invoke(@this, @new); // Re-assign LaTeX from Content or vice versa + @this._synchronizingProperties = true; + try { updateOtherProperty?.Invoke(@this, @new); } // Re-assign LaTeX from Content or vice versa + finally { @this._synchronizingProperties = false; } + @this.OnDiagnostic("VisualInvalidation"); @this.InvalidateVisual(); } var prop = XProperty.Register(propertyName, defaultValue); @@ -112,6 +128,19 @@ void PropertyChanged(TThis @this, object? newValue) { } static string GetPropertyName(XProperty prop) => prop.Name; } + static void SynchronizeFromContent(BaseView view, TContent? _) { + if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX); + view.ErrorMessage = view.Painter.ErrorMessage; + } + static void SynchronizeFromLaTeX(BaseView view, string? _) { + if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX); + view.SetSynchronizedValue(ContentProperty, view.Painter.Content); + view.ErrorMessage = view.Painter.ErrorMessage; + } + void InvalidateMeasureTracked() { OnDiagnostic("MeasureInvalidation"); InvalidateMeasure(); } + void SetSynchronizedValue(XProperty property, object? value) { + SetValue(property, value); + } public BaseView() { // Respect built-in styles Styles.Add(new global::Avalonia.Styling.Style(global::Avalonia.Styling.Selectors.Is>) { @@ -163,7 +192,10 @@ public override void Render(global::Avalonia.Media.DrawingContext context) { base.Render(context); var canvas = new XCanvas(context, Bounds.Size); #elif Maui - updateOtherProperty?.Invoke(@this, @new); // Re-assign LaTeX from Content or vice versa + @this._synchronizingProperties = true; + try { updateOtherProperty?.Invoke(@this, @new); } // Re-assign LaTeX from Content or vice versa + finally { @this._synchronizingProperties = false; } + @this.OnDiagnostic("VisualInvalidation"); @this.Invalidate(); } return XProperty.Create(propertyName, typeof(TValue), typeof(TThis), defaultValue, @@ -171,6 +203,17 @@ public override void Render(global::Avalonia.Media.DrawingContext context) { } static string GetPropertyName(XProperty prop) => prop.PropertyName; } + static void SynchronizeFromContent(BaseView view, TContent? _) { + if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX); + view.ErrorMessage = view.Painter.ErrorMessage; + } + static void SynchronizeFromLaTeX(BaseView view, string? _) { + if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX); + view.SetSynchronizedValue(ContentProperty, view.Painter.Content); + view.ErrorMessage = view.Painter.ErrorMessage; + } + void InvalidateMeasureTracked() { OnDiagnostic("MeasureInvalidation"); InvalidateMeasure(); } + void SetSynchronizedValue(XProperty property, object? value) => SetValue(property, value); protected override Microsoft.Maui.Graphics.Size MeasureOverride(double widthConstraint, double heightConstraint) => Painter.Measure((float)widthConstraint) is { } rect ? new(Math.Min(rect.Width, widthConstraint), Math.Min(rect.Height, heightConstraint)) // We can't allocate too big of a GraphicsView on MAUI Windows. @@ -208,7 +251,13 @@ void Draw(XCanvas_Canvas rawCanvas) { // dirtyRect may be larger than Width and Height on Windows which leads to incorrect alignments var canvas = (rawCanvas, new Microsoft.Maui.Graphics.SizeF((float)Width, (float)Height)); #elif Uno - // Uno/WinUI bindings are gone when a property is assigned unlike Avalonia and MAUI, so we omit updateOtherProperty. + // Keep the same guarded counterpart update on Uno/WinUI. The guard + // prevents a second painter conversion; SetValue is the dependency + // property value update used by Uno for this control's properties. + @this._synchronizingProperties = true; + try { updateOtherProperty?.Invoke(@this, @new); } + finally { @this._synchronizingProperties = false; } + @this.OnDiagnostic("VisualInvalidation"); @this.Invalidate(); } return XProperty.Register(propertyName, typeof(TValue), typeof(TThis), @@ -216,6 +265,20 @@ void Draw(XCanvas_Canvas rawCanvas) { } static string GetPropertyName(XProperty prop) => ""; // unsupported :( } + static void SynchronizeFromContent(BaseView view, TContent? _) { + if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX); + view.ErrorMessage = view.Painter.ErrorMessage; + } + static void SynchronizeFromLaTeX(BaseView view, string? _) { + if (view.Painter.ErrorMessage == null) view.SetSynchronizedValue(LaTeXProperty, view.Painter.LaTeX); + view.SetSynchronizedValue(ContentProperty, view.Painter.Content); + view.ErrorMessage = view.Painter.ErrorMessage; + } + void InvalidateMeasureTracked() { OnDiagnostic("MeasureInvalidation"); InvalidateMeasure(); } + void SetSynchronizedValue(XProperty property, object? value) { + if (!UnoPropertySynchronization.ShouldAssignCounterpart(ReadLocalValue(property))) return; + SetValue(property, value); + } public BaseView() { PointerPressed += (sender, e) => { if (e.GetCurrentPoint(this) is { Properties.IsLeftButtonPressed: true } point && EnablePanning) @@ -321,4 +384,4 @@ protected override void RenderOverride(XCanvas canvas, Windows.Foundation.Size a } public partial class MathView : BaseView { } public partial class TextView : BaseView { } -} \ No newline at end of file +} diff --git a/ReadMe.md b/ReadMe.md index 0301d2e0..3fb5beac 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -358,6 +358,10 @@ Dependency|Used by|License [@FoggyFinder](https://github.com/FoggyFinder) +### Rendering update benchmarks + +`CSharpMath.Rendering.Benchmarks` includes a `ViewUpdateChurnBenchmarks` benchmark for the representative `x_1 = 1.234e50` input. It compares repeated LaTeX assignment, repeated content assignment, and measuring a persistent painter after one initial assignment at 100 and 1000 iterations. Run it in Release with BenchmarkDotNet and inspect the generated report; timings are intentionally not recorded here because they depend on the machine and renderer. For high-frequency updates, prefer reusing a parsed content object or painter when the input source permits it. + Thanks for reading.