Skip to content
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

Improve performance of DynamicResource usages #5610

Merged
merged 7 commits into from
Jun 25, 2024

Conversation

batzen
Copy link
Contributor

@batzen batzen commented Oct 30, 2021

Fixes Issue #4468

Description

The currently used implementation to keep track of DynamicResource usages is suboptimal as the list can grow quite large and scanning gets very expensive.
That's caused by two issues:

  • Every call to FetchResource adds a new entry to the list
  • The list implementation is slow as it does not use keys

The improved implementation only adds items with the same key once by checking for existing items before adding new ones. As the new implementation uses keys, retrieval of items is very fast.

Customer Impact

Slow performance.

Testing

Tested the app from #4468

@batzen batzen requested a review from a team as a code owner October 30, 2021 18:19
@ghost ghost added the PR metadata: Label to tag PRs, to facilitate with triage label Oct 30, 2021
@ghost ghost requested review from fabiant3, ryalanms and SamBent October 30, 2021 18:19
@batzen
Copy link
Contributor Author

batzen commented Mar 12, 2022

@dotnet/wpf-developers any news on this one?

@batzen batzen added the Community Contribution A label for all community Contributions label May 5, 2022
@ghost ghost assigned batzen May 5, 2022
@batzen batzen added the Performance Performance related issue label May 5, 2022
Copy link
Member

@RussKie RussKie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you thought about how it can be tested? How did you confirm the performance is improved?

@RussKie RussKie added the 📭 waiting-author-feedback To request more information from author. label Aug 26, 2022
@ghost ghost removed the 📭 waiting-author-feedback To request more information from author. label Dec 15, 2022
@batzen
Copy link
Contributor Author

batzen commented Dec 15, 2022

Have you thought about how it can be tested? How did you confirm the performance is improved?

I asked about how we are expected to write tests in multiple of my PRs, but never got an answer...

Performance improvements were checked by testing the app from #4468.

@pchaurasia14 pchaurasia14 merged commit 38a157c into dotnet:main Jun 25, 2024
8 checks passed
@pchaurasia14
Copy link
Member

Thanks @batzen !
Sorry, this took long to get merged.

@lightwalker01
Copy link

@batzen Contratulations, you finally got it! 😉👍🏼🍾

@znakeeye
Copy link

In which release can we expect this fix?

@lindexi
Copy link
Contributor

lindexi commented Jun 25, 2024

@znakeeye I think it will enter .NET 9.

@harshit7962
Copy link
Member

@batzen, we have encountered a regression due to these changes. As it turns out, we are not expecting a null resourceKey here, but we are validating the same check here. This results in an ArgumentNullException. Is expecting a non-null value as resourceKey thought of? and if so, the validation checks no longer seems to be valid.

Please find the sample repro here. Try running the Wpf.Ui.Gallery application and it should throw the above mentioned exception. It is excepted to fail with .NET9, preview 7 binaries.

@batzen
Copy link
Contributor Author

batzen commented Jul 8, 2024

@harshit7962 Will have a look as soon as i can.

@batzen
Copy link
Contributor Author

batzen commented Jul 13, 2024

Created PR #9393 to fix the issues

@harshit7962
Copy link
Member

@batzen, thank for your fix earlier, but even with the fix we are yet to close a few regression issues that we got to know of recently. I have created a sample repro here. The application is expected to crash with preview 7 (or rc1) binaries of .NET 9.

Apologies for this iterative reporting, hopefully we will be closing these in a single go this time.

@batzen
Copy link
Contributor Author

batzen commented Jul 29, 2024

@harshit7962 The first error i encounter is an exception because a SolidColorBrush is being used as a Color inside of ButtonStyles.xaml in line 2 where it says <SolidColorBrush x:Key="Menu.Static.Background" Color="{DynamicResource SolidBackgroundFillColorSecondaryBrush}" />.

This was also an error before my changes, but the exception was thrown at a different point in time.
As the line was changed in Text-Grab last year and no one seems to have noticed that the background stays transparent because the resource has the wrong type, IMHO the new behavior is much better as the error would have been very obvious.

Before my changes the exception was thrown during rendering, caught there and the program just continued:

Unhandled exception: System.InvalidOperationException: '#FF1C1C1C' is not a valid value for property 'Color'.
   at System.Windows.DependencyObject.GetEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, RequestFlags requests)
   at System.Windows.DependencyObject.GetValueEntry(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, RequestFlags requests)
   at System.Windows.DependencyObject.GetValue(DependencyProperty dp)
   at System.Windows.Media.SolidColorBrush.get_Color()
   at System.Windows.Media.SolidColorBrush.UpdateResource(Channel channel, Boolean skipOnChannelCheck)
   at System.Windows.Media.SolidColorBrush.AddRefOnChannelCore(Channel channel)
   at System.Windows.Media.Brush.System.Windows.Media.Composition.DUCE.IResource.AddRefOnChannel(Channel channel)
   at System.Windows.Media.RenderData.System.Windows.Media.Composition.DUCE.IResource.AddRefOnChannel(Channel channel)
   at System.Windows.UIElement.RenderContent(RenderContext ctx, Boolean isOnChannel)
   at System.Windows.Media.Visual.UpdateContent(RenderContext ctx, VisualProxyFlags flags, Boolean isOnChannel)
   at System.Windows.Media.Visual.RenderRecursive(RenderContext ctx)
   at System.Windows.Media.Visual.UpdateChildren(RenderContext ctx, ResourceHandle handle)
   at System.Windows.Media.Visual.RenderRecursive(RenderContext ctx)
   at System.Windows.Media.Visual.UpdateChildren(RenderContext ctx, ResourceHandle handle)
   at System.Windows.Media.Visual.RenderRecursive(RenderContext ctx)
   at System.Windows.Media.Visual.Render(RenderContext ctx, UInt32 childIndex)
   at System.Windows.Media.CompositionTarget.Compile(Channel channel)
   at System.Windows.Media.CompositionTarget.System.Windows.Media.ICompositionTarget.Render(Boolean inResize, Channel channel)
   at System.Windows.Media.MediaContext.Render(ICompositionTarget resizedCompositionTarget)
   at System.Windows.Media.MediaContext.RenderMessageHandlerCore(Object resizedCompositionTarget)
   at System.Windows.Media.MediaContext.RenderMessageHandler(Object resizedCompositionTarget)
   at System.Windows.Media.MediaContext.Resize(ICompositionTarget resizedCompositionTarget)
   at System.Windows.Interop.HwndTarget.OnResize()
   at System.Windows.Interop.HwndTarget.HandleMessage(WindowMessage msg, IntPtr wparam, IntPtr lparam)
   at System.Windows.Interop.HwndSource.HwndTargetFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
   at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
   at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
   at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)

After my changes the exception is thrown during BAML processing:

Unhandled exception. System.Windows.Markup.XamlParseException: 'Set property 'System.Windows.ResourceDictionary.Source' threw an exception.' Line number '17' and line position '18'.
 ---> System.InvalidOperationException: '#FF1C1C1C' is not a valid value for property 'Color'.
   at System.Windows.DependencyObject.EvaluateExpression(EntryIndex entryIndex, DependencyProperty dp, Expression expr, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry newEntry) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs:line 1809
   at System.Windows.DependencyObject.EvaluateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry newEntry, OperationType operationType) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs:line 1939
   at System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs:line 1348
   at System.Windows.DependencyObject.InvalidateProperty(DependencyProperty dp, Boolean preserveCurrentValue) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs:line 1210
   at System.Windows.DependencyObject.InvalidateProperty(DependencyProperty dp) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs:line 1179
   at System.Windows.ResourceReferenceExpression.InvalidateTargetProperty(Object sender, EventArgs e) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\ResourceReferenceExpression.cs:line 388
   at System.Windows.ResourceReferenceExpression.InvalidateExpressionValue(Object sender, EventArgs e) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\ResourceReferenceExpression.cs:line 383
   at System.Windows.DependencyObject.OnInheritanceContextChanged(EventArgs args) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs:line 2649
   at System.Windows.Freezable.AddInheritanceContext(DependencyObject context, DependencyProperty property) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\Freezable.cs:line 1180
   at System.Windows.DependencyObject.ProvideSelfAsInheritanceContext(DependencyObject doValue, DependencyProperty dp) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs:line 877
   at System.Windows.DependencyObject.ProvideSelfAsInheritanceContext(Object value, DependencyProperty dp) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\WindowsBase\System\Windows\DependencyObject.cs:line 850
   at System.Windows.ResourceDictionary.AddInheritanceContext(DependencyObject inheritanceContext, Object value) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\ResourceDictionary.cs:line 2306
   at System.Windows.ResourceDictionary.AddInheritanceContextToValues() in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\ResourceDictionary.cs:line 2335
   at System.Windows.ResourceDictionary.set_Source(Uri value) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\ResourceDictionary.cs:line 261
   at System.Windows.Baml2006.WpfSharedBamlSchemaContext.<>c.<Create_BamlProperty_ResourceDictionary_Source>b__344_0(Object target, Object value) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\Markup\Baml2006\WpfGeneratedKnownProperties.cs:line 7535
   at System.Windows.Baml2006.WpfKnownMemberInvoker.SetValue(Object instance, Object value) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\Markup\Baml2006\WpfKnownMemberInvoker.cs:line 44
   at MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetValue(XamlMember member, Object obj, Object value) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\System.Xaml\System\Xaml\Runtime\ClrObjectRuntime.cs:line 298
   at MS.Internal.Xaml.Runtime.ClrObjectRuntime.SetValue(Object inst, XamlMember property, Object value) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\System.Xaml\System\Xaml\Runtime\ClrObjectRuntime.cs:line 284
   --- End of inner exception stack trace ---
   at System.Windows.Markup.XamlReader.RewrapException(Exception e, IXamlLineInfo lineInfo, Uri baseUri) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\Markup\XamlReader.cs:line 509
   at System.Windows.Markup.WpfXamlLoader.Load(XamlReader xamlReader, IXamlObjectWriterFactory writerFactory, Boolean skipJournaledProperties, Object rootObject, XamlObjectWriterSettings settings, Uri baseUri) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\Markup\WpfXamlLoader.cs:line 175
   at System.Windows.Markup.WpfXamlLoader.LoadBaml(XamlReader xamlReader, Boolean skipJournaledProperties, Object rootObject, XamlAccessLevel accessLevel, Uri baseUri) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\Markup\WpfXamlLoader.cs:line 43
   at System.Windows.Markup.XamlReader.LoadBaml(Stream stream, ParserContext parserContext, Object parent, Boolean closeStream) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\Markup\XamlReader.cs:line 1095
   at System.Windows.Application.LoadComponent(Object component, Uri resourceLocator) in C:\DEV\OSS_Own\dotnet\wpf_my_prs\src\Microsoft.DotNet.Wpf\src\PresentationFramework\System\Windows\Application.cs:line 468
   at Text_Grab.App.InitializeComponent() in C:\DEV\issues\wpf\5610\Text-Grab\Text-Grab\App.xaml:line 1
   at Text_Grab.App.Main()

@harshit7962
Copy link
Member

@batzen Agreed that the exception being thrown is the correct way, however this creates a compat problem for applications that currently use DynamicResource and rely on the default behaviour (not crashing) even when incorrect value is provided.
With this fix currently all those apps would not be able to update to .NET 9 without XAML changes on their end.
I think we should find a way to ensure backward compatibility in this case.

harshit7962 added a commit to harshit7962/wpf that referenced this pull request Jul 29, 2024
@miloush
Copy link
Contributor

miloush commented Jul 29, 2024

Can someone clarify what the migration expectations to newer versions of WPF are in general? Why are we expecting .NET Core 3 apps to run on .NET 9 without any change? Apps that cannot update to .NET 9 without recompilation can stay on .NET 8, that's why we have runtime config to specify which runtime should be used.

@h3xds1nz
Copy link
Contributor

h3xds1nz commented Jul 29, 2024

I agree with @miloush here as I've been asking myself the same question.

This issue is also the prime example, essentially having a bug in your code that didn't only cause a crash thanks to the exception being swallowed. It just makes no sense in my opinion. And there's a history of such changes happening from version to version outside WPF, don't see why it should be any different when it's a step in the right direction.

@harshit7962
Copy link
Member

@batzen, we are trying to evaluate the regression radius of the changes at the moment. I have created a minimal repro of the above application here. The following is my findings as of now.

The crash is reported only when we include the following in our csproj.

<ItemGroup>
    <Resource Include="ButtonStyles.xaml">
        <Generator>MSBuild:Compile</Generator>
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Resource>
</ItemGroup>

Need your help in understanding this change in behavior.

Tagging @miloush and @h3xds1nz for their feedbacks and opinions.

@miloush
Copy link
Contributor

miloush commented Jul 30, 2024

Is this only XAML parsing issue or runtime too? (Since it's a dynamic resource, a wrong type can be provided at runtime. If that now crashes and didn't used to, it would be even more concerning than the XAML parsing which is apparent immediately)

@h3xds1nz
Copy link
Contributor

h3xds1nz commented Jul 30, 2024

  • I've tested the same stupid thing against .NET 8 and it will crash in runtime (when you set a resource ref) the same way it will crash with this PR in latest master, same spot, same call-stack; when running EvaluateExpression with InvalidPropertyValue
  • So the issue is indeed only when parsing BAML/XAML; and imho now it crashes in the correct spot and it is the correct behavior. Even IntelliSense is gonna be angry at you once you execute it.

What I fail to understand in detail because I can't debug the previous version today is how did this change happened but I can imagine that the value was already DeferredReference at that point and that's why it didn't crash while evaluating.

@miloush
Copy link
Contributor

miloush commented Jul 30, 2024

I agree that these are errors in the applications and not WPF, and I fully support the early, compile-time failure. However, already during one preview release there is evidence this change breaks things - and that doesn't happen very often, it is the reason why preview releases exist in the first place. It is also very late in the release cycle and I doubt any further changes to the codebase will be entertained for 9 without giving people an opportunity to test them.

I wonder whether we could have it failing compilation forward but not crash applications if they run on a newer runtime than they have been compiled with?

@RussKie
Copy link
Member

RussKie commented Jul 30, 2024

Control the behaviour with a feature switch.

@miloush
Copy link
Contributor

miloush commented Jul 30, 2024

Switch is my least favorite option, we keep piling (often unnecessary) switches and never remove them. I am also not quite sure how switch would achieve a feature to be available at compile time but not at runtime (and there might be not enough time to test the switch before RTM). I believe the agreement is we do want the changes in this PR to be part of the product and if the intent is to update the PR to avoid breaking these apps without a switch, then the PR can be updated and merged back later, unless there is an urgency that I missed.

(On the other hand you could say setting a .NET runtime version in the config is a switch.)

@RussKie
Copy link
Member

RussKie commented Jul 31, 2024

Feature switches are used at runtime.
They could probably be used at design-time, but that would be a misuse of feature switches IMO.

@RussKie
Copy link
Member

RussKie commented Jul 31, 2024

You can learn more at https://learn.microsoft.com/dotnet/core/runtime-config/.

@harshit7962
Copy link
Member

  • I've tested the same stupid thing against .NET 8 and it will crash in runtime (when you set a resource ref) the same way it will crash with this PR in latest master, same spot, same call-stack; when running EvaluateExpression with InvalidPropertyValue

@h3xds1nz, regarding this, I would like to clarify the exact scenario. The application will not crash in .NET 8, if we do not include the brush SolidBackgroundFillColorSecondaryBrush (used here) directly in the project's resource dictionary (local) .

Instead if it is referenced from a resource dictionary not directly included with our application. In the repro, it is included via the Fluent dictionary here.

Since the above mentioned dictionary is not available in .NET 8, I have tweaked the above repro to load the wpf.ui styles which should make the difference between .NET 8 and .NET 9 more evident.

The mentioned behavior is present in the application here. The application is expected to launch in .NET 8 and crash with XamlParseException on .NET 9.

@h3xds1nz
Copy link
Contributor

h3xds1nz commented Jul 31, 2024

@h3xds1nz, regarding this, I would like to clarify the exact scenario. The application will not crash in .NET 8, if we do not include the brush SolidBackgroundFillColorSecondaryBrush (used here) directly in the project's resource dictionary (local) .

Sorry, it was a direct reply to miloush's question, when you try to set an invalid resource reference type during runtime on FrameworkElement f.e. The behaviour there is the same. As I've further continued, the only difference before and after PR is what we've confirmed, that is that the BAML/XAML parsing throws a XamlParseException already instead of it being swallowed.

@pchaurasia14
Copy link
Member

Can someone clarify what the migration expectations to newer versions of WPF are in general?

Actually, we would like to avoid breaking people as much as possible. And the places we would like to break are the ones that have good risk v/s rewards ratio.

Given the current state where applications having incorrect DynamicResource declaration would start to fail at runtime (where it didn't use to fail in earlier releases), this may create blocker for people wanting to migrate to .NET 9. The problem will be compounded if your app ends up using a library that provided a bad DynamicResource xaml.

We absolutely love this change, and we don't want to defer this change for next .NET release. As a stop-gap measure, we would be proceeding with adding an opt-out switch for .NET 9 release only.

We realize this is not ideal and having an opt-out for every single feature is certainly not the way we want our product to evolve. However, we are currently very close to release deadline for .NET 9 and I'm afraid that any more behavior changes at this point may create compat problems that we may not have enough time to fix.

@h3xds1nz
Copy link
Contributor

h3xds1nz commented Aug 1, 2024

@pchaurasia14 Thank you, being it an opt-out switch for .NET 9 release only is very much an acceptable solution that is consistent with the time constraints for .NET 9 in my opinion.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Community Contribution A label for all community Contributions Performance Performance related issue PR metadata: Label to tag PRs, to facilitate with triage
Projects
None yet
Development

Successfully merging this pull request may close these issues.