-
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
Android shadow performance #26789
base: main
Are you sure you want to change the base?
Android shadow performance #26789
Conversation
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
/rebase |
2a51aee
to
2a218db
Compare
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
2f0b86f
to
eccf37e
Compare
I'm late to the party but an attempt to improve shadow's performance was made before here #10523. Moving the code to the Java side might help but I think what would benefit the most is a shadow cache. All views that have the same size and the same shadow properties (very common scenario in CollectionView items for example) should use the same bitmap. This will eliminate a huge number of calls including C# to Java ones. There is an implementation of the shadow cache in the PR. |
@AmrAlSayed0 thanks for sharing, I'll check it out. |
d40a883
to
5cb0404
Compare
bec5474
to
3178ee0
Compare
3178ee0
to
f74dfb6
Compare
f74dfb6
to
1702ea7
Compare
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
6d5d6ad
to
87c5759
Compare
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
6da5858
to
d01b92c
Compare
float radius = Context.ToPixels(Shadow.Radius); | ||
float offsetX = Context.ToPixels(Shadow.Offset.X); | ||
float offsetY = Context.ToPixels(Shadow.Offset.Y); |
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 calls the Context
property three times in a row. This will call into Java each time, doing the bookkeeping to lookup the C# object.
Can you store Context
in a local to reuse it?
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.
Good catch!
// Use application context to avoid memory leaks | ||
Context applicationContext = context.getApplicationContext(); |
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.
Do you have any details why to use getApplicationContext
?
It looks like the ShadowBitmapPool
is used once per PlatformWrapperView
, which would have the same lifetime as an activity anyway?
If we used a single ShadowBitmapPool
for the entire application, seems like getApplicationContext
would be appropriate in that case.
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.
See comment below: we use only one pool.
public PlatformWrapperView(Context context) { | ||
super(context); | ||
this.viewBounds = new Rect(); | ||
this.bitmapPool = ShadowBitmapPool.get(context); |
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.
So, we use 1 pool per PlatformWrapperView
.
Can we use 1 pool for the entire application? That would allow many views to share the pool.
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.
Actually that static method returns one pool for the entire application, so exactly what you're asking :)
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
d01b92c
to
a9b540b
Compare
/azp run |
Azure Pipelines successfully started running 3 pipeline(s). |
Correct me if I'm wrong but what this is doing is using a pool not a cache. Which means that no 2 views will ever use the same bitmap, it's just that that bitmaps will be reused and recycled instead of being created each time it's needed to better conserve memory. I was thinking of a cache for when, for example, 200 items in a CollectionView request the same exact bitmap cuz they have MeasureFirstItem set (hence same size) and have the same template (hence the same shadow properties) and the same bitmap can be set for both. I know this PR is already doing too much so this maybe an optimization for a later time .. |
@AmrAlSayed0 there's a small "cache" at the view's level which reuses the bitmap content when not But from my analysis, this is probably ineffective as every time something changes on the descendants ( The reason for this invalidation is that the shadow is based on content pixels. This is really a pain as Android doesn't offer a better way to draw a shadow based on the content. What I've done here though, is detecting if you have a shadow applied to a solid rectangle, or a solid Unfortunately there is an edge case not covered, so I'm discussing with the MAUI team what to do. Thanks for your input on this! |
Description of Change
Apparently
DrawShadow
has an absurd impact on the performance.Here's a speedscope from our app sorted by self time descending, also look at total time %!!!
This PR:
Glide
'sLruBitmapPool
to reduce the memory pressure on JavaPaint
only when shadow changesCanvas
API to speedup the shadow rendering performance by a lotsaveLayer
call from platform interop: it's a very expensive operation done for no reasons hereI tried as much as possible to not break signatures but given Java to C# bindings are automatically generated, there was no way completely avoid touching public APIs.
Here I've executed the testing host app with
return new ShadowBenchmark();
as main page and scrolling the CV for ~30 seconds.Before
DrawShadow
takes 24%.before.speedscope.json
After
DrawShadow
is gone.after.speedscope.json
Important notes about the benchmark
Now you may suppose that we've simply hidden the cost of
drawShadow
from the profiler.That is in part true, this is why I've also measured this on a real app by looking at the root
drawChild
performance.You can clearly see a 40% improvement (833ms vs 523ms).
Issues Fixed