Skip to content

Commit

Permalink
refactor: shape perf
Browse files Browse the repository at this point in the history
  • Loading branch information
Xiaoy312 committed Dec 9, 2024
1 parent 1e655ed commit 52c4fc4
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 11 deletions.
24 changes: 24 additions & 0 deletions src/Uno.UI/FeatureConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -877,5 +877,29 @@ internal static class DebugOptions

public static bool WaitIndefinitelyInEventTester { get; set; }
}

public static class Shape
{
#if __WASM__
/// <summary>
/// Gets or sets whether native svg attributes assignments can be postponed until the first arrange pass.
/// </summary>
/// <remarks>This avoid double assignments(with js interop call) from both OnPropertyChanged and UpdateRender.</remarks>
public static bool DelayUpdateUntilFirstArrange { get; set; } = true;

/// <summary>
/// Gets or sets whether native getBBox() result will be cached.
/// </summary>
public static bool CacheBBoxCalculationResult { get; set; } = true;
/// <summary>
/// Gets or sets the size of getBBox cache. The default size is 500.
/// </summary>
public static int BBoxCacheSize
{
get => Microsoft.UI.Xaml.Shapes.Shape.BBoxCacheSize;
set => Microsoft.UI.Xaml.Shapes.Shape.BBoxCacheSize = value;
}
#endif
}
}
}
88 changes: 77 additions & 11 deletions src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs
Original file line number Diff line number Diff line change
@@ -1,27 +1,43 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Uno;
using Uno.Disposables;
using Uno.Extensions;
using System.Numerics;
using Windows.Foundation;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Wasm;
using Uno;
using Uno.Collections;
using Uno.Disposables;
using Uno.Extensions;
using Uno.UI;
using Uno.UI.Xaml;

using RadialGradientBrush = Microsoft/* UWP don't rename */.UI.Xaml.Media.RadialGradientBrush;
using System.Numerics;
using System.Diagnostics;
using Uno.UI.Xaml;

