dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
21.63k stars 1.62k forks source link

Make ImageSource more async-friendly #22098

Open symbiogenesis opened 1 week ago

symbiogenesis commented 1 week ago

If SemaphoreSlim is used instead of locks, then the stream-based derived types of ImageSource can be more async-friendly.

These derived types are downloading image data from elsewhere, and that data is likely to take some significant time. Thus, the more async-friendly approach makes sense.

This seems most likely to help when the same ImageSource is being referenced in multiple places, which is the current purpose of the locks.

MartyIX commented 1 week ago

It would be great to describe the changes a bit more. Especially, what your goal is bere.

symbiogenesis commented 1 week ago

It would be great to describe the changes a bit more. Especially, what your goal is bere.

Ok, I will add a bit more.

MartyIX commented 1 week ago

btw: I guess a bit unrelated but I have noticed this line:

https://github.com/dotnet/maui/blob/48d5ebf359ea22bef8ade4108b275246caa0c5b2/src/Core/src/Platform/ImageSourcePartLoader.cs#L71

and I wonder why ConfigureAwait(false) is used there when

https://github.com/dotnet/maui/blob/48d5ebf359ea22bef8ade4108b275246caa0c5b2/src/Core/src/Platform/ImageSourcePartLoader.cs#L73

looks like it requires to be called by the UI thread rather than some random thread.

symbiogenesis commented 1 week ago

btw: I guess a bit unrelated but I have noticed this line:

https://github.com/dotnet/maui/blob/48d5ebf359ea22bef8ade4108b275246caa0c5b2/src/Core/src/Platform/ImageSourcePartLoader.cs#L71

and I wonder why ConfigureAwait(false) is used there when

https://github.com/dotnet/maui/blob/48d5ebf359ea22bef8ade4108b275246caa0c5b2/src/Core/src/Platform/ImageSourcePartLoader.cs#L73

looks like it requires to be called by the UI thread rather than some random thread.

seems to come from this PR https://github.com/dotnet/maui/pull/2352 by @PureWeen

My understanding is that ConfigureAwait(false) needs to be set all the way up the chain for it to be in effect. Any breaks in that, and it will revert to standard behavior.

Given that every time UpdateImageSourceAsync() is invoked in the codebase, it is not invoked with ConfigureAwait(false) that means that the setting has no effect within the codebase. Although, it is a public method and anyone could call it.

But, I suppose that having it set that way opens up the possibility for it to be called with ConfigureAwait(false) and have that work as expected. But if it is really needing the UI thread then probably doing that would not be desirable. Maybe there is some kind of headless test case where it makes sense.

MartyIX commented 1 week ago

My understanding is that ConfigureAwait(false) needs to be set all the way up the chain for it to be in effect. Any breaks in that, and it will revert to standard behavior.

That's not how async work. You can test the behavior by running this unit test:

[Fact]
public async Task TestAsync()
{
    System.Diagnostics.Debug.WriteLine($"{nameof(TestAsync)} [1] ThreadId={Environment.CurrentManagedThreadId}");

    await DeepAsync();

    System.Diagnostics.Debug.WriteLine($"{nameof(TestAsync)} [2] ThreadId={Environment.CurrentManagedThreadId}");
}

public async Task DeepAsync()
{
    System.Diagnostics.Debug.WriteLine($"{nameof(DeepAsync)} [1] ThreadId={Environment.CurrentManagedThreadId}");

    await DeeperAsync();

    System.Diagnostics.Debug.WriteLine($"{nameof(DeepAsync)} [2] ThreadId={Environment.CurrentManagedThreadId}");
}

public async Task DeeperAsync()
{
    System.Diagnostics.Debug.WriteLine($"{nameof(DeeperAsync)} [1] ThreadId={Environment.CurrentManagedThreadId}");

    await Task.Delay(1000).ConfigureAwait(false);

    System.Diagnostics.Debug.WriteLine($"{nameof(DeeperAsync)} [2] ThreadId={Environment.CurrentManagedThreadId}");
}

it prints:

TestAsync [1] ThreadId=16
DeepAsync [1] ThreadId=16
DeeperAsync [1] ThreadId=16
DeeperAsync [2] ThreadId=9 <-- This is what I'm talking about
DeepAsync [2] ThreadId=16
TestAsync [2] ThreadId=17
symbiogenesis commented 1 week ago

My understanding is that ConfigureAwait(false) needs to be set all the way up the chain for it to be in effect. Any breaks in that, and it will revert to standard behavior.

That's not how async work. You can test the behavior by running this unit test:

I like your unit test explanation. But, does SourceManager.CompleteLoad() really need the UI thread? It seems to just be calling Dispose() on a field, and two setters that have no logic.

public void CompleteLoad(IDisposable? result)
{
    _sourceResult = result;
    _sourceCancellation?.Dispose();
    _sourceCancellation = null;

    IsResolutionDependent = false;
    CurrentResolution = 1.0f;
}
MartyIX commented 1 week ago

The way I think about it is:

So my best guess is that it works because it's not exercised too much and it's prone to introducing bugs in the future.

symbiogenesis commented 1 week ago

The way I think about it is:

  • Does it fail miserably? No.
  • Is it correct? It seems it isn't because there is no synchronization (no locks, etc.)

So my best guess is that it works because it's not exercised too much and it's prone to introducing bugs in the future.

It is possible there aren't enough locks in those other files, but I just checked and it appears that they are only instantiated within handlers for individual controls, and triggered on events like property mapping. So perhaps that is why the ImageSourcePartLoader and ImageSourceServiceResultManager do not currently have locks.

By contrast, ImageSource itself is something that you could bind to more than one bindable property.

jsuarezruiz commented 1 week ago

/azp run

azure-pipelines[bot] commented 1 week ago
Azure Pipelines successfully started running 3 pipeline(s).
jsuarezruiz commented 1 day ago

/azp run

azure-pipelines[bot] commented 1 day ago
Azure Pipelines successfully started running 3 pipeline(s).