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
[android] avoid OnLayout()
for Label
#21291
Merged
jonathanpeppers
merged 1 commit into
dotnet:main
from
jonathanpeppers:PlatformAppCompatTextView
Mar 27, 2024
Merged
[android] avoid OnLayout()
for Label
#21291
jonathanpeppers
merged 1 commit into
dotnet:main
from
jonathanpeppers:PlatformAppCompatTextView
Mar 27, 2024
Conversation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
jonathanpeppers
added
platform/android 🤖
legacy-area-perf
Startup / Runtime performance
labels
Mar 18, 2024
jonathanpeppers
force-pushed
the
PlatformAppCompatTextView
branch
from
March 19, 2024 13:41
f5d4177
to
e6216a5
Compare
jonathanpeppers
added a commit
to jonathanpeppers/maui
that referenced
this pull request
Mar 20, 2024
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.
jonathanpeppers
force-pushed
the
PlatformAppCompatTextView
branch
from
March 26, 2024 13:45
60a21d9
to
c8aebdc
Compare
jonathanpeppers
commented
Mar 26, 2024
Comment on lines
+20
to
+24
@Override | ||
public void setText(CharSequence text, BufferType type) { | ||
isFormatted = !(text instanceof String); | ||
super.setText(text, type); | ||
} |
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.
Note that calling TextView.Text = "foo"
will call inside here:
The C# TextView.Text
property is bound to setText(CharSequence)
.
jsuarezruiz
approved these changes
Mar 27, 2024
StephaneDelcroix
pushed a commit
that referenced
this pull request
Mar 27, 2024
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`.
Eilon
added
t/perf
The issue affects performance (runtime speed, memory usage, startup time, etc.)
area-controls-label
Label, Span
and removed
legacy-area-perf
Startup / Runtime performance
labels
May 10, 2024
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Labels
area-controls-label
Label, Span
platform/android 🤖
t/perf
The issue affects performance (runtime speed, memory usage, startup time, etc.)
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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: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 notnull
. The bulk of allLabel
's canavoid this call?
To do this:
Write a new
PlatformAppCompatTextView.java
that overrideonLayout()
It only calls
onLayoutFormatted()
if aisFormatted
boolean
field is
true
We can set
isFormatted
if a formatted string is passed in, such as:isFormatted = !(text instanceof String)
With this change in place, the above
MauiTextView.OnLayout()
call iscompletely gone from
dotnet-trace
output. Scrolling the sample also"feels" a bit snappier.
This should improve the performance of all non-formatted
Label
s onAndroid.
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.