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

Update PropertyValues in TriggerBase/FrameworkElementFactory via reference #10003

Open
wants to merge 8 commits into
base: main
Choose a base branch
from

Conversation

h3xds1nz
Copy link
Contributor

@h3xds1nz h3xds1nz commented Oct 26, 2024

Description

Adds ability to retrieve elements via ref from FrugalStructList<T> and the underlying storage formats, allowing for future optimizations. I intentionally did not modify the indexer as this can be done easily later on, so there's a new method for it now, which will allow for incremental optimizations in future on another types, where it makes sense and is safe to do so.

This allows us to update items via reference, which is particularly useful in case of PropertyValues used in TriggerBase and FrameworkElementFactory since it is a struct that needs to be copied everytime on update back and forth.

It is particularly efficient optimization here since the struct is full of managed pointers and ain't blittable, which takes considerable time to track back and forth for a simple update on one property.

I've also updated the getter/setter on FrugalStructList<T> for a minor speed boost, since it is used in almost every WPF collection, every branch counts. Nothing extra new, this is used in List<T> by default for several releases.

Original getter/setter prolog:

mov rcx, [rcx+8]
test rcx, rcx      
je short L0021 
cmp edx, [rcx+8]     
jge short L0021
test edx, edx
jl short L0021

New getter/setter prolog:

mov rcx, [rcx+8]
test rcx, rcx    
je short L001d 
cmp edx, [rcx+8] 
jae short L001d

Updating PropertyValues before/after

Method Mean [ns] Error [ns] StdDev [ns] Code Size [B] Allocated [B]
Original 459.11 ns 2.278 ns 2.131 ns 1,072 B -
PR__EDIT 58.45 ns 0.464 ns 0.434 ns 469 B -
Minimal benchmark code
[Benchmark]
public static FrugalStructList<PropertyValue> PropertyValues = new();
public static FrugalStructList<PropertyValue> PropertyValues2 = new();

public static IEnumerable<TriggerCondition[]> KeysToTest
{
    get
    {
        yield return new TriggerCondition[] { new TriggerCondition() {  SourceChildIndex = 4}, new TriggerCondition() { SourceChildIndex = 1 }, new TriggerCondition() { SourceChildIndex = 3 } };
    }
}

[GlobalSetup]
public static void Setup()
{
  PropertyValues.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });

  PropertyValues2.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues2.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues2.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues2.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues2.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
  PropertyValues2.Add(new PropertyValue() { ChildName = "abcd", Conditions = new TriggerCondition[5], Property = TextBox.TextProperty, ValueInternal = 45, ValueType = PropertyValueType.Trigger });
}

[ArgumentsSource(nameof(KeysToTest))]
public void Original(TriggerCondition[] triggerCondition)
{
    // Set Condition for all data triggers
    for (int i = 0; i < PropertyValues.Count; i++)
    {
        PropertyValue propertyValue = PropertyValues[i];

        propertyValue.Conditions = triggerCondition;
        switch (propertyValue.ValueType)
        {
            case PropertyValueType.Trigger:
                propertyValue.ValueType = PropertyValueType.DataTrigger;
                break;
            case PropertyValueType.DataTrigger:
                propertyValue.ValueType = PropertyValueType.Trigger;
                break;
            case PropertyValueType.PropertyTriggerResource:
                propertyValue.ValueType = PropertyValueType.DataTriggerResource;
                break;
            default:
                throw new InvalidOperationException("fail");
        }

        // Put back modified struct
        PropertyValues[i] = propertyValue;
    }
}

[Benchmark]
[ArgumentsSource(nameof(KeysToTest))]
public void PR__EDIT(TriggerCondition[] triggerCondition)
{
    // Set Condition for all data triggers
    for (int i = 0; i < PropertyValues2.Count; i++)
    {
        ref PropertyValue propertyValue = ref PropertyValues2.GetEntryAtRef(i);

        propertyValue.Conditions = triggerCondition;
        switch (propertyValue.ValueType)
        {
            case PropertyValueType.Trigger:
                propertyValue.ValueType = PropertyValueType.DataTrigger;
                break;
            case PropertyValueType.DataTrigger:
                propertyValue.ValueType = PropertyValueType.Trigger;
                break;
            case PropertyValueType.PropertyTriggerResource:
                propertyValue.ValueType = PropertyValueType.DataTriggerResource;
                break;
            default:
                throw new InvalidOperationException("fail");
        }
    }
}

Customer Impact

Increased startup performance mostly, but also when dynamically creating triggers at runtime and obviously everything related to ContentPresenter, Validation, ItemsControl templates etc. I could observe about several milliseconds improvement with a simple app with a few controls with ScrollViewer (ListBox) etc.

Regression

No.

Testing

Local build, sample apps testing with styles/triggers to check they're applied property.

Risk

Low-to-medium, reviewers shall check that the ref is not forwarded/used anywhere while the FrugalStructList<T> could be modified (this really means when Grow or for example Remove happens), R/W operations via indexer do not matter as they do not modify the backing store in size/copy it.

Microsoft Reviewers: Open in CodeFlow

@h3xds1nz h3xds1nz requested review from a team as code owners October 26, 2024 21:32
@dotnet-policy-service dotnet-policy-service bot added PR metadata: Label to tag PRs, to facilitate with triage Community Contribution A label for all community Contributions labels Oct 26, 2024
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 PR metadata: Label to tag PRs, to facilitate with triage
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant