Skip to content

Use System.Linq in fewer places #31199

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

Merged
5 commits merged into from
Apr 24, 2021
Merged

Use System.Linq in fewer places #31199

5 commits merged into from
Apr 24, 2021

Conversation

pranavkm
Copy link
Contributor

  • Replace with alternatives or iteration where possible
  • Rewrite Router component to avoid allocations

@pranavkm pranavkm marked this pull request as ready for review March 24, 2021 20:09
@pranavkm pranavkm requested a review from a team as a code owner March 24, 2021 20:09
@pranavkm
Copy link
Contributor Author

FYI @eerhardt

@javiercn
Copy link
Member

I'm in general OK with this, however this is somewhat of a loosing battle if we don't "ban" these APIs with an analyzer. I'm also a bit wary about how much of a win this is for real apps when it comes to trimming, since all libraries/customers need to also avoid these APIs for it to make a difference.

@eerhardt
Copy link
Member

FYI @stephentoub - since you've done a lot of this work in dotnet/runtime.

@Pilchie Pilchie added the area-blazor Includes: Blazor, Razor Components label Mar 24, 2021
@@ -16,13 +15,13 @@ internal class MemberAssignment
[DynamicallyAccessedMembers(Component)] Type type,
BindingFlags bindingFlags)
{
var dictionary = new Dictionary<string, List<PropertyInfo>>();
var dictionary = new Dictionary<string, List<PropertyInfo>>(StringComparer.Ordinal);
Copy link
Member

Choose a reason for hiding this comment

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

Why pass in the comparer explicitly? It's not a problem, I'm just wondering what it's fixing. Just for clarity?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A lot of dictionaries in ASP.NET Core tend to use or require an OrdinalIgnoreCase behavior. I prefer being explicit about it when we don't.

}
}

private void RefreshRouteTableLegacy()
{
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to compile against downlevel? If not, Prepend will be cheaper than new[] + Concat here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can update this, but I'm hopeful we could remove or trim this code: #29913

@@ -29,8 +28,7 @@ internal class ComponentParametersTypeCache
[RequiresUnreferencedCode("This type attempts to load component parameters that may be trimmed.")]
private static Type? ResolveType(Key key, Assembly[] assemblies)
{
var assembly = assemblies
.FirstOrDefault(a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal));
var assembly = Array.Find(assemblies, a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal));
Copy link
Member

@stephentoub stephentoub Mar 25, 2021

Choose a reason for hiding this comment

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

This is still closing over key and allocating a delegate to boot. May as well avoid that, too :)

foreach (Assembly a in assemblies)
{
    if (a.GetName().Name == key.Assembly)
    {
        return a.GetType(key.Type, throwOnError: false, ignoreCase: false);
    }
}

return null;

@@ -29,8 +27,7 @@ internal class RootComponentTypeCache

private static Type? ResolveType(Key key, Assembly[] assemblies)
{
var assembly = assemblies
.FirstOrDefault(a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal));
var assembly = Array.Find(assemblies, a => string.Equals(a.GetName().Name, key.Assembly, StringComparison.Ordinal));
Copy link
Member

Choose a reason for hiding this comment

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

Ditto

@runfoapp runfoapp bot mentioned this pull request Mar 25, 2021
@@ -16,13 +15,13 @@ internal class MemberAssignment
[DynamicallyAccessedMembers(Component)] Type type,
BindingFlags bindingFlags)
{
var dictionary = new Dictionary<string, List<PropertyInfo>>();
var dictionary = new Dictionary<string, List<PropertyInfo>>(StringComparer.Ordinal);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

A lot of dictionaries in ASP.NET Core tend to use or require an OrdinalIgnoreCase behavior. I prefer being explicit about it when we don't.

}
}

private void RefreshRouteTableLegacy()
{
var assemblies = AdditionalAssemblies == null ? new[] { AppAssembly } : new[] { AppAssembly }.Concat(AdditionalAssemblies);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can update this, but I'm hopeful we could remove or trim this code: #29913

foreach (var componentType in componentTypes)
{
// We're deliberately using inherit = false here.
//
// RouteAttribute is defined as non-inherited, because inheriting a route attribute always causes an
// ambiguity. You end up with two components (base class and derived class) with the same route.
var routeAttributes = componentType.GetCustomAttributes<RouteAttribute>(inherit: false);
var templates = new List<string>(routeAttributes.Length);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Slightly annoying because I have to use the non-generic version of GetCustomAttributes to have it return an array. The generic one returns an IEnumerable

var moveNext = _spanEnumerator.MoveNext();
Debug.Assert(moveNext, "We expect this to at least have one item.");
return moveNext;
}
Copy link
Member

Choose a reason for hiding this comment

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

This is a lot of extra machinery to reason about and maintain, especially with the caveats like "Do NOT make this readonly". People reading it may feel it could be done much more simply (e.g., the previous implementation) so it would be good to insert some kind of comment clarifying why we're doing all this.

How big is the payoff from doing all this? Bearing in mind that our plan is to move to a sourcegenerator-based way of setting component parameters, are we still going to be getting a meaningful benefit from doing this afterwards?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I definitely went overboard with this. It's a lot of code for what would amount to be a growingly smaller case (i.e. component libraries that were compiled without source generators). I'll turn this back into a foreach loop. It has one extra allocation, but I'd wager the readability is preferrable.

.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(key => this.GetSection(key))
.ToList();
var hashSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Copy link
Member

@HaoK HaoK Mar 30, 2021

Choose a reason for hiding this comment

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

Completely unrelated question for my own insight, why does WASM need to be able to build/modify configuration providers like this (i.e. why do we need a WASMHostConfiguration that deviates from the configure/build/immutable configuration pattern)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The host builder in WASM is a single shot operation similar to the feather http builder. You configure services on it and tell it to run. We found that you frequently need access to configuration as you're building it - hence it has this read / write behavior. I thought there was an issue filed by @davidfowl to add support for this by default in config, but I can't seem to find it.

Copy link
Member

Choose a reason for hiding this comment

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

@halter73 is gonna file it soon.

@@ -67,7 +67,10 @@ private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)

Validator.TryValidateProperty(propertyValue, validationContext, results);
_messages.Clear(fieldIdentifier);
_messages.Add(fieldIdentifier, results.Select(result => result.ErrorMessage!));
foreach (var result in CollectionsMarshal.AsSpan(results))
Copy link
Member

Choose a reason for hiding this comment

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

Is this faster than foreach (var result in results) because it skips something like checking-whether-the-list-was-modified on each read?

If so, then as a meta-point about this feature itself rather than your use of it here, it seems like a weird way of expressing the intent because there's nothing obvious in the naming of CollectionsMarshal.AsSpan that implies this, or provides any indication of what would go wrong if the assumption turned out to be false. Perhaps this is something we only expect advanced .NET devs to do and don't feel it needs to be made any more obvious.

Copy link
Member

Choose a reason for hiding this comment

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

It's this "unsafe" way of getting you access to the internal array. The *Marshal is a pattern .NET uses to express that these APIs are advanced and could do bad things if misused

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yup - it basically gives you access to the static array that the list is using as it's implementation: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Runtime/InteropServices/CollectionsMarshal.cs#L19. It's only safe to use if the list isn't modified as part of the iteration which is true in this particular case.

@SteveSandersonMS SteveSandersonMS self-requested a review April 23, 2021 09:44
Copy link
Member

@SteveSandersonMS SteveSandersonMS left a comment

Choose a reason for hiding this comment

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

Excellent stuff!

@pranavkm pranavkm added this to the 6.0-preview5 milestone Apr 23, 2021
@ghost
Copy link

ghost commented Apr 23, 2021

Hello @pranavkm!

Because this pull request has the auto-merge label, I will be glad to assist with helping to merge this pull request once all check-in policies pass.

p.s. you can customize the way I help with merging this pull request, such as holding this pull request until a specific person approves. Simply @mention me (@msftbot) and give me an instruction to get started! Learn more here.

@pranavkm pranavkm closed this Apr 23, 2021
@pranavkm pranavkm reopened this Apr 23, 2021
@@ -158,12 +158,27 @@ internal async Task RunAsyncCore(CancellationToken cancellationToken, WebAssembl
var loggerFactory = Services.GetRequiredService<ILoggerFactory>();
_renderer = new WebAssemblyRenderer(Services, loggerFactory);

var rootComponents = _rootComponents;
foreach (var rootComponent in rootComponents)
var initializationTcs = new TaskCompletionSource();
Copy link
Member

Choose a reason for hiding this comment

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

I don't see a TaskCreationOptions.RunContinuationsAsynchronously

@ghost ghost merged commit 0c464fa into main Apr 24, 2021
@ghost ghost deleted the prkrishn/no-linq branch April 24, 2021 21:19
This pull request was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants