-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Remove Custom Cache in ImageEx to evaluate binary impact size #3736
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
Conversation
Thanks michael-hawker for opening a Pull Request! The reviewers will test the PR and highlight if there is any conflict or changes required. If the PR is approved we will proceed to merge the pull request 🙌 |
e46b371
to
508a3bb
Compare
Too Large an Impact on App Binary Size 675kb
508a3bb
to
e24db50
Compare
95d819e
to
c43f68a
Compare
Validated this fix does produce the validated savings in the final compiled binary. Just need to decide if we take it. Otherwise good to review. |
Microsoft.Toolkit.Uwp.UI.Controls.Core/ImageEx/ImageExBase.Source.cs
Outdated
Show resolved
Hide resolved
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 good. If you want to openup custom cashing strategies to users in this PR or another, I think both work fine.
…sing ImageExBase/ImageEx Removed class level fields which were really local variables. Made new virtual methods. Still works fine in Sample App.
Can now use the sample button as a toggle. Close button is now visible. Made grid taller so sample is more apparent on high resolution monitors.
/// </example> | ||
/// <param name="imageUri"><see cref="Uri"/> of the image to load from the cache.</param> | ||
/// <returns><see cref="Task"/></returns> | ||
protected virtual Task ProvideCachedResourceAsync(Uri imageUri) |
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.
Should I rename this as AttachCachedResourceAsync
? Or should I have it return the resource and call AttachSource
in the parent method? Thoughts?
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 am leaning towards the override method returns the resource. I think it might be simpler for the consumer and for the implementation.
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.
Would it be something like
protected virtual ImageSource GetSource(Uri image)
{
return new BitmapImage(image);
}
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 guess the control would have to call AttachSource
it's self. Would the control have to handle the lock? Would that be a good thing, rather then let the consumer deal with 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.
Looks good, I think the docs on ProvideCachedResourceAsync
will be helpful.
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 nice, and love how these changes will keep the same flexibility but reduce the file size a lot! Just left a few minor observations and ideas, but overall it looks good 😄
/// <summary> | ||
/// Gets value tracking the currently requested source Uri. This can be helpful to use when implementing <see cref="AttachCachedResourceAsync(Uri)"/> where loading an image from a cache takes longer and the current container has been recycled and is no longer valid since a new image has been set. | ||
/// </summary> | ||
protected Uri CurrentSourceUri { get; private set; } |
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 not 100% convinced about this property 🤔
As in, it seems to me that this is only mean to be used by inheriting classes overriding the loading method to check against the Uri
they have as an argument, to detect whether the image has been changed while they were still loading. In that case wouldn't it be clearer and potentially less error prone to instead just keep a private field of type CancellationTokenSource
, and pass a token to that virtual LoadCachedImageAsync
method? That was people implementing that would just need to check whether or not the token is canceller, or they could just as easily just call [CancellationToken.ThrowIfCancellationRequested(https://docs.microsoft.com/en-us/dotnet/api/system.threading.cancellationtoken.throwifcancellationrequested?view=net-5.0), since the corresponding exception is already property handled by the callee anyway. The control would just need to cancel the token whenever the image is changed, and then assign a new token source if needed and pass the token downstream.
Thoughts?
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.
Yup, sounds good.
/// Method to call to assign an <see cref="ImageSource"/> value to the underlying <see cref="Image"/> powering <see cref="ImageExBase"/>. | ||
/// </summary> | ||
/// <param name="source"><see cref="ImageSource"/> to assign to the image.</param> | ||
protected void AttachSource(ImageSource source) |
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 method is called by one of the earlier conditional branches in the method SetSource
below, but it's not resetting the local CurrentSourceUri
property. Doesn't this mean that technically the image might load an image synchronously from there while at the same time a previous cached image loading is still asynchronously processing, which would end up overriding the final image once it completes? Since the CurrentSourceUri
property would still be the same, so that overridden method wouldn't be able to detect a change in the control state 🤔
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 handled with the Token.
} | ||
|
||
await LoadImageAsync(_uri); | ||
await LoadImageAsync(CurrentSourceUri); |
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 await call is done in an async void
method, and the callee doesn't have a try/catch
block - if there was an exception the whole process would likely crash. Is this intentional, or would we want to add a safety path there to instead just display nothing and raise the ImageFailed
event and corresponding visual state?
I see that the various try/catch
blocks are mentioned in the docs for the custom LoadCachedImageAsync
instead, what if we used them here instead so that they'd automatically work for overridden methods too, but they'd also at the same time shield the control from other possible crashes in the default implementation?
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.
Done, we handle everything now. I did find that the original code was calling open/failed twice, as that's handled automatically by us attaching the source, so that should be all cleaned-up now too in the new commit.
/// catch (OperationCanceledException) | ||
/// { | ||
/// // nothing to do as cancellation has been requested. | ||
/// } | ||
/// catch (Exception e) | ||
/// { | ||
/// lock (LockObj) |
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.
Just a small note to remind to update this XML docs and remove the try/catch
logic if we do decide to go with the previous idea and to move this control flow logic up one scope, since consumers here wouldn't need to repeat this anymore at that point. 🙂
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.
Updated!
…attern Automatically handle failure cases for image loading, added comment to clarify events. Do better type checks & cleaned-up calls to type conversions
This PR has been marked as "needs attention 👋" and awaiting a response from the team. |
Hello @michael-hawker! Because this pull request has the p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (
|
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 good!
Left another couple of nitpicks but I'm still approving this as I really don't want to block this just because I found two more minor details to possibly improve, they're nothing major anyway 😄
The refactoring itself and the new way the control works are great! 🚀
|
||
this._tokenSource = new CancellationTokenSource(); | ||
_tokenSource = new CancellationTokenSource(); |
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.
Nit: we could move the initialization of this instance down below after our synchronous paths are taken, so that in case the image is loaded synchronously instead we can skip the allocation here entirely. Basically we really only need to create this if ProvideCachedResourceAsync
is called, otherwise we don't use the token anywhere else 🙂
|
||
this._tokenSource = new CancellationTokenSource(); | ||
_tokenSource = new CancellationTokenSource(); | ||
|
||
AttachSource(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.
Question: is toggling this visual state unconditionally by design? Because if the input is not null
, doing this here no matter what means we're basically always cycling between loaded -> unloaded -> loading -> loaded whenever a new image is displayed.
I'm thinking maybe we could skip a couple of visual states here and:
- Only switch to the unloaded state when we're actually setting a
null
source - Only set the loading state if we're loading an image asynchronously (that is, in the first two paths of our
LoadImageAsync
method.
It's a minor change but in theory it might help with performance a tiny bit in cases where we have a lot of controls being displayed eg. in a virtualized list, as those would be toggling the target image all the time?
Just an idea 😄
Thanks @Sergio0694 I think this is a good first step, we can follow-on, add some tests, and optimize the states as a follow-on. |
Quick test to evaluate the footprint of this individual control on the package.
Based off PR #3727
Comparing against #3733, wondering if it's this single API or not that has an effect, as everything else seems rather common or using other things already being used in other APIs in the Toolkit...
Updated as we identified the custom caching in #2486 is the culprit to the application footprint. Reverting to just use System Cache with the applied fix still to evaluate final impact again with compiled dlls.
This should save around 675kb to optimized app footprint.
Also Fixes #3741 removing unused element.