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

[API Proposal]: Add weak event listener helper class #61517

Open
dotMorten opened this issue Nov 12, 2021 · 11 comments
Open

[API Proposal]: Add weak event listener helper class #61517

dotMorten opened this issue Nov 12, 2021 · 11 comments
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Milestone

Comments

@dotMorten
Copy link

dotMorten commented Nov 12, 2021

Background and motivation

It's quite common in MVVM scenarios, that you will bind your models and viewmodels to UI controls. These UI controls will often listen for PropertyChanged and CollectionChanged events to update the UI dynamically. However since the UI views are quite often transient but the model data is long lived, you often see very expensive UI controls getting stuck in memory due to the event handler not getting unsubscribed. This problem is of course not limited to UI components only, but where you'll often see large costs associated with not unsubscribing from the events.

A typical pattern for this is to use a helper class to subscribe to weak event handlers, and over and over again we see different implementations of this in various libraries. So much so recently .NET MAUI decided to graduate their toolkit helper to the main MAUI library. This got me thinking that since this is such a common scenario, that .NET should provide this at a lower level for all libraries to use.

Earlier I had suggested that this should be a language feature, but it was concluded this should be an API feature: dotnet/roslyn#101

Example of various implementations:

API Proposal

I think there are quite a few different approaches that can be taken, as shown with all the implementations above. I'd refer to the .NET MAUI example to start with:

public void AddEventHandler(Delegate? handler, [CallerMemberName] string eventName = null)
{
	ArgumentNullException.ThrowIfNull(eventName);
	ArgumentNullException.ThrowIfNull(handler)

	var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo");

	AddEventHandler(eventName, handler.Target, methodInfo, eventHandlers);
}

public void RemoveEventHandler(Delegate? handler, [CallerMemberName] string eventName = "")
{
	ArgumentNullException.ThrowIfNull(eventName);
	ArgumentNullException.ThrowIfNull(handler)

	var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo");

	RemoveEventHandler(eventName, handler.Target, methodInfo, eventHandlers);
}

API Usage

readonly WeakEventManager weakEventManager = new WeakEventManager();

event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
{
    add => weakEventManager.AddEventHandler(value);
    remove => weakEventManager.RemoveEventHandler(value);
}

void OnPropertyChanged([CallerMemberName] in string propertyName = "") => weakEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(INotifyPropertyChanged.PropertyChanged));

Alternative Designs

Windows Community Toolkit:
WeakEventListener.cs

var weakEvent = new WeakEventListener<INotifyCollectionChanged, object, NotifyCollectionChangedEventArgs>(valNotifyCollection)
{
    OnEventAction = (instance, source, args) => obj.SetActive(IsNullOrEmpty(instance)),
    OnDetachAction = (weakEventListener) => valNotifyCollection.CollectionChanged -= weakEventListener.OnEvent
};

Risks

No response

@dotMorten dotMorten added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Nov 12, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged New issue has not been triaged by the area owner label Nov 12, 2021
@dotnet-issue-labeler
Copy link

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

@terrajobst
Copy link
Member

@huoyaoyuan
Copy link
Member

I remember there is weak event helper in WPF assemblies.

The weak event listener should get better integration with GC. For example, it should not require explicit clean up, and should clean up automatically when GC triggers.

@jamesmontemagno
Copy link
Member

I like it and would be a great feature for library creators

@ghost
Copy link

ghost commented Nov 17, 2021

Tagging subscribers to this area: @dotnet/area-system-runtime
See info in area-owners.md if you want to be subscribed.

Issue Details

Background and motivation

It's quite common in MVVM scenarios, that you will bind your models and viewmodels to UI controls. These UI controls will often listen for PropertyChanged and CollectionChanged events to update the UI dynamically. However since the UI views are quite often transient but the model data is long lived, you often see very expensive UI controls getting stuck in memory due to the event handler not getting unsubscribed. This problem is of course not limited to UI components only, but where you'll often see large costs associated with not unsubscribing from the events.

A typical pattern for this is to use a helper class to subscribe to weak event handlers, and over and over again we see different implementations of this in various libraries. So much so recently .NET MAUI decided to graduate their toolkit helper to the main MAUI library. This got me thinking that since this is such a common scenario, that .NET should provide this at a lower level for all libraries to use.

Earlier I had suggested that this should be a language feature, but it was concluded this should be an API feature: dotnet/roslyn#101

Example of various implementations:

API Proposal

I think there are quite a few different approaches that can be taken, as shown with all the implementations above. I'd refer to the .NET MAUI example to start with:

public void AddEventHandler(Delegate? handler, [CallerMemberName] string eventName = null)
{
	ArgumentNullException.ThrowIfNull(eventName);
	ArgumentNullException.ThrowIfNull(handler)

	var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo");

	AddEventHandler(eventName, handler.Target, methodInfo, eventHandlers);
}

public void RemoveEventHandler(Delegate? handler, [CallerMemberName] string eventName = "")
{
	ArgumentNullException.ThrowIfNull(eventName);
	ArgumentNullException.ThrowIfNull(handler)

	var methodInfo = handler.GetMethodInfo() ?? throw new NullReferenceException("Could not locate MethodInfo");

	RemoveEventHandler(eventName, handler.Target, methodInfo, eventHandlers);
}

API Usage

readonly WeakEventManager weakEventManager = new WeakEventManager();

event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged
{
    add => weakEventManager.AddEventHandler(value);
    remove => weakEventManager.RemoveEventHandler(value);
}

void OnPropertyChanged([CallerMemberName] in string propertyName = "") => weakEventManager.HandleEvent(this, new PropertyChangedEventArgs(propertyName), nameof(INotifyPropertyChanged.PropertyChanged));

Alternative Designs

Windows Community Toolkit:
WeakEventListener.cs

var weakEvent = new WeakEventListener<INotifyCollectionChanged, object, NotifyCollectionChangedEventArgs>(valNotifyCollection)
{
    OnEventAction = (instance, source, args) => obj.SetActive(IsNullOrEmpty(instance)),
    OnDetachAction = (weakEventListener) => valNotifyCollection.CollectionChanged -= weakEventListener.OnEvent
};

Risks

No response

Author: dotMorten
Assignees: -
Labels:

api-suggestion, area-System.Runtime, untriaged

Milestone: -

@tannergooding tannergooding added needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration and removed untriaged New issue has not been triaged by the area owner labels Jul 15, 2022
@tannergooding tannergooding added this to the Future milestone Jul 15, 2022
@hawkerm
Copy link

hawkerm commented Jan 12, 2023

+1 Related to #18645 as well.

@michael-hawker
Copy link

michael-hawker commented Apr 6, 2023

Looked more into this a bit from our Windows Community Toolkit code vs. the MAUI one recently.

I think both APIs are required (or a solution to the problems they're trying to solve). They're not 'alternate' or competing implementations, they both try to solve the problem from different sides of the event syntax. One is used when you own the event code and want to prevent others from capturing a reference to you. The other is used when you don't own the event, and don't want your code to capture the reference (in the provided example of a ViewModel capturing a UI object for instance this is usually the case). That is to say:

  1. On one side, like the MAUI one, it's at the implementation level of the event itself and someone owning that code, such that anyone subscribing to the event don't capture the reference to the instance.

  2. On the other side, like the WCT one, it's at the reference level of someone wanting to subscribe to the event without capturing the instance. This is useful for classes from the framework where the code isn't owned and using the pattern above.

So, I think both patterns are required for different scenarios? Don't think there's a singular API that can support both scenarios easily?

It could be nice if there was just some syntactic sugar for both sides to make either of these scenarios work... like on the implementors side to prevent being captured have it be declared as a weak event:

public weak event PropertyChangedEventHandler? INotifyPropertyChanged.PropertyChanged;

Or also on the subscribing side:

object.PropertyChanged ~= HandlePropertyChanged;

(Just picked ~ here as it's not commonly used and represents a weaker relationship, but it could be anything.)

All the extra overhead of not capturing references would then just be handled by the runtime without the extra overhead (and risk for mis-implementation of the pattern, which is error prone) on the developer. This is a hard pattern to understand, nuanced, hard to detect when done wrong, and easy to mis-code.

It could be also better optimized in the case that a weak event subscriber tries to register to an already weak event this way.

But at least there'd be a way for either side of an event to not capture a reference and have it be weak without the other side having to do anything or a bunch of complex patterns and knowledge required outside of 'wanting a weak reference'.

@michael-hawker
Copy link

michael-hawker commented Apr 6, 2023

Wanted to provide the current WeakEventListener pattern from our implementation here as well, as the example in the post doesn't really convey how it works end-to-end:

var inpc = rowGroupInfo.CollectionViewGroup as INotifyPropertyChanged;
var weakPropertyChangedListener = new WeakEventListener<DataGrid, object, PropertyChangedEventArgs>(this) {
    OnEventAction = static (instance, source, eventArgs) => instance.CollectionViewGroup_PropertyChanged(source, eventArgs),
    OnDetachAction = (weakEventListener) => inpc.PropertyChanged -= weakEventListener.OnEvent // Use Local References Only
}
inpc.PropertyChanged += weakPropertyChangedListener.OnEvent;

@jonathanpeppers
Copy link
Member

I've been looking into some performance implications of MAUI's WeakEventManager.

Take for example usage, like in Application.RequestedThemeChanged.

public event EventHandler<AppThemeChangedEventArgs> RequestedThemeChanged
{
	add => _weakEventManager.AddEventHandler(value);
	remove => _weakEventManager.RemoveEventHandler(value);
}

This event fires when Dark or Light mode changes on each platform, and you can data-bind the result via the {AppThemeBinding} markup extension. I think the idea to use this on Application.RequestedThemeChanged, was to be helpful so that developers wouldn't as easily create memory leaks, as Application lives as long as the process.

However, what happens in practice is that:

  • The MAUI project template defines {AppThemeBindng} in a default set of styles. Developers are meant to use these as a startup point for tweaking colors, etc.
  • Because of this, every MAUI view subscribes to this event -- potentially multiple times.
  • The subscribers are a Dictionary<string, List<Subscriber>>, so there is a dictionary lookup followed by a O(N) search -- for any operation.

A general "weak event" pattern in .NET would be nice here.

But I would be more interested to know how it could be implemented in a performant way.

If the design is a new C# compiler feature like object.PropertyChanged ~= HandlePropertyChanged;. What does the C# compiler emit?

Or is this only possible to implement when combined with a new runtime feature?

@jbe2277
Copy link
Contributor

jbe2277 commented Apr 15, 2023

@jonathanpeppers I'm wondering if a weak subscribing side approach has a better performance than a weak publisher (implementor) side as your example from MAUI.

I have implemented the weak subscribing side approach here which works very similar to the WPF WeakEventManager:

I believe it might be faster than the MAUI implementation because it does not use reflection or open delegates.

Example usage for PropertyChanged event:

public class Publisher : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;
}

public class Subscriber
{
    public void Init(Publisher publisher)
    {
        // Instead of publisher.PropertyChanged += Handler; use the following statement:
        WeakEvent.PropertyChanged.Add(publisher, Handler)        
    }

    public void Handler(object? sender, PropertyChangedEventArgs e) { }
}

More details can be found on this Wiki page Weak Event.

@jonathanpeppers
Copy link
Member

One of the issues I see, it seems every implementation involves two WeakReference<T> and an intermediate object (that may or may not include a finalizer).

I don't see how to make "weak events" in the ~same performance/ballpark as a vanilla C# event.

jonathanpeppers added a commit to jonathanpeppers/maui that referenced this issue Apr 17, 2023
Context: dotnet#12130
Context: https://github.com/angelru/CvSlowJittering

Profiling a customer sample app, I noticed a lot of time spent in
`{AppThemeBinding}` and `WeakEventManager` while scrolling:

    2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr...
    2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()
    2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1<TEventArgs_REF>,string)

16% is a *lot* to notice while scrolling. Sometimes I've made
improvements where I only shaved off 3% of the total time.

What is going on here is:

* Default `maui` template has lots of `{AppThemeBinding}` in the
  default `Styles.xaml`. This supports Light vs Dark theming.

* `{AppThemeBinding}` subscribes to `Application.RequestedThemeChanged`

* Making every MAUI view subscribe to this event -- potentially
  multiple times.

* Subscribers are a `Dictionary<string, List<Subscriber>>`, where
  there is a dictionary lookup followed by a O(N) search for
  unsubscribe operations.

I spent a little time investigating if we can make a faster
`WeakEventManager`, in general:

dotnet/runtime#61517

I did not immediately see a way to make "weak events" fast, but I did
see a way to make this scenario fast.

Before:
* For any `{AppThemeBinding}`, it calls both:
    * `RequestedThemeChanged -= OnRequestedThemeChanged` O(N) time
    * `RequestedThemeChanged += OnRequestedThemeChanged` constant time
* Where the `-=` is notably slower, due to possibly 100s of subscribers.

After:
* Create an `_attached` boolean, so we know know the "state" if it is
  attached or not.
* New bindings only call `+=`, where `-=` will now only be called by
  `{AppThemeBinding}` in *rare* cases.
* Most .NET MAUI apps do not "unapply" bindings, but `-=` would only
  be used in that case.

After this change, the following method disappeared from `dotnet-trace`
output completely:

    2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()

Meaning that `AppThemeBinding.AttachEvents()` is now so fast that 0%
(basically no time) is spent inside this method.

I also could notice a difference in general startup time of the sample
app. An average of 10 runs on a Pixel 5:

    Before:
    Average(ms): 967.7
    Std Err(ms): 4.62132737064436
    Std Dev(ms): 14.6139203045133
    After:
    Average(ms): 958.9
    Std Err(ms): 3.22645316098034
    Std Dev(ms): 10.2029407525478

So I could notice a ~10ms improvement to startup in this app, and
scrolling seemed a bit better as well.

Note that I don't think this completely solves dotnet#12130, as things still
seem sluggish to me when scrolling. But it is a reasonable improvement
to start with that benefits all .NET MAUI apps on all platforms.
mattleibow pushed a commit to dotnet/maui that referenced this issue Apr 19, 2023
* [controls] fix performance issue in {AppThemeBinding}

Context: #12130
Context: https://github.com/angelru/CvSlowJittering

Profiling a customer sample app, I noticed a lot of time spent in
`{AppThemeBinding}` and `WeakEventManager` while scrolling:

    2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr...
    2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()
    2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1<TEventArgs_REF>,string)

16% is a *lot* to notice while scrolling. Sometimes I've made
improvements where I only shaved off 3% of the total time.

What is going on here is:

* Default `maui` template has lots of `{AppThemeBinding}` in the
  default `Styles.xaml`. This supports Light vs Dark theming.

* `{AppThemeBinding}` subscribes to `Application.RequestedThemeChanged`

* Making every MAUI view subscribe to this event -- potentially
  multiple times.

* Subscribers are a `Dictionary<string, List<Subscriber>>`, where
  there is a dictionary lookup followed by a O(N) search for
  unsubscribe operations.

I spent a little time investigating if we can make a faster
`WeakEventManager`, in general:

dotnet/runtime#61517

I did not immediately see a way to make "weak events" fast, but I did
see a way to make this scenario fast.

Before:
* For any `{AppThemeBinding}`, it calls both:
    * `RequestedThemeChanged -= OnRequestedThemeChanged` O(N) time
    * `RequestedThemeChanged += OnRequestedThemeChanged` constant time
* Where the `-=` is notably slower, due to possibly 100s of subscribers.

After:
* Create an `_attached` boolean, so we know know the "state" if it is
  attached or not.
* New bindings only call `+=`, where `-=` will now only be called by
  `{AppThemeBinding}` in *rare* cases.
* Most .NET MAUI apps do not "unapply" bindings, but `-=` would only
  be used in that case.

After this change, the following method disappeared from `dotnet-trace`
output completely:

    2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()

Meaning that `AppThemeBinding.AttachEvents()` is now so fast that 0%
(basically no time) is spent inside this method.

I also could notice a difference in general startup time of the sample
app. An average of 10 runs on a Pixel 5:

    Before:
    Average(ms): 967.7
    Std Err(ms): 4.62132737064436
    Std Dev(ms): 14.6139203045133
    After:
    Average(ms): 958.9
    Std Err(ms): 3.22645316098034
    Std Dev(ms): 10.2029407525478

So I could notice a ~10ms improvement to startup in this app, and
scrolling seemed a bit better as well.

Note that I don't think this completely solves #12130, as things still
seem sluggish to me when scrolling. But it is a reasonable improvement
to start with that benefits all .NET MAUI apps on all platforms.

* PR feedback
github-actions bot pushed a commit to dotnet/maui that referenced this issue Jun 1, 2023
Context: #12130
Context: https://github.com/angelru/CvSlowJittering

Profiling a customer sample app, I noticed a lot of time spent in
`{AppThemeBinding}` and `WeakEventManager` while scrolling:

    2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr...
    2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()
    2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1<TEventArgs_REF>,string)

16% is a *lot* to notice while scrolling. Sometimes I've made
improvements where I only shaved off 3% of the total time.

What is going on here is:

* Default `maui` template has lots of `{AppThemeBinding}` in the
  default `Styles.xaml`. This supports Light vs Dark theming.

* `{AppThemeBinding}` subscribes to `Application.RequestedThemeChanged`

* Making every MAUI view subscribe to this event -- potentially
  multiple times.

* Subscribers are a `Dictionary<string, List<Subscriber>>`, where
  there is a dictionary lookup followed by a O(N) search for
  unsubscribe operations.

I spent a little time investigating if we can make a faster
`WeakEventManager`, in general:

dotnet/runtime#61517

I did not immediately see a way to make "weak events" fast, but I did
see a way to make this scenario fast.

Before:
* For any `{AppThemeBinding}`, it calls both:
    * `RequestedThemeChanged -= OnRequestedThemeChanged` O(N) time
    * `RequestedThemeChanged += OnRequestedThemeChanged` constant time
* Where the `-=` is notably slower, due to possibly 100s of subscribers.

After:
* Create an `_attached` boolean, so we know know the "state" if it is
  attached or not.
* New bindings only call `+=`, where `-=` will now only be called by
  `{AppThemeBinding}` in *rare* cases.
* Most .NET MAUI apps do not "unapply" bindings, but `-=` would only
  be used in that case.

After this change, the following method disappeared from `dotnet-trace`
output completely:

    2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()

Meaning that `AppThemeBinding.AttachEvents()` is now so fast that 0%
(basically no time) is spent inside this method.

I also could notice a difference in general startup time of the sample
app. An average of 10 runs on a Pixel 5:

    Before:
    Average(ms): 967.7
    Std Err(ms): 4.62132737064436
    Std Dev(ms): 14.6139203045133
    After:
    Average(ms): 958.9
    Std Err(ms): 3.22645316098034
    Std Dev(ms): 10.2029407525478

So I could notice a ~10ms improvement to startup in this app, and
scrolling seemed a bit better as well.

Note that I don't think this completely solves #12130, as things still
seem sluggish to me when scrolling. But it is a reasonable improvement
to start with that benefits all .NET MAUI apps on all platforms.
rmarinho pushed a commit to dotnet/maui that referenced this issue Jun 1, 2023
* [controls] fix performance issue in {AppThemeBinding}

Context: #12130
Context: https://github.com/angelru/CvSlowJittering

Profiling a customer sample app, I noticed a lot of time spent in
`{AppThemeBinding}` and `WeakEventManager` while scrolling:

    2.08s (17%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.Apply(object,Microsoft.Maui.Controls.BindableObject,Micr...
    2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()
    2.04s (16%) microsoft.maui!Microsoft.Maui.WeakEventManager.RemoveEventHandler(System.EventHandler`1<TEventArgs_REF>,string)

16% is a *lot* to notice while scrolling. Sometimes I've made
improvements where I only shaved off 3% of the total time.

What is going on here is:

* Default `maui` template has lots of `{AppThemeBinding}` in the
  default `Styles.xaml`. This supports Light vs Dark theming.

* `{AppThemeBinding}` subscribes to `Application.RequestedThemeChanged`

* Making every MAUI view subscribe to this event -- potentially
  multiple times.

* Subscribers are a `Dictionary<string, List<Subscriber>>`, where
  there is a dictionary lookup followed by a O(N) search for
  unsubscribe operations.

I spent a little time investigating if we can make a faster
`WeakEventManager`, in general:

dotnet/runtime#61517

I did not immediately see a way to make "weak events" fast, but I did
see a way to make this scenario fast.

Before:
* For any `{AppThemeBinding}`, it calls both:
    * `RequestedThemeChanged -= OnRequestedThemeChanged` O(N) time
    * `RequestedThemeChanged += OnRequestedThemeChanged` constant time
* Where the `-=` is notably slower, due to possibly 100s of subscribers.

After:
* Create an `_attached` boolean, so we know know the "state" if it is
  attached or not.
* New bindings only call `+=`, where `-=` will now only be called by
  `{AppThemeBinding}` in *rare* cases.
* Most .NET MAUI apps do not "unapply" bindings, but `-=` would only
  be used in that case.

After this change, the following method disappeared from `dotnet-trace`
output completely:

    2.05s (16%) microsoft.maui.controls!Microsoft.Maui.Controls.AppThemeBinding.AttachEvents()

Meaning that `AppThemeBinding.AttachEvents()` is now so fast that 0%
(basically no time) is spent inside this method.

I also could notice a difference in general startup time of the sample
app. An average of 10 runs on a Pixel 5:

    Before:
    Average(ms): 967.7
    Std Err(ms): 4.62132737064436
    Std Dev(ms): 14.6139203045133
    After:
    Average(ms): 958.9
    Std Err(ms): 3.22645316098034
    Std Dev(ms): 10.2029407525478

So I could notice a ~10ms improvement to startup in this app, and
scrolling seemed a bit better as well.

Note that I don't think this completely solves #12130, as things still
seem sluggish to me when scrolling. But it is a reasonable improvement
to start with that benefits all .NET MAUI apps on all platforms.

* PR feedback

---------

Co-authored-by: Jonathan Peppers <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api-suggestion Early API idea and discussion, it is NOT ready for implementation area-System.Runtime needs-further-triage Issue has been initially triaged, but needs deeper consideration or reconsideration
Projects
None yet
Development

No branches or pull requests

10 participants