Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor styling #3636

Merged
merged 12 commits into from
Mar 5, 2020
Merged

Refactor styling #3636

merged 12 commits into from
Mar 5, 2020

Conversation

grokys
Copy link
Member

@grokys grokys commented Mar 2, 2020

What does the pull request do?

Previously @MarchingCube and myself had identified that our styling system was one of the performance bottlenecks in our code, mainly due to its extensive use of Rx and Disposables and the allocations they make.

This PR refactors the styling system with an eye to reducing memory allocations:

  • Make AvaloniaObject.SetValue return an IDisposable for values that can be undone (i.e. non LocalValue values)
  • When instancing a Setter on a control, create a typed object to produce value/binding so that untyped binding adapters can be avoided
  • When instancing a Setter on a control, don't create a binding if the parent style doesn't have an activator, instead use SetValue now that it returns an IDisposable
  • Replaced the use of IObservable<bool> for style activators. While using IObservable<> itself does not necessarily have a large allocation overhead, the particularly limited use of it in this case meant that we can save a few allocations by using our own interface: IStyleActivator. In particular, to AND or OR a series of observables, one needs to allocate an object for each input observable in order to identify which of the inputs have changed. IStyleActivator.Subscribe on the other hand receives an int tag which is passed on each OnNext invocation to identify the input.
  • Improve our selector matching to check the type of the control before checking property values (and hopefully make it more readable too)
  • #nullable enable the files that were touched

What is the current behavior?

Some benchmarks on master:

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
AttachTextBoxStyles 25.27 us 0.4460 us 0.4172 us 9.0332 2.9907 - 8.51 KB
CreateCalendar 23.52 ms 0.4679 ms 0.7145 ms 1000.0000 - - 4.35 MB

Running ControlCatalog.NetCore on master and opening all the tabs:

542.38 MB total
165.05 MB .NET total
126.87 MB .NET used
2.35M objects

What is the updated/expected behavior with this PR?

Method Mean Error StdDev Gen 0 Gen 1 Gen 2 Allocated
AttachTextBoxStyles 18.32 us 0.1683 us 0.1405 us 1.1292 0.2747 - 6.8 KB
CreateCalendar 19.90 ms 0.3329 ms 0.3114 ms 1000.0000 - - 3.68 MB

Running ControlCatalog.NetCore on this branch and opening all the tabs:

518.25 MB total
138.43 MB .NET total
108.39 MB .NET used
1.93M objects

Checklist

  • Added unit tests (if possible)?

Breaking changes

Some of the low-level style APIs have changed a little, but these shouldn't be being used from user code.

This PR removes support for removing styles (well, you can still remove them but they won't get unapplied to controls). This is because I want to implement adding styles too and intend to do it in another PR.

We already have some specific internal methods for routing certain methods via an untyped property to a typed property, but adding support for the visitor pattern allows us to support arbitrary use-cases.
- Don't use Rx in the styling system. Instead introduces `IStyleActivator` which is like an `IObservable<bool>`-lite in order to cut down on allocations.
- #nullable enable on touched files
Match selectors from left-to-right, as before we were checking things like property equality (and creating a `PropertyEqualsActivator`) before checking that the control is of the correct type. Also hopefully makes the selector matching logic more readable.
We need to make sure to set up the bindings as soon as the setter is instanced, even if it's not activated immediately.
Animations are still using an `IObservable<bool>` to trigger them as I didn't feel confident refactoring them as part of this PR.
I'm not sure what should happen here when the style deactivates and is re-activated. Should the one-time binding trigger again? Or should it only trigger on first activation? Wait and see what the use-case for this is (if any).
@grokys grokys requested review from MarchingCube and a team March 2, 2020 21:59
src/Avalonia.Styling/Styling/StyleInstance.cs Outdated Show resolved Hide resolved
@@ -224,44 +147,12 @@ void ISetResourceParent.SetParent(IResourceNode parent)

if (parent == null)
{
Detach();
//Detach();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this commented out?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, and I forgot to mention it in the description. This PR actually removes support for removing styles (well, you can still remove them but they won't get unapplied to controls). This is because I wanted to implement adding styles too and intended to do it in another PR. Not sure if this is a blocker for this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it have any observable difference if we don’t support it for now?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If someone is removing styles from a Styles collection, yes. However the fact that adding styles to these doesn't currently work in my mind makes it unlikely that anyone is relying on this. I plan to submit a PR to implement adding/removing styles in the next day or so anyway.

src/Avalonia.Styling/StyledElement.cs Show resolved Hide resolved
@kekekeks
Copy link
Member

kekekeks commented Mar 3, 2020

Make AvaloniaObject.SetValue return an IDisposable for values that can be undone (i.e. non LocalValue values)

What happens to said disposable if something else sets a different value?

@grokys
Copy link
Member Author

grokys commented Mar 3, 2020

What happens to said disposable if something else sets a different value?

The new value will take effect until that itself is disposed. It might be worth giving some background here: this change is enabled by #3255:

Previously all values set via SetValue were only active until a binding of the same or higher priority produced a value, in which case the SetValue value was lost.

This meant that style setters needed to create a binding with a single value, otherwise the setter value would be lost when e.g. a setter with StyleTrigger binding was activated.

This PR changes that behavior: LocalValue priority values now work as before but other priority values are stored alongside bindings in the entries for the priority value and as such will not be overwritten by binding values. This will allow more efficient style values in a future PR as single values will no longer have to be created as single-value non-completing bindings

The future PR is this PR. What that #3255 didn't do though was provide a way to remove the priority value when a style was detached, which is what I added here.

Copy link
Collaborator

@MarchingCube MarchingCube left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

Style remove not working for a while should be fine (I think that might break our theme switcher).
And you still have merge conflicts to resolve.

@grokys
Copy link
Member Author

grokys commented Mar 5, 2020

Ok, implemented support for adding/removing styles, will open a PR shortly. In the meantime, will merge this.

@grokys grokys merged commit a70f774 into master Mar 5, 2020
@grokys grokys deleted the refactor/styling branch March 5, 2020 18:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants