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

Apply a variant of the visitor pattern for reflection-based generic type instantiation. #72373

Closed
wants to merge 2 commits into from

Conversation

eiriktsarpalis
Copy link
Member

The System.Text.Json default metadata resolver has been using reflection to instantiate some of its generic metadata types such as JsonTypeInfo<T> and JsonPropertyInfo<T>. This of course is necessary since the value of T cannot be statically determined in most cases.

This PR updates some of the existing reflection-based generic type instantiation logic so that type parameter reification concerns are delegated to a "type witness", and any domain-specific metadata instantiation is performed via a generic visitor that is accepted by the witness. This approach has a few benefits:

  1. Makes the metadata layer more amenable to refactoring: constructors are not invoked via reflection and any parameter mismatches are caught at compile time.
  2. Avoids TargetInvocationException being surfaced to the user.
  3. Records nice performance gains compared to System.Activator or direct MethodInfo invocation on the micro level.

cc @stephentoub @GrabYourPitchforks @jkotas for feedback.

@ghost
Copy link

ghost commented Jul 18, 2022

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

Issue Details

The System.Text.Json default metadata resolver has been using reflection to instantiate some of its generic metadata types such as JsonTypeInfo<T> and JsonPropertyInfo<T>. This of course is necessary since the value of T cannot be statically determined in most cases.

This PR updates some of the existing reflection-based generic type instantiation logic so that type parameter reification concerns are delegated to a "type witness", and any domain-specific metadata instantiation is performed via a generic visitor that is accepted by the witness. This approach has a few benefits:

  1. Makes the metadata layer more amenable to refactoring: constructors are not invoked via reflection and any parameter mismatches are caught at compile time.
  2. Avoids TargetInvocationException being surfaced to the user.
  3. Records nice performance gains compared to System.Activator or direct MethodInfo invocation on the micro level.

cc @stephentoub @GrabYourPitchforks @jkotas for feedback.

Author: eiriktsarpalis
Assignees: eiriktsarpalis
Labels:

area-System.Text.Json

Milestone: -

{
return (JsonTypeInfo)methodInfo.MakeGenericMethod(type).Invoke(null, new[] { options })!;
}
catch (TargetInvocationException ex)
Copy link
Member

Choose a reason for hiding this comment

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

FYI there is BindingFlags.DoNotWrapExceptions that lets the raw exception throw.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's not available in netstandard2.0, hence this extra code. That being said, it is evident from this PR that we haven't been applying the flag consistently.

@@ -576,9 +590,7 @@ public static JsonTypeInfo CreateJsonTypeInfo(Type type, JsonSerializerOptions o
ThrowHelper.ThrowArgumentException_CannotSerializeInvalidType(nameof(type), type, null, null);
}

s_createJsonTypeInfo ??= typeof(JsonTypeInfo).GetMethod(nameof(CreateJsonTypeInfo), new Type[] { typeof(JsonSerializerOptions) })!;
return (JsonTypeInfo)s_createJsonTypeInfo.MakeGenericMethod(type)
Copy link
Member

Choose a reason for hiding this comment

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

Just a note that this PR is changing the refactoring done in #67526.

In effect, replacing MethodInfo.MakeGenericMethod with Activor.CreateInstance()?

Copy link
Member Author

Choose a reason for hiding this comment

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

It's using Type.MakeGenericType + Activator instead of MethodInfo.MakeGenericMethod, but everything is behind logic marked RUC so it shouldn't matter much.

@jkotas
Copy link
Member

jkotas commented Jul 18, 2022

Records nice performance gains

I expect that this change has performance losses in static footprint. The runtime has to load number of extra generic types for each T.

@jkotas
Copy link
Member

jkotas commented Jul 18, 2022

Records nice performance gains

I expect that this change has performance losses in static footprint. The runtime has to load number of extra generic types for each T.

Given that these operations are typically executed just once, the static footprint is going to dominate the performance impact. My guess is that this change is (startup) performance regression for typical json serialization uses.

@eiriktsarpalis
Copy link
Member Author

eiriktsarpalis commented Jul 18, 2022

Given that these operations are typically executed just once, the static footprint is going to dominate the performance impact. My guess is that this change is (startup) performance regression for typical json serialization uses.

How could I measure potential regressions in this space? We have a few cold-start serialization benchmarks in dotnet/performance but effectively they follow the same approach as the benchmark linked in this PR (creating metadata from scratch in the same process).

@jkotas
Copy link
Member

jkotas commented Jul 18, 2022

https://gist.github.com/jkotas/aaaab624fbd034265cbecd6b6d8508ae

Current win-x64 release main: ~700ms
This PR: ~2400ms (~3.4x regression)

@eiriktsarpalis
Copy link
Member Author

Nice and simple. I think I'll keep that for future checks. Thanks.

@ghost ghost locked as resolved and limited conversation to collaborators Aug 18, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants