-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
[C] use ResourcesChanged to propagate Theme #21229
Conversation
Is there a benchmark or some observation how much faster this is, please? We have an app where there are may AppThemeBindings, so I'm very curious. |
02e7fa3
to
340e53e
Compare
@@ -101,6 +101,15 @@ public IAppLinks AppLinks | |||
|
|||
_singleWindowMainPage = value; | |||
|
|||
if (value is not null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this fixes another bug. resources changed at application level weren't propagated to MainPage
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think if you are just using MainPage, then you get parented to a window during the creating of the UI - so once the app is ready, it calls CreateWindow and assigns the MainPage to the window.
The _singleWindowMainPage
is just a placeholder while the app is still being constructed. Changing MainPage later literally just replaces the content of the first window:
Windows[0].Page = value;
What you have observed might just be because MainPage really is a delayed construction and assigned. Now this is to say it may be fine what you are doing here, but will it result in a double listener registration once the window is created and the MainPage parented to the window?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe, but that doesn't work in unit tests
@@ -827,10 +827,12 @@ private protected override void OnHandlerChangingCore(HandlerChangingEventArgs a | |||
return; | |||
|
|||
if (args.NewHandler == null) | |||
#pragma warning disable CS0618 // Type or member is obsolete |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well, it's not anymore in this version of the PR, but maybe it should
@@ -34,6 +34,7 @@ public void ThemeChangeUsingSetAppThemeColor() | |||
{ | |||
Text = "Green on Light, Red on Dark" | |||
}; | |||
app.LoadPage(new ContentPage {Content = label}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
apptheme propagation REQUIRES that the control is parented, which is fine, as otherwise it wouldn't be on screen
@@ -241,7 +241,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.Appium", "src\TestUt | |||
EndProject | |||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UITest.NUnit", "src\TestUtils\src\UITest.NUnit\UITest.NUnit.csproj", "{A307B624-48D4-494E-A70D-5B3CDF6620CF}" | |||
EndProject | |||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Controls.SourceGen.UnitTests", "src\Controls\tests\SourceGen.UnitTests\Conrtrols.SourceGen.UnitTests\Controls.SourceGen.UnitTests.csproj", "{06747B55-91DB-47F5-B7A2-56526C28A0D3}" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😮
WeakReference<BindableObject> _weakTarget; | ||
BindableProperty _targetProperty; | ||
bool _attached; | ||
// bool _attached; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not used anywhere, I think we can eliminate this variable.
|
||
public void OnAppThemeChanged() | ||
{ | ||
Binding.Apply(false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm curious about the performance impact, especially on CollectionView scrolling. Could we perhaps have an UITest with this example #18505 where we can display the minimum fps in a Label by scrolling and verifying that the value is greater than n (where n is value higher than before)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may need @jonathanpeppers to run this PR in a release app with profiling. Our UI tests have a lot of overhead and appium things running.
{ | ||
_parent = parent; | ||
Binding = binding; | ||
this.SetDynamicResource(AppThemeProperty, "__ApplicationTheme__"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this string should be in some sort of const place and maybe also have a __MAUI__
prefix.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
__MAUI__MatthewIsAwesome__ApplicationTheme__
?
@@ -8,10 +8,38 @@ namespace Microsoft.Maui.Controls | |||
{ | |||
class AppThemeBinding : BindingBase | |||
{ | |||
class AppThemeProxy : Element | |||
{ | |||
public AppThemeProxy(Element parent, AppThemeBinding binding) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see the AppThemeBinding
uses a weak reference for the target: WeakReference<BindableObject> _weakTarget;
Is this now creating a string reference that might cause issues? Maybe the AppThemeBinding does not need to be weak?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the proxy might prevent the AppThemeBinding to GC, but the AppThemeBinding doesn't hold a strong ref to the BO.
in this case, I trust the GC to do the job
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like I was wrong
((IElementDefinition)parent).AddResourcesChangedListener(OnParentResourcesChanged); | ||
} | ||
|
||
public static BindableProperty AppThemeProperty = BindableProperty.Create("AppTheme", typeof(AppTheme), typeof(AppThemeBinding), AppTheme.Unspecified, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will it be more expensive to have a separate proxy Element object with a single extra property vs just adding this to maybe the base Element?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we can't add that to the base Element, as an element might need have multiple AppThemebindings, and then multiples proxies. moving that to the element will require some sort of event subscription, and that's what we're avoiding here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking better than mine with all my extra things, and this looks cleaner to. I tried to cover all the cases of all the things, but just making it work when the theme changes AND the element is parented is better. I tried to make it always work even when there is a floating element.
I also just dumped a bunch of thoughts and questions on this. More trying to see what you were thinking and if there is any gotchas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to significantly improve {AppThemeBinding}
. I tested this app on a Pixel 5:
Before I was seeing:
With calling method:
(Used git revert -n 340e53e6
)
Then tried the change in this PR:
Just compare the %, as the traces have different durations.
Scrolling the sample app, it "feels" better too.
340e53e
to
2982b41
Compare
Context: dotnet#18505 Context: https://github.com/dotnet/maui/files/13251041/MauiCollectionView.zip Context: dotnet#21229 (review) In profiling scrolling of an app with a `<CollectionView/>` and 12 `<Label/>`s, we see time spent in: 1.9% Microsoft.Maui!Microsoft.Maui.Platform.MauiTextView.OnLayout(bool,int,int,int,int) This is a callback from Java to C#, which has a performance cost. Reviewing the code, we would only need to make this callback *at all* if `Label.TextType` is not `TextType.Text`. The bulk of all `Label`'s can avoid this call? To do this: * Write a new `PlatformAppCompatTextView.java` that override `onLayout()` * It only calls `onLayoutFormatted()` if a `isFormatted` `boolean` field is `true` * We can set `isFormatted` when `Label.TextType` changes to something other than `TextType.Text`. With this change in place, the above `MauiTextView.OnLayout()` call is completely gone from `dotnet-trace` output. Scrolling the sample also "feels" a bit snappier. This should improve the performance of all `Label`s on Android. This is the mininum amount of API changes possible -- which seems like what we should go for if we ship this change in .NET 8 servicing.
Context: dotnet#18505 Context: https://github.com/dotnet/maui/files/13251041/MauiCollectionView.zip Context: dotnet#21229 (review) In profiling scrolling of an app with a `<CollectionView/>` and 12 `<Label/>`s, we see time spent in: 1.9% Microsoft.Maui!Microsoft.Maui.Platform.MauiTextView.OnLayout(bool,int,int,int,int) This is a callback from Java to C#, which has a performance cost. Reviewing the code, we would only need to make this callback *at all* if `Label.TextType` is not `TextType.Text`. The bulk of all `Label`'s can avoid this call? To do this: * Write a new `PlatformAppCompatTextView.java` that override `onLayout()` * It only calls `onLayoutFormatted()` if a `isFormatted` `boolean` field is `true` * We can set `isFormatted` when `Label.TextType` changes to something other than `TextType.Text`. With this change in place, the above `MauiTextView.OnLayout()` call is completely gone from `dotnet-trace` output. Scrolling the sample also "feels" a bit snappier. This should improve the performance of all `Label`s on Android. This is the mininum amount of API changes possible -- which seems like what we should go for if we ship this change in .NET 8 servicing.
Applies to: dotnet#18505 Context: https://github.com/dotnet/maui/files/13251041/MauiCollectionView.zip I profiled the above sample with `dotnet-trace` with the following PRs applied locally: * dotnet#21229 * dotnet#21291 While scrolling, a lot of time is spent in `ResourceDictionary` lookups on an Android Pixel 5 device: 2.0% Microsoft.Maui.Controls!Microsoft.Maui.Controls.ResourceDictionary.TryGetValue(string,object&) Drilling in, I can see System.Linq's `Reverse()` method: 0.56% System.Linq!System.Linq.Enumerable.ReverseIterator<TSource_REF>.MoveNext() 0.14% System.Linq!System.Linq.Enumerable.Reverse(System.Collections.Generic.IEnumerable`1<TSource_REF>) 0.04% System.Linq!System.Linq.Enumerable.ReverseIterator<TSource_REF>..ctor(System.Collections.Generic.IEnumerable`1<TSource_REF>) 0.04% System.Linq!System.Linq.Enumerable.ReverseIterator<TSource_REF>.Dispose() `Reverse()` can be problematic as it can sometimes create a copy of the entire collection, in order to sort in reverse. We can juse use a reverse `for`-loop instead. The indexer, we can also avoid a double-lookup: if (dict.ContainsKey(index)) return dict[index]; And instead do: if (dict.TryGetValue(index, out var value)) return value; The MAUI project template seems to setup a few "merged" `ResourceDictionary` as it contains `Styles.xaml`, so this is why this code path is being hit. I wrote a BenchmarkDotNet benchmark, and it indicates the collection is being copied, as the 872 bytes of allocation occur: | Method | key | Mean | Error | StdDev | Gen0 | Allocated | |------------ |------------ |----------:|---------:|---------:|-------:|----------:| | TryGetValue | key0 | 11.45 ns | 0.026 ns | 0.023 ns | - | - | | Indexer | key0 | 24.72 ns | 0.133 ns | 0.118 ns | - | - | | TryGetValue | merged99,99 | 117.06 ns | 2.334 ns | 2.497 ns | 0.1042 | 872 B | | Indexer | merged99,99 | 145.60 ns | 2.737 ns | 2.286 ns | 0.1042 | 872 B | With these changes in place, I see less time spent inside: 0.91% Microsoft.Maui.Controls!Microsoft.Maui.Controls.ResourceDictionary.TryGetValue(string,object&) The benchmark no longer allocates either: | Method | key | Mean | Error | StdDev | Allocated | |------------ |------------ |----------:|----------:|----------:|----------:| | TryGetValue | key0 | 11.92 ns | 0.094 ns | 0.084 ns | - | | Indexer | merged99,99 | 23.12 ns | 0.418 ns | 0.391 ns | - | | Indexer | key0 | 24.20 ns | 0.485 ns | 0.453 ns | - | | TryGetValue | merged99,99 | 29.09 ns | 0.296 ns | 0.262 ns | - | This should improve the performance "parenting" of any MAUI view on all platforms -- as well as scrolling `CollectionView`.
Context: dotnet#18505 Context: https://github.com/dotnet/maui/files/13251041/MauiCollectionView.zip Context: dotnet#21229 (review) In profiling scrolling of an app with a `<CollectionView/>` and 12 `<Label/>`s, we see time spent in: 1.9% Microsoft.Maui!Microsoft.Maui.Platform.MauiTextView.OnLayout(bool,int,int,int,int) This is a callback from Java to C#, which has a performance cost. Reviewing the code, we would only need to make this callback *at all* if `Label.FormattedText` is not `null`. The bulk of all `Label`'s can avoid this call? To do this: * Write a new `PlatformAppCompatTextView.java` that override `onLayout()` * It only calls `onLayoutFormatted()` if a `isFormatted` `boolean` field is `true` * We can set `isFormatted` if a formatted string is such as: `isFormatted = !(text instanceof String)` With this change in place, the above `MauiTextView.OnLayout()` call is completely gone from `dotnet-trace` output. Scrolling the sample also "feels" a bit snappier. This should improve the performance of all non-formatted `Label`s on Android. This is the mininum amount of API changes possible -- which seems like what we should go for if we ship this change in .NET 8 servicing.
Context: #18505 Context: https://github.com/dotnet/maui/files/13251041/MauiCollectionView.zip Context: #21229 (review) In profiling scrolling of an app with a `<CollectionView/>` and 12 `<Label/>`s, we see time spent in: 1.9% Microsoft.Maui!Microsoft.Maui.Platform.MauiTextView.OnLayout(bool,int,int,int,int) This is a callback from Java to C#, which has a performance cost. Reviewing the code, we would only need to make this callback *at all* if `Label.FormattedText` is not `null`. The bulk of all `Label`'s can avoid this call? To do this: * Write a new `PlatformAppCompatTextView.java` that override `onLayout()` * It only calls `onLayoutFormatted()` if a `isFormatted` `boolean` field is `true` * We can set `isFormatted` if a formatted string is such as: `isFormatted = !(text instanceof String)` With this change in place, the above `MauiTextView.OnLayout()` call is completely gone from `dotnet-trace` output. Scrolling the sample also "feels" a bit snappier. This should improve the performance of all non-formatted `Label`s on Android. This is the mininum amount of API changes possible -- which seems like what we should go for if we ship this change in .NET 8 servicing.
Applies to: #18505 Context: https://github.com/dotnet/maui/files/13251041/MauiCollectionView.zip I profiled the above sample with `dotnet-trace` with the following PRs applied locally: * #21229 * #21291 While scrolling, a lot of time is spent in `ResourceDictionary` lookups on an Android Pixel 5 device: 2.0% Microsoft.Maui.Controls!Microsoft.Maui.Controls.ResourceDictionary.TryGetValue(string,object&) Drilling in, I can see System.Linq's `Reverse()` method: 0.56% System.Linq!System.Linq.Enumerable.ReverseIterator<TSource_REF>.MoveNext() 0.14% System.Linq!System.Linq.Enumerable.Reverse(System.Collections.Generic.IEnumerable`1<TSource_REF>) 0.04% System.Linq!System.Linq.Enumerable.ReverseIterator<TSource_REF>..ctor(System.Collections.Generic.IEnumerable`1<TSource_REF>) 0.04% System.Linq!System.Linq.Enumerable.ReverseIterator<TSource_REF>.Dispose() `Reverse()` can be problematic as it can sometimes create a copy of the entire collection, in order to sort in reverse. We can juse use a reverse `for`-loop instead. The indexer, we can also avoid a double-lookup: if (dict.ContainsKey(index)) return dict[index]; And instead do: if (dict.TryGetValue(index, out var value)) return value; The MAUI project template seems to setup a few "merged" `ResourceDictionary` as it contains `Styles.xaml`, so this is why this code path is being hit. I wrote a BenchmarkDotNet benchmark, and it indicates the collection is being copied, as the 872 bytes of allocation occur: | Method | key | Mean | Error | StdDev | Gen0 | Allocated | |------------ |------------ |----------:|---------:|---------:|-------:|----------:| | TryGetValue | key0 | 11.45 ns | 0.026 ns | 0.023 ns | - | - | | Indexer | key0 | 24.72 ns | 0.133 ns | 0.118 ns | - | - | | TryGetValue | merged99,99 | 117.06 ns | 2.334 ns | 2.497 ns | 0.1042 | 872 B | | Indexer | merged99,99 | 145.60 ns | 2.737 ns | 2.286 ns | 0.1042 | 872 B | With these changes in place, I see less time spent inside: 0.91% Microsoft.Maui.Controls!Microsoft.Maui.Controls.ResourceDictionary.TryGetValue(string,object&) The benchmark no longer allocates either: | Method | key | Mean | Error | StdDev | Allocated | |------------ |------------ |----------:|----------:|----------:|----------:| | TryGetValue | key0 | 11.92 ns | 0.094 ns | 0.084 ns | - | | Indexer | merged99,99 | 23.12 ns | 0.418 ns | 0.391 ns | - | | Indexer | key0 | 24.20 ns | 0.485 ns | 0.453 ns | - | | TryGetValue | merged99,99 | 29.09 ns | 0.296 ns | 0.262 ns | - | This should improve the performance "parenting" of any MAUI view on all platforms -- as well as scrolling `CollectionView`.
Description of Change
instead of subscribing to ThemeChanged event, use the ResourcesChanged mechanism of DynamicResource to propagate the change
Issues Fixed