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

Reduce allocations while loading deferred resources #15070

Merged

Conversation

MrJul
Copy link
Member

@MrJul MrJul commented Mar 21, 2024

What does the pull request do?

This PR aims to remove most unnecessary allocations related to deferred content inside resources dictionaries.
While loading the Fluent theme, there's a bit less than 1450 deferred items created, and that's normal.
However, they come with several allocations, both very short lived ones, and some longer ones, stored.

This PR removes more than 15000 allocations while loading the FluentTheme, half of which were long lived.

Resuls to load the FluentTheme:

Method Mean Error StdDev Gen0 Gen1 Allocated
FluentTheme (Before) 1.670 ms 0.0192 ms 0.0160 ms 66.4063 64.4531 1.07 MB
FluentTheme (After) 633.4 us 12.60 us 6.59 us 13.6719 12.6953 228.17 KB

Note that it's still the JIT that takes most of the time when starting up the application (use NativeAOT when you can!), but I'll still take those gains :)

As part of this PR, StaticResourceExtension.ProvideValue() was also improved (not that it was slow to begin with):

Method Mean Error StdDev Gen0 Allocated
StaticResource (Before) 238.5 ns 1.45 ns 1.35 ns 0.0153 256 B
StaticResource (After) 160.6 ns 1.50 ns 1.33 ns - -

How was the solution implemented (if it's not obvious)?

All numbers below are for loading the Fluent theme.

Buffer resource nodes

In XamlIlRuntimeHelpers, a reusable buffer is now used to store the parent resources nodes and get an array out of that, without using Linq. The array is stored instead of the list.

Removes ≈4350 allocations:

  • 1450 List<IResourceNode> (long lived)
  • 1450 OfType<IResourceNode> enumerator (short lived)
  • 1450 IResourceNode[] backing array from List having to grow (short lived)

Extra enumerator allocations happening when the deferred resource will be built have also been removed.

Cache default parents

DefaultAvaloniaXamlIlParentStackProvider now caches its parent as an array, since it's only the Application.Current instance, which is very unlikely to change between two calls.

Removes ≈1450 allocations:

  • 1450 Parents enumerator (short lived)

List parents without allocating

A specialized version of IAvaloniaXamlIlParentStackProvider is introduced IAvaloniaXamlIlEagerParentStackProvider: it can iterate the parents without any allocation. This interface is implemented by the runtime XAML context.

Removes ≈3500 allocations:

  • 3500 ParentStackEnumerable.Enumerator (short lived)

As a bonus, using this interface makes StaticResource.ProvideValue() allocation-free.

Avoid delegate allocations

XamlIlRuntimeHelpers.DeferredTransformationFactoryV3 now returns an IDeferredContent instead of a Func<IServiceProvider, object>.
The returned closure directly implements this interface, so we can get rid of the delegate.
ResourceDictionary is now aware of IDeferredContent and can store it directly.

The XAML compiler now passes a function pointer to DeferredTransformationFactoryV3 to build its content instead of creating a delegate.

Removes ≈4350 allocations:

  • 2900 Func<IServiceProvider, object> (long lived)
  • 1450 DeferredItem inside the resource dictionary (long lived)

Reuse parent nodes

DeferredTransformationFactoryV3 reuses the parent resource nodes if they're the same as the last time it was called. This method is often called with the same parents (e.g. all items in the same resource dictionary), so this change provides nice gains. Out of 1450 items in the fluent theme, there are only 20 different lists of parents.

Removes ≈1430 allocations:

  • 1430 IResourceNode[] (long lived)

Set resource dictionary capacity

ResourceDictionary.EnsureCapacity() is called.
This is #15069 which is the only one I could really make separate (all other modifications are intertwined), but that I measured as part of this PR.

Removes ≈30 allocations, but larger ones:

  • 30 Dictionary+Entry[] (short lived)

Notes

ResourceDictionary.AddDeferred(IDeferredContent) and Add(IDeferredContent) are equivalent. I didn't bother with disallowing one or the other, but this might be worth mentioning.

Dependencies

This PR is complete, but is marked as a draft because it contains merges from several other PR:

When these get merged, I will rebase this PR to have a clean diff.

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0046430-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0046841-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul MrJul force-pushed the feature/reduce-xaml-load-allocations branch from 2d38408 to ef26dda Compare April 24, 2024 13:31
@MrJul MrJul marked this pull request as ready for review April 24, 2024 13:32
@avaloniaui-bot
Copy link

You can test this PR using the following package version. 11.2.999-cibuild0047665-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@@ -6,4 +6,11 @@ public interface IAvaloniaXamlIlParentStackProvider
{
IEnumerable<object> Parents { get; }
}

public interface IAvaloniaXamlIlEagerParentStackProvider : IAvaloniaXamlIlParentStackProvider
Copy link
Member

Choose a reason for hiding this comment

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

TODO: we should really avoid XamlIL in public API when possible. Not a job for this PR, as it would end up in breaking changes anyway.

@maxkatz6 maxkatz6 added this pull request to the merge queue Apr 25, 2024
@maxkatz6 maxkatz6 added area-xaml backport-candidate-11.1.x Consider this PR for backporting to 11.1 branch labels Apr 25, 2024
@maxkatz6 maxkatz6 removed this pull request from the merge queue due to a manual request Apr 25, 2024
@maxkatz6 maxkatz6 merged commit a7e1026 into AvaloniaUI:master Apr 25, 2024
10 checks passed
maxkatz6 pushed a commit that referenced this pull request Apr 25, 2024
* Reduce allocations in XamlIlRuntimeHelpers

* Iterate XAML parents without allocations

* Use IDeferredContent for XAML deferred content to reduce allocations

* Use function pointer in DeferredTransformationFactory

* Reuse parent resource nodes if possible for deferred content

* Fix function pointer usage with SRE
@maxkatz6 maxkatz6 added backported-11.1.x and removed backport-candidate-11.1.x Consider this PR for backporting to 11.1 branch labels Apr 25, 2024
@MrJul MrJul deleted the feature/reduce-xaml-load-allocations branch May 1, 2024 14:45
@kekekeks
Copy link
Member

@MrJul @maxkatz6 It seems that this PR has introduced a breaking change. IAvaloniaXamlIlEagerParentStackProvider isn't supported by XAML compiled by Avalonia 11.0.x, so attempting to cast to it will trigger exceptions on XAML load.

@kekekeks
Copy link
Member

You are only doing a type check for the current stack provider, but EagerParentStackEnumerator assumes that all providers in the chain support the new interface

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants