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

[controls] Brush.Foo should return immutable instances #3824

Merged
merged 1 commit into from
Dec 22, 2021

Conversation

jonathanpeppers
Copy link
Member

@jonathanpeppers jonathanpeppers commented Dec 21, 2021

Description of Change

When profiling a dotnet new maui app, with this package:

https://github.com/jonathanpeppers/Mono.Profiler.Android

The alloc report shows:

Allocation summary
Bytes      Count  Average Type name
39984        147 2 72     Microsoft.Maui.Controls.SolidColorBrush

Stack trace:

38352 bytes from:
(wrapper runtime-invoke) object:runtime_invoke_void (object,intptr,intptr,intptr)
Microsoft.Maui.Controls.VisualElement:.cctor ()
(wrapper runtime-invoke) object:runtime_invoke_void (object,intptr,intptr,intptr)
Microsoft.Maui.Controls.Brush:.cctor ()

Reviewing the Brush class, there are indeed 147 SolidColorBrush
created on startup that are stored in fields.

But what is weird about this, is that SolidColorBrush is mutable!

public Color Color
{
    get => (Color)GetValue(ColorProperty);
    set => SetValue(ColorProperty, value);
}

So I could literally write code like:

Brush.Blue.Color = Colors.Red;

Blue is red! (insert evil laughter?)

I think the appropriate fix here is that all of these static readonly fields should just be properties that return a new
ImmutableBrush. We can cache the values in fields on demand. Then
someone can't do something evil like change Blue to Red?

I reviewed WPF source code to check what they do, and they took a
similar approach:

https://github.com/dotnet/wpf/blob/5e8187344b2b561ef08b9ca2735cd89cbdd3c11e/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/brushes.cs#L33-L1586

We should make this API change now before MAUI is stable, and we have
the side benefit to save 39984 bytes of memory on startup?

I added tests for these scenarios, and discovered 3 typos for Brush
colors that listed the wrong color.

Additions made

  • static readonly fields in Brush are now properties that return a new instance.

PR Checklist

  • Targets the correct branch
  • Tests are passing (or failures are unrelated)
  • Targets a single property for a single control (or intertwined few properties)
  • Adds the property to the appropriate interface
  • Avoids any changes not essential to the handler property
  • Adds the mapping to the PropertyMapper in the handler
  • Adds the mapping method to the WinUI, Android, iOS, and Standard aspects of the handler
  • Implements the actual property updates (usually in extension methods in the Platform section of Core)
  • Tags ported renderer methods with [PortHandler]
  • Adds an example of the property to the sample project (MainPage)
  • Adds the property to the stub class
  • Implements basic property tests in DeviceTests

Does this PR touch anything that might affect accessibility?

No

@jonathanpeppers jonathanpeppers marked this pull request as ready for review December 21, 2021 17:27
@jonathanpeppers jonathanpeppers added the legacy-area-perf Startup / Runtime performance label Dec 21, 2021
@PureWeen
Copy link
Member

PureWeen commented Dec 21, 2021

@jonathanpeppers is there any value here with storing these on access into a local Dictionary or variable so they don't get allocated on every call? I realize there's the issue of the mutability but in theory we could account for that by subclassing the brush and throwing if you try to change the color.

Just thinking if we can get best of both worlds

@jonathanpeppers
Copy link
Member Author

@PureWeen we could subclass and make the Color property setter throw.

But you could still do:

Brush.Blue.SetValue(SolidColorBrush.ColorProperty, Color.Red);

Is there a way to "extend" a BindableProperty and make it readonly?

WPF calls DependencyObject.Freeze() on the cached value they have:

https://github.com/dotnet/wpf/blob/5e8187344b2b561ef08b9ca2735cd89cbdd3c11e/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Knowncolors.cs#L212-L220

I don't there there is a concept of "freeze" in MAUI?

@PureWeen
Copy link
Member

@jonathanpeppers we have readonly keys you can use

static readonly BindablePropertyKey HeadlessOffsetPropertyKey =
but not sure if we can make that work in this scenario.

My initial "semi-ok" approach here would be to do it via an OnPropertyChanged handler on the bindable property. Have that call into the concrete implementation and then you could override that to throw on the ImmutableBrush

Comment on lines 79 to 80
public static SolidColorBrush Ivory => new(Colors.Ivory);
public static SolidColorBrush Khaki => new(Colors.Ivory);
Copy link
Member Author

Choose a reason for hiding this comment

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

If I see typos like this, it's probably not on purpose?

Copy link
Member Author

Choose a reason for hiding this comment

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

I found 3 like this after adding a test to check for it.

Copy link
Member

Choose a reason for hiding this comment

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

yea, definitely not on purpose but it's been like that since XF :-/

https://github.com/xamarin/Xamarin.Forms/blob/ba709a180f1ac2218a1c9fa8b0566bd92e96e215/Xamarin.Forms.Core/Brush.cs#L78

So probably been wrong for a bit now

@jonathanpeppers jonathanpeppers changed the title [controls] Brush.Foo should return new instances [controls] Brush.Foo should return immutable instances Dec 21, 2021
When profiling a `dotnet new maui` app, with this package:

https://github.com/jonathanpeppers/Mono.Profiler.Android

The `alloc` report shows:

    Allocation summary
    Bytes      Count  Average Type name
    39984        147 2 72     Microsoft.Maui.Controls.SolidColorBrush

Stack trace:

    38352 bytes from:
    (wrapper runtime-invoke) object:runtime_invoke_void (object,intptr,intptr,intptr)
    Microsoft.Maui.Controls.VisualElement:.cctor ()
    (wrapper runtime-invoke) object:runtime_invoke_void (object,intptr,intptr,intptr)
    Microsoft.Maui.Controls.Brush:.cctor ()

Reviewing the `Brush` class, there are indeed 147 `SolidColorBrush`
created on startup that are stored in fields.

But what is weird about this, is that `SolidColorBrush` is mutable!

    public Color Color
    {
        get => (Color)GetValue(ColorProperty);
        set => SetValue(ColorProperty, value);
    }

So I could literally write code like:

    Brush.Blue.Color = Colors.Red;

Blue is red! (insert evil laughter?)

I think the appropriate fix here is that all of these `static
readonly` fields should just be properties that return a new
`ImmutableBrush`. We can cache the values in fields on demand. Then
someone can't do something evil like change `Blue` to `Red`?

I reviewed WPF source code to check what they do, and they took a
similar approach:

https://github.com/dotnet/wpf/blob/5e8187344b2b561ef08b9ca2735cd89cbdd3c11e/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/brushes.cs#L33-L1586

We should make this API change now before MAUI is stable, and we have
the side benefit to save 39984 bytes of memory on startup?

I added tests for these scenarios, and discovered 3 typos for `Brush`
colors that listed the wrong color.
@PureWeen PureWeen merged commit ac6befc into dotnet:main Dec 22, 2021
@jonathanpeppers jonathanpeppers deleted the brush-colors branch January 3, 2022 14:30
@github-actions github-actions bot locked and limited conversation to collaborators Dec 21, 2023
@Eilon Eilon added the t/perf The issue affects performance (runtime speed, memory usage, startup time, etc.) label May 10, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
legacy-area-perf Startup / Runtime performance t/perf The issue affects performance (runtime speed, memory usage, startup time, etc.)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants