Browse Source

Merge pull request #8433 from AvaloniaUI/fixes/8389-datagrid-detach

Improve performance of style class selector subscriptions
release/0.10.16
Dan Walmsley 4 years ago
committed by Steven Kirk
parent
commit
fbce80d722
  1. 53
      src/Avalonia.Styling/Controls/Classes.cs
  2. 14
      src/Avalonia.Styling/Controls/IClassesChangedListener.cs
  3. 1
      src/Avalonia.Styling/Properties/AssemblyInfo.cs
  4. 26
      src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs
  5. 3
      src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs
  6. 85
      tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs
  7. 2
      tests/Avalonia.LeakTests/ControlTests.cs
  8. 5
      tests/Avalonia.Styling.UnitTests/SelectorTests_Template.cs

53
src/Avalonia.Styling/Controls/Classes.cs

@ -14,6 +14,8 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public class Classes : AvaloniaList<string>, IPseudoClasses public class Classes : AvaloniaList<string>, IPseudoClasses
{ {
private List<IClassesChangedListener>? _listeners;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class. /// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary> /// </summary>
@ -39,6 +41,11 @@ namespace Avalonia.Controls
{ {
} }
/// <summary>
/// Gets the number of listeners subscribed to this collection for unit testing purposes.
/// </summary>
internal int ListenerCount => _listeners?.Count ?? 0;
/// <summary> /// <summary>
/// Parses a classes string. /// Parses a classes string.
/// </summary> /// </summary>
@ -62,6 +69,7 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Add(name); base.Add(name);
NotifyChanged();
} }
} }
@ -89,6 +97,7 @@ namespace Avalonia.Controls
} }
base.AddRange(c); base.AddRange(c);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -103,6 +112,8 @@ namespace Avalonia.Controls
RemoveAt(i); RemoveAt(i);
} }
} }
NotifyChanged();
} }
/// <summary> /// <summary>
@ -122,6 +133,7 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Insert(index, name); base.Insert(index, name);
NotifyChanged();
} }
} }
@ -154,6 +166,7 @@ namespace Avalonia.Controls
if (toInsert != null) if (toInsert != null)
{ {
base.InsertRange(index, toInsert); base.InsertRange(index, toInsert);
NotifyChanged();
} }
} }
@ -169,7 +182,14 @@ namespace Avalonia.Controls
public override bool Remove(string name) public override bool Remove(string name)
{ {
ThrowIfPseudoclass(name, "removed"); ThrowIfPseudoclass(name, "removed");
return base.Remove(name);
if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -197,6 +217,7 @@ namespace Avalonia.Controls
if (toRemove != null) if (toRemove != null)
{ {
base.RemoveAll(toRemove); base.RemoveAll(toRemove);
NotifyChanged();
} }
} }
@ -214,6 +235,7 @@ namespace Avalonia.Controls
var name = this[index]; var name = this[index];
ThrowIfPseudoclass(name, "removed"); ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index); base.RemoveAt(index);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -224,6 +246,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count) public override void RemoveRange(int index, int count)
{ {
base.RemoveRange(index, count); base.RemoveRange(index, count);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -255,6 +278,7 @@ namespace Avalonia.Controls
} }
base.AddRange(source); base.AddRange(source);
NotifyChanged();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -263,13 +287,38 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Add(name); base.Add(name);
NotifyChanged();
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
bool IPseudoClasses.Remove(string name) bool IPseudoClasses.Remove(string name)
{ {
return base.Remove(name); if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
}
internal void AddListener(IClassesChangedListener listener)
{
(_listeners ??= new()).Add(listener);
}
internal void RemoveListener(IClassesChangedListener listener)
{
_listeners?.Remove(listener);
}
private void NotifyChanged()
{
if (_listeners is null)
return;
foreach (var listener in _listeners)
listener.Changed();
} }
private void ThrowIfPseudoclass(string name, string operation) private void ThrowIfPseudoclass(string name, string operation)

14
src/Avalonia.Styling/Controls/IClassesChangedListener.cs

@ -0,0 +1,14 @@
namespace Avalonia.Controls
{
/// <summary>
/// Internal interface for listening to changes in <see cref="Classes"/> in a more
/// performant manner than subscribing to CollectionChanged.
/// </summary>
internal interface IClassesChangedListener
{
/// <summary>
/// Notifies the listener that the <see cref="Classes"/> collection has changed.
/// </summary>
void Changed();
}
}

1
src/Avalonia.Styling/Properties/AssemblyInfo.cs

@ -6,4 +6,5 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")]
[assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")] [assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
[assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]

26
src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls;
#nullable enable #nullable enable
@ -10,21 +11,17 @@ namespace Avalonia.Styling.Activators
/// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a /// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a
/// control. /// control.
/// </summary> /// </summary>
internal sealed class StyleClassActivator : StyleActivatorBase internal sealed class StyleClassActivator : StyleActivatorBase, IClassesChangedListener
{ {
private readonly IList<string> _match; private readonly IList<string> _match;
private readonly IAvaloniaReadOnlyList<string> _classes; private readonly Classes _classes;
private NotifyCollectionChangedEventHandler? _classesChangedHandler;
public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match) public StyleClassActivator(Classes classes, IList<string> match)
{ {
_classes = classes; _classes = classes;
_match = match; _match = match;
} }
private NotifyCollectionChangedEventHandler ClassesChangedHandler =>
_classesChangedHandler ??= ClassesChanged;
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch) public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{ {
int remainingMatches = toMatch.Count; int remainingMatches = toMatch.Count;
@ -55,23 +52,20 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0; return remainingMatches == 0;
} }
protected override void Initialize() void IClassesChangedListener.Changed()
{ {
PublishNext(IsMatching()); PublishNext(IsMatching());
_classes.CollectionChanged += ClassesChangedHandler;
} }
protected override void Deinitialize() protected override void Initialize()
{ {
_classes.CollectionChanged -= ClassesChangedHandler; PublishNext(IsMatching());
_classes.AddListener(this);
} }
private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e) protected override void Deinitialize()
{ {
if (e.Action != NotifyCollectionChangedAction.Move) _classes.RemoveListener(this);
{
PublishNext(IsMatching());
}
} }
private bool IsMatching() => AreClassesMatching(_classes, _match); private bool IsMatching() => AreClassesMatching(_classes, _match);

3
src/Avalonia.Styling/Styling/TypeNameAndClassSelector.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.Controls;
using Avalonia.Styling.Activators; using Avalonia.Styling.Activators;
#nullable enable #nullable enable
@ -125,7 +126,7 @@ namespace Avalonia.Styling
{ {
if (subscribe) if (subscribe)
{ {
var observable = new StyleClassActivator(control.Classes, _classes.Value); var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value);
return new SelectorMatch(observable); return new SelectorMatch(observable);
} }

85
tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs

@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Styling;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class Style_ClassSelector
{
private Style _style = null!;
public Style_ClassSelector()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[GlobalSetup]
public void Setup()
{
_style = new Style(x => x.OfType<TestClass>().Class("foo"))
{
Setters = { new Setter(TestClass.StringProperty, "foo") }
};
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply_Toggle()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.Classes.Add("foo");
target.Classes.Remove("foo");
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply_Detach()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.DetachStyles();
}
private class TestClass : Control
{
public static readonly StyledProperty<string?> StringProperty =
AvaloniaProperty.Register<TestClass, string?>("String");
public void DetachStyles() => InvalidateStyles();
}
private class TestClass2 : Control
{
}
}
}

2
tests/Avalonia.LeakTests/ControlTests.cs

@ -250,7 +250,7 @@ namespace Avalonia.LeakTests
// The TextBox should have subscriptions to its Classes collection from the // The TextBox should have subscriptions to its Classes collection from the
// default theme. // default theme.
Assert.NotEmpty(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers()); Assert.NotEqual(0, textBox.Classes.ListenerCount);
// Clear the content and ensure the TextBox is removed. // Clear the content and ensure the TextBox is removed.
window.Content = null; window.Content = null;

5
tests/Avalonia.Styling.UnitTests/SelectorTests_Template.cs

@ -143,14 +143,13 @@ namespace Avalonia.Styling.UnitTests
var border = (Border)target.Object.VisualChildren.Single(); var border = (Border)target.Object.VisualChildren.Single();
var selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType<Border>(); var selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType<Border>();
var activator = selector.Match(border).Activator; var activator = selector.Match(border).Activator;
var inccDebug = (INotifyCollectionChangedDebug)styleable.Object.Classes;
using (activator.Subscribe(_ => { })) using (activator.Subscribe(_ => { }))
{ {
Assert.Single(inccDebug.GetCollectionChangedSubscribers()); Assert.Equal(1, ((Classes)styleable.Object.Classes).ListenerCount);
} }
Assert.Null(inccDebug.GetCollectionChangedSubscribers()); Assert.Equal(0, ((Classes)styleable.Object.Classes).ListenerCount);
} }
private void BuildVisualTree<T>(Mock<T> templatedControl) where T : class, IVisual private void BuildVisualTree<T>(Mock<T> templatedControl) where T : class, IVisual

Loading…
Cancel
Save