Very slow performance due to internal WPF deferred resource reference list
- Dominant language
- C#
- Stars
- 7.7k
- Forks
- 1.3k
- Avg merge
- 1d 11h
- Merged PRs (30d)
- 61
Description
* .NET Core Version: 5.0.5
* Windows version: 10.0.19042
* Does the bug reproduce also in WPF for .NET Framework 4.8?: Yes
* Is this bug related specifically to tooling in Visual Studio (e.g. XAML Designer, Code editing, etc...)? No
**Problem description:**
Opening, closing and moving between views in a WPF application becomes extremely slow.
**Actual behavior:**
Debugging tools show that considerable time is spent in [FindWeakReference()](https://source.dot.net/WindowsBase/R/97683fbd5ceeb8ca.html) iterating over items in [ResourceDictionary._deferredResourceReferences](https://source.dot.net/PresentationFramework/R/256cf1aca0bb8160.html).

The list accumulates tens of thousands of entries.
**Expected behavior:**
No delays as described above. From browsing the source code, we believe that entries in `_deferredResourceReferences` are intended to be short lived.
**Minimal repro:**
https://github.com/oatkins/WpfDeferredResourceLookupRepro/tree/repro
The above repro is a bit contrived, but reproduces delays of the same order of magnitude as we are seeing in a real LOB application. It seems that all of the following are needed to reproduce this:
1. Controls whose style has dependency properties set to `DynamicResource` references.
2. Some of those dependency properties supply values that are not used straight away or at all. (This apparently causes evaluation of the property to be deferred).
3. At least one resource is redefined: that is, in the resource dictionaries merged into the application's resources, it is defined in more than one place.
4. For the controls referenced in 1. above, at least one other dependency property that _is_ used is set using a `DynamicResource` reference.
Steps 1-3 cause the list of `_deferredResourceReferences` to become long, but this doesn't actually affect performance without step 4.
This is easy to reproduce using the popular [MahApps.Metro](https://github.com/MahApps/MahApps.Metro) UI toolkit for the following reasons:
- It uses dynamic resources extensively to support theme switching [1 and 4 above].
- [Some styles](https://github.com/MahApps/MahApps.Metro/blob/a636cc8295742a230682ad7c5033cf4520b0916c/src/MahApps.Metro/Styles/Controls.CheckBox.xaml#L291) adopt a pattern that involves setting _many_ dependency properties [2 above].
- Some dependency properties are rarely accessed, e.g. [Validation.ErrorTemplate](https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.validation.errortemplate?view=net-5.0) and [ContextMenuService.ContextMenu](https://docs.microsoft.com/en-us/dotnet/api/system.windows.controls.contextmenuservice.contextmenu?view=net-5.0#System_Windows_Controls_ContextMenuService_ContextMenu) [2 above].
- It's common to import all the MahApps styles into the application's resources, then override some of them with customized versions [3 above].
In our application, this performance bottleneck is typically responsible for adding an additional 7 seconds to the time it takes to open a form, bringing the total time to 10 seconds. It accounts for an even longer delay closing the same form.
Note that, while an application using MahApps is particularly likely to exhibit this behaviour, I'm reporting it here because its quite easily reproducible in an application that doesn't deviate from normal WPF coding practices.
We are hoping to work around this by modifying our styles (and overriding default MahApps styles) to reduce the number of dependency properties that are assigned a value using `DynamicResource` when that value is not read almost immediately. Our application currently runs on .NET Framework 4.8, but we hope, in time, to migrate it to .NET 5 or 6. However, the repro linked above shows that the problem is currently manifest in both flavours of WPF.
This issue is to do a few things:
- Track discussion about whether this could be improved in WPF itself.
- Is it expected that the list of deferred resources should grow to contain tens of thousands of items? Is retaining these necessary in this situation, or could the code be adjusted to release them earlier?
- If not, could the list be re-implemented as some kind of hash set to reduce the time spent in `FindWeakReference()`?
- Solicit any other suggestions on how we might avoid hitting this scenario.
- We realise that changing this in WPF itself might be considered high risk for you (especially in .NET Framework), but we'd appreciate your observations, especially on whether this is a "known issue" with WPF.
Contributor guide
Assessment
This issue has not been assessed yet.