namespace Microsoft.UI.Xaml.Shapes
{
partial class Shape

{
private static readonly LruCache<string, Rect> _bboxCache = new(500);

internal static int BBoxCacheSize
{
get => _bboxCache.Capacity;
set => _bboxCache.Capacity = value;
}
}

partial class Shape
{
private readonly SerialDisposable _fillBrushSubscription = new SerialDisposable();
private readonly SerialDisposable _strokeBrushSubscription = new SerialDisposable();

private DefsSvgElement _defs;
private protected readonly SvgElement _mainSvgElement;
private protected bool _shouldUpdateNative = !FeatureConfiguration.Shape.DelayUpdateUntilFirstArrange; // used to block updates to native element until 1st Arrange

protected Shape() : base("svg", isSvg: true)
{
Expand All @@ -37,14 +53,30 @@ private protected Shape(string mainSvgElementTag) : base("svg", isSvg: true)

private protected void UpdateRender()
{
OnFillBrushChanged();
OnStrokeBrushChanged();
UpdateStrokeThickness();
UpdateStrokeDashArray();
// We can delay the setting of these property until first arrange pass,
// so to prevent double-updates from both OnPropertyChanged & ArrangeOverride.
// These updates are a little costly as we need to cross the cs-js bridge.
_shouldUpdateNative = true;

// Regarding to caching of GetPathBoundingBox(js: getBBox) result:
// On shapes that depends on it, so: Line, Path, Polygon, Polyline
// all the properties below (at the time of written) has no effect on getBBox:
// > Note that the values of the opacity, visibility, fill, fill-opacity, fill-rule, stroke-dasharray and stroke-dashoffset properties on an element have no effect on the bounding box of an element.
// > -- https://svgwg.org/svg2-draft/coords.html#BoundingBoxes
// while not mentioned, stroke-width doesnt affect getBBox neither (for the 4 classes of shape mentioned above).

// StrokeThickness can alter getBBox on Ellipse and Rectangle, but we dont use getBBox in these two.

OnFillBrushChanged(); // fill
OnStrokeBrushChanged(); // stroke
UpdateStrokeThickness(); // stroke-width
UpdateStrokeDashArray(); // stroke-dasharray
}

private void OnFillBrushChanged()
{
if (!_shouldUpdateNative) return;

// We don't request an update of the HitTest (UpdateHitTest()) since this element is never expected to be hit testable.
// Note: We also enforce that the default hit test == false is not altered in the OnHitTestVisibilityChanged.

Expand Down Expand Up @@ -111,6 +143,8 @@ private void OnFillBrushChanged()

private void OnStrokeBrushChanged()
{
if (!_shouldUpdateNative) return;

var svgElement = _mainSvgElement;
var stroke = Stroke;

Expand Down Expand Up @@ -159,6 +193,8 @@ private void OnStrokeBrushChanged()

private void UpdateStrokeThickness()
{
if (!_shouldUpdateNative) return;

Check failure on line 196 in src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI/UI/Xaml/Shapes/Shape.wasm.cs#L196

Add curly braces around the nested statement(s) in this 'if' block.

var svgElement = _mainSvgElement;
var strokeThickness = ActualStrokeThickness;

Expand All @@ -174,6 +210,8 @@ private void UpdateStrokeThickness()

private void UpdateStrokeDashArray()
{
if (!_shouldUpdateNative) return;

var svgElement = _mainSvgElement;

if (StrokeDashArray is not { } strokeDashArray)
Expand Down Expand Up @@ -203,7 +241,35 @@ private UIElementCollection GetDefs()

private static Rect GetPathBoundingBox(Shape shape)
{
return shape._mainSvgElement.GetBBox();
if (FeatureConfiguration.Shape.CacheBBoxCalculationResult)
{
var key = shape switch
{
// most other properties don't impact getBBox result, see also comments in UpdateRender.
Path path => $"path:{(path.Data as GeometryData)?.Data}",
Line line => $"line:{line.X1},{line.Y1}-{line.X2}{line.Y2}",
Polygon polygon => $"polygon:{polygon.Points?.ToCssString()}",
Polyline polyline => $"polyline:{polyline.Points?.ToCssString()}",

#if DEBUG // we shouldn't hit these cases anyways
(Ellipse or Rectangle) => throw new InvalidOperationException(),
#endif
_ => null,
};

if (!string.IsNullOrEmpty(key))
{
if (!_bboxCache.TryGetValue(key, out var rect))
{
_bboxCache[key] = rect = shape._mainSvgElement.GetBBox();
}

return rect;
}
}

var result = shape._mainSvgElement.GetBBox();
return result;
}

private protected void Render(Shape shape, Size? size = null, double scaleX = 1d, double scaleY = 1d, double renderOriginX = 0d, double renderOriginY = 0d)
Expand Down
119 changes: 119 additions & 0 deletions src/Uno.UWP/Collections/LruCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Cache;
using System.Text;
using System.Threading.Tasks;

namespace Uno.Collections;

public partial class LruCache<TKey, TValue>
{
// essentially, a KeyValuePair where the value is mutable
private class KeyedItem
{
public TKey Key { get; init; }
public TValue Value { get; set; }

public KeyedItem(TKey key, TValue value)
{
this.Key = key;
this.Value = value;
}
}
}

/// <summary>
/// A least-recently-used cache, keeping only the last n added/updated items.
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
public partial class LruCache<TKey, TValue> where TKey : notnull
{
private readonly Dictionary<TKey, LinkedListNode<KeyedItem>> _map = new();
private readonly LinkedList<KeyedItem> _list = new();
private int _capacity;

public int Count => _map.Count;
public int Capacity
{
get => _capacity;
set => UpdateCapacity(value);
}

public TValue this[TKey key]
{
get => Get(key);
set => Put(key, value);
}

public LruCache(int capacity)
{
if (capacity < 0) throw new ArgumentOutOfRangeException("capacity must be positive or zero.");

this._capacity = capacity;
}

public TValue Get(TKey key)
{
return TryGetValue(key, out var value) ? value : default;
}
public bool TryGetValue(TKey key, out TValue value)
{
if (_map.TryGetValue(key, out var node))
{
_list.Remove(node);
_list.AddFirst(node);

value = node.Value.Value;
return true;
}
else
{
value = default;
return false;
}
}
public void Put(TKey key, TValue value)
{
if (_capacity == 0) return;

Check failure on line 79 in src/Uno.UWP/Collections/LruCache.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UWP/Collections/LruCache.cs#L79

Add curly braces around the nested statement(s) in this 'if' block.

if (_map.TryGetValue(key, out var node))
{
node.Value.Value = value;

_list.Remove(node);
_list.AddFirst(node);
}
else
{
while (_map.Count >= _capacity)
{
_map.Remove(_list.Last.Value.Key);
_list.RemoveLast();
}

_map.Add(key, _list.AddFirst(new KeyedItem(key, value)));
}
}

public void UpdateCapacity(int capacity)
{
if (capacity < 0) throw new ArgumentOutOfRangeException("capacity must be positive or zero.");

Check failure on line 102 in src/Uno.UWP/Collections/LruCache.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UWP/Collections/LruCache.cs#L102

Add curly braces around the nested statement(s) in this 'if' block.

_capacity = capacity;
if (_capacity == 0)
{
_map.Clear();
_list.Clear();
}
else
{
while (_map.Count > _capacity)
{
_map.Remove(_list.Last.Value.Key);
_list.RemoveLast();
}
}
}
}

0 comments on commit 52c4fc4

Please sign in to comment.