Skip to content

Support STJ Polymorphism #45405

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
merged 31 commits into from
Jan 10, 2023
Merged

Conversation

brunolins16
Copy link
Member

@brunolins16 brunolins16 commented Dec 1, 2022

Overview

Contributes to #44852

This PR change RDF and JsonOutputFormatter to, when a JsonPolymorphismOptions is detected, uses the declared type's JsonTypeInfo to call the serializer.

⚠️ I will create a follow up PR to add STJ Polymorphism support for Http Results.
⚠️ AOT/Linker-friendly support is not cover in this PR

Benchmark results

MapXXX

Summary:

  • Similar or slight better RPS
  • ~ 6% less Total Allocated bytes (when using JsonTypeInfo)
  • JsonDerived faster than runtime type usage.
Fast path

Sample

app.MapGet("/json", () => new { Text = "teste" });

Results

application main PR
CPU Usage (%) 83 83 0.00%
Cores usage (%) 998 997 -0.10%
Working Set (MB) 193 195 +1.04%
Private Memory (MB) 577 595 +3.12%
Start Time (ms) 89 88 -1.12%
Max CPU Usage (%) 83 83 +0.35%
Max Working Set (MB) 202 204 +0.56%
Max GC Heap Size (MB) 96 94 -1.72%
Size of committed memory by the GC (MB) 127 128 +1.18%
Max Number of Gen 0 GCs / sec 2.00 2.00 0.00%
Max Number of Gen 1 GCs / sec 1.00 1.00 0.00%
Max Number of Gen 2 GCs / sec 1.00 1.00 0.00%
Max Time in GC (%) 1.00 0.00
Max Gen 0 Size (B) 584 584 0.00%
Max Gen 1 Size (B) 3,661,672 3,715,352 +1.47%
Max Gen 2 Size (B) 3,667,440 3,729,760 +1.70%
Max LOH Size (B) 321,472 321,472 0.00%
Max POH Size (B) 1,186,016 1,363,176 +14.94%
Total Allocated Bytes 4,411,891,376 4,116,792,736 -6.69%
Max GC Heap Fragmentation 4 4 -4.43%
# of Assemblies Loaded 86 86 0.00%
Max Exceptions (#/s) 298 352 +18.12%
Max Lock Contention (#/s) 26 27 +3.85%
Max ThreadPool Threads Count 23 25 +8.70%
Max ThreadPool Queue Length 152 233 +53.29%
Max ThreadPool Items (#/s) 767,649 772,454 +0.63%
Max Active Timers 1 1 0.00%
IL Jitted (B) 222,856 223,357 +0.22%
Methods Jitted 2,344 2,346 +0.09%
load main PR
CPU Usage (%) 91 91 0.00%
Cores usage (%) 1,097 1,096 -0.09%
Working Set (MB) 119 130 +9.24%
Private Memory (MB) 358 358 0.00%
Start Time (ms) 0 0
First Request (ms) 141 137 -2.84%
Requests/sec 583,933 583,921 -0.00%
Requests 8,814,217 8,815,645 +0.02%
Mean latency (ms) 0.42 0.42 -0.19%
Max latency (ms) 14.94 15.20 +1.74%
Bad responses 0 0
Socket errors 0 0
Read throughput (MB/s) 114.72 114.72 0.00%
Latency 50th (ms) 0.37 0.37 0.00%
Latency 75th (ms) 0.48 0.48 0.00%
Latency 90th (ms) 0.65 0.65 +0.15%
Latency 99th (ms) 1.46 1.46 0.00%
Runtime type check + TypeInfo serializer

Sample

app.MapGet("/json", () => new Message { Text = "teste" });

class Message : BaseMessage { }

class BaseMessage
{
    public string Text { get; set; }
}

Results

application main PR
CPU Usage (%) 83 84 +1.20%
Cores usage (%) 998 1,008 +1.00%
Working Set (MB) 193 195 +1.04%
Private Memory (MB) 585 579 -1.03%
Start Time (ms) 88 88 0.00%
Max CPU Usage (%) 83 84 +0.65%
Max Working Set (MB) 205 204 -0.56%
Max GC Heap Size (MB) 95 94 -0.25%
Size of committed memory by the GC (MB) 130 131 +1.33%
Max Number of Gen 0 GCs / sec 2.00 2.00 0.00%
Max Number of Gen 1 GCs / sec 1.00 1.00 0.00%
Max Number of Gen 2 GCs / sec 1.00 1.00 0.00%
Max Time in GC (%) 0.00 0.00
Max Gen 0 Size (B) 26,712 584 -97.81%
Max Gen 1 Size (B) 3,450,208 3,651,432 +5.83%
Max Gen 2 Size (B) 3,744,864 3,623,080 -3.25%
Max LOH Size (B) 321,472 321,472 0.00%
Max POH Size (B) 1,186,016 1,186,016 0.00%
Total Allocated Bytes 4,398,211,880 4,126,395,960 -6.18%
Max GC Heap Fragmentation 4 4 +1.91%
# of Assemblies Loaded 86 86 0.00%
Max Exceptions (#/s) 348 372 +6.90%
Max Lock Contention (#/s) 22 31 +40.91%
Max ThreadPool Threads Count 24 23 -4.17%
Max ThreadPool Queue Length 181 174 -3.87%
Max ThreadPool Items (#/s) 770,007 784,509 +1.88%
Max Active Timers 1 1 0.00%
IL Jitted (B) 222,571 223,284 +0.32%
Methods Jitted 2,348 2,349 +0.04%
load main PR
CPU Usage (%) 91 92 +1.10%
Cores usage (%) 1,097 1,102 +0.46%
Working Set (MB) 120 119 -0.83%
Private Memory (MB) 358 358 0.00%
Start Time (ms) 0 0
First Request (ms) 138 144 +4.35%
Requests/sec 583,081 584,599 +0.26%
Requests 8,804,383 8,826,007 +0.25%
Mean latency (ms) 0.42 0.42 +0.66%
Max latency (ms) 13.72 14.22 +3.64%
Bad responses 0 0
Socket errors 0 0
Read throughput (MB/s) 114.55 114.85 +0.26%
Latency 50th (ms) 0.37 0.37 -0.81%
Latency 75th (ms) 0.48 0.48 +0.21%
Latency 90th (ms) 0.64 0.66 +2.34%
Latency 99th (ms) 1.41 1.42 +0.71%
Polymorphic

Sample

app.MapGet("/json", BaseMessage () => new Message { Text = "teste" });

class Message : BaseMessage { }

[JsonDerivedType(typeof(Message))]
class BaseMessage
{
    public string Text { get; set; }
}

Results

application main PR
CPU Usage (%) 84 84 0.00%
Cores usage (%) 1,003 1,003 0.00%
Working Set (MB) 193 196 +1.55%
Private Memory (MB) 576 588 +2.08%
Start Time (ms) 89 88 -1.12%
Max CPU Usage (%) 83 84 +0.85%
Max Working Set (MB) 201 205 +2.22%
Max GC Heap Size (MB) 96 93 -2.86%
Size of committed memory by the GC (MB) 128 134 +4.79%
Max Number of Gen 0 GCs / sec 2.00 2.00 0.00%
Max Number of Gen 1 GCs / sec 1.00 1.00 0.00%
Max Number of Gen 2 GCs / sec 1.00 1.00 0.00%
Max Time in GC (%) 0.00 0.00
Max Gen 0 Size (B) 584 584 0.00%
Max Gen 1 Size (B) 3,572,664 3,662,304 +2.51%
Max Gen 2 Size (B) 3,660,960 3,667,616 +0.18%
Max LOH Size (B) 321,472 321,472 0.00%
Max POH Size (B) 1,186,016 1,186,016 0.00%
Total Allocated Bytes 4,381,664,640 4,132,810,160 -5.68%
Max GC Heap Fragmentation 4 4 +0.31%
# of Assemblies Loaded 86 86 0.00%
Max Exceptions (#/s) 275 314 +14.18%
Max Lock Contention (#/s) 24 24 0.00%
Max ThreadPool Threads Count 23 24 +4.35%
Max ThreadPool Queue Length 113 254 +124.78%
Max ThreadPool Items (#/s) 782,049 786,959 +0.63%
Max Active Timers 1 1 0.00%
IL Jitted (B) 222,943 223,189 +0.11%
Methods Jitted 2,350 2,347 -0.13%
load main PR
CPU Usage (%) 92 92 0.00%
Cores usage (%) 1,103 1,099 -0.36%
Working Set (MB) 120 119 -0.83%
Private Memory (MB) 358 358 0.00%
Start Time (ms) 0 0
First Request (ms) 137 144 +5.11%
Requests/sec 583,935 585,208 +0.22%
Requests 8,817,410 8,836,006 +0.21%
Mean latency (ms) 0.42 0.42 +1.02%
Max latency (ms) 13.96 14.25 +2.08%
Bad responses 0 0
Socket errors 0 0
Read throughput (MB/s) 114.72 114.97 +0.22%
Latency 50th (ms) 0.36 0.36 -0.27%
Latency 75th (ms) 0.49 0.49 0.00%
Latency 90th (ms) 0.66 0.67 +0.45%
Latency 99th (ms) 1.40 1.45 +3.57%

Controllers

Summary:

  • Improvements depends on untyped JsonTypeInfo new apis
  • Additional call to GetTypeInfo will happen.
Controllers

Sample

public BaseMessage Json()
{
    return new Message { Text = "Hello, World!" };
}

public class BaseMessage { public string Text { get; set; } }
public class Message : BaseMessage {}

Results

application main PR
CPU Usage (%) 95 93 -2.11%
Cores usage (%) 1,137 1,118 -1.67%
Working Set (MB) 178 173 -2.81%
Private Memory (MB) 166 163 -1.81%
Start Time (ms) 224 228 +1.79%
Max CPU Usage (%) 99 96 -3.63%
Max Working Set (MB) 188 181 -3.86%
Max GC Heap Size (MB) 101 95 -6.32%
Size of committed memory by the GC (MB) 122 115 -5.52%
Max Number of Gen 0 GCs / sec 9.00 9.00 0.00%
Max Number of Gen 1 GCs / sec 7.00 2.00 -71.43%
Max Number of Gen 2 GCs / sec 1.00 1.00 0.00%
Max Time in GC (%) 1.00 1.00 0.00%
Max Gen 0 Size (B) 3,422,168 3,605,624 +5.36%
Max Gen 1 Size (B) 6,647,576 6,570,280 -1.16%
Max Gen 2 Size (B) 4,542,904 5,088,000 +12.00%
Max LOH Size (B) 424,440 424,440 0.00%
Max POH Size (B) 1,251,320 1,251,320 0.00%
Total Allocated Bytes 19,079,922,016 19,006,488,720 -0.38%
Max GC Heap Fragmentation 46 44 -4.13%
# of Assemblies Loaded 111 111 0.00%
Max Exceptions (#/s) 403 370 -8.19%
Max Lock Contention (#/s) 19 9 -52.63%
Max ThreadPool Threads Count 23 23 0.00%
Max ThreadPool Queue Length 204 205 +0.49%
Max ThreadPool Items (#/s) 641,942 631,171 -1.68%
Max Active Timers 0 0
IL Jitted (B) 268,073 267,888 -0.07%
Methods Jitted 3,389 3,387 -0.06%
load main PR
CPU Usage (%) 50 50 0.00%
Cores usage (%) 600 597 -0.50%
Working Set (MB) 119 119 0.00%
Private Memory (MB) 358 358 0.00%
Start Time (ms) 0 0
First Request (ms) 141 141 0.00%
Requests/sec 239,383 237,996 -0.58%
Requests 3,614,549 3,593,534 -0.58%
Mean latency (ms) 1.47 1.43 -2.72%
Max latency (ms) 185.26 195.49 +5.52%
Bad responses 0 0
Socket errors 0 0
Read throughput (MB/s) 41.78 41.54 -0.57%
Latency 50th (ms) 1.02 1.03 +0.98%
Latency 75th (ms) 1.07 1.08 +0.93%
Latency 90th (ms) 1.14 1.14 0.00%
Latency 99th (ms) 14.45 6.97 -51.76%

@brunolins16
Copy link
Member Author

/benchmark

@pr-benchmarks
Copy link

pr-benchmarks bot commented Dec 1, 2022

Crank Pull Request Bot

/benchmark <benchmark[,...]> <profile[,...]> <component,[...]> <arguments>

Benchmarks:

  • plaintext: TechEmpower Plaintext Scenario - ASP.NET Platform implementation
  • json: TechEmpower JSON Scenario - ASP.NET Platform implementation
  • fortunes: TechEmpower Fortunes Scenario - ASP.NET Platform implementation
  • yarp: YARP - http-http with 10 bytes
  • mvcjsoninput2k: Sends 2Kb Json Body to an MVC controller

Profiles:

  • aspnet-perf-lin: INTEL/Linux 12 Cores
  • aspnet-perf-win: INTEL/Windows 12 Cores
  • aspnet-citrine-lin: INTEL/Linux 28 Cores
  • aspnet-citrine-win: INTEL/Windows 28 Cores
  • aspnet-citrine-arm: ARM/Linux 32 Cores
  • aspnet-citrine-amd: AMD/Linux 48 Cores

Components:

  • kestrel
  • mvc

Arguments: any additional arguments to pass through to crank, e.g. --variable name=value

@brunolins16
Copy link
Member Author

/benchmark json aspnet-perf-win mvc

@pr-benchmarks
Copy link

pr-benchmarks bot commented Dec 1, 2022

Benchmark started for json on aspnet-perf-win with mvc. Logs: link

@pr-benchmarks
Copy link

pr-benchmarks bot commented Dec 1, 2022

json - aspnet-perf-win

application json.base json.pr
CPU Usage (%) 77 77 0.00%
Cores usage (%) 928 928 0.00%
Working Set (MB) 70 71 +1.43%
Private Memory (MB) 65 66 +1.54%
Build Time (ms) 1,882 3,660 +94.47%
Start Time (ms) 148 133 -10.14%
Published Size (KB) 96,997 96,997 0.00%
.NET Core SDK Version 8.0.100-alpha.1.22601.15 8.0.100-alpha.1.22601.15
load json.base json.pr
CPU Usage (%) 100 100 0.00%
Cores usage (%) 1,198 1,199 +0.08%
Working Set (MB) 119 119 0.00%
Private Memory (MB) 363 363 0.00%
Start Time (ms) 1 0
First Request (ms) 67 68 +1.49%
Requests/sec 680,412 666,439 -2.05%
Requests 10,272,871 10,060,915 -2.06%
Mean latency (ms) 1.33 1.73 +30.08%
Max latency (ms) 36.49 81.64 +123.73%
Bad responses 0 0
Socket errors 0 0
Read throughput (MB/s) 94.74 92.79 -2.06%
Latency 50th (ms) 0.34 0.32 -5.65%
Latency 75th (ms) 1.55 1.91 +23.23%
Latency 90th (ms) 3.77 5.14 +36.34%
Latency 99th (ms) 10.01 14.46 +44.46%


namespace Microsoft.AspNetCore.Http;

public sealed class HttpJsonOptionsSetup : IConfigureOptions<JsonOptions>
Copy link
Member

Choose a reason for hiding this comment

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

This should be:

Suggested change
public sealed class HttpJsonOptionsSetup : IConfigureOptions<JsonOptions>
public sealed class HttpJsonOptionsSetup : IPostConfigureOptions<JsonOptions>

Copy link
Member Author

Choose a reason for hiding this comment

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

@davidfowl I changed to set the DefaultJsonTypeResolver directly when the JsonOptions is created. Are you ok with that?

Copy link
Member

Choose a reason for hiding this comment

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

What happens if somebody sets it to null?

Copy link
Member Author

@brunolins16 brunolins16 Dec 10, 2022

Choose a reason for hiding this comment

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

Right now, we are failing back to use the serializer with runtime type. In Native AOT probably we will need to 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.

I just changed the behavior after all discussion, and it will throw when we call JsonOptions.MakeReadOnly().


if (declaredType is not null &&
runtimeType != declaredType &&
SerializerOptions.TypeInfoResolver != null &&
Copy link
Member

@eiriktsarpalis eiriktsarpalis Dec 15, 2022

Choose a reason for hiding this comment

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

TypeInfoResolver should always be non-null at this point.

Copy link
Member Author

Choose a reason for hiding this comment

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

The serializer could not be called yet, so, null still valid, no?

Copy link
Member

Choose a reason for hiding this comment

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

null resolvers only get accepted by the serialization methods that are marked RequiredUnreferencedCode. Given that you plan on replacing these with the linker-safe overloads, your code will sooner or later need to enforce that invariant.

Copy link
Member Author

Choose a reason for hiding this comment

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

Given that you plan on replacing these with the linker-safe overloads, your code will sooner or later need to enforce that invariant.

That is true for native AOT (Trim safe)-only. When the app is not configured to trim it could fall back to the current call to the overloaded marked with RequiredUnreferencedCode.

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 think I finally got your point 😂. If the TypeInfoResolver is null at this point is an indication of:

  1. It was explicitly set to null by the users, since we are setting it to Default... in a non-trimmed(aot) apps, or;
  2. It is a trimmed/aot app and the source gen context was not configured correctly.

If I understand correctly, in both case a call to MakeReadOnly will throw because it is misconfigured, that really make sense, and that is why you are asking for make the call very early. Is that correct?

If so, I believe you are correct, and we should only make calls to linker-safe overloads both here and RDF (doesn't matter if in AOT/Trimmed app or not) and add the MakeReadOnly as early as possible.

Comment on lines 42 to 43
[RequiresUnreferencedCode("System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver might require types that cannot be statically analyzed and might need runtime code generation.")]
[RequiresDynamicCode("System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver might require types that cannot be statically analyzed and might need runtime code generation. Enable EnsureJsonTrimmability feature switch for native AOT applications.")]
Copy link
Member

Choose a reason for hiding this comment

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

Am I correct in understanding that after we have dotnet/sdk#29719, we'll update the JsonOptions static constructor to stop calling this method when the "EnsureJsonTrimmability" feature switch is set?

If we're going to mention the feature switch, we'd probably also want to include the full name, "Microsoft.AspNetCore.EnsureJsonTrimmability", and link to https://learn.microsoft.com/dotnet/core/runtime-config/#runtimeconfigjson, but I think picking one of the msbuild properties and telling people to add it their csproj would be simpler.

Setting any one of "EnsureAspNetCoreJsonTrimmability", "PublishTrimmed", "IsTrimmable" or "PublishAot" properties to true should be sufficient, right? Will warning fire for unreferenced or dynamic code when these properties are not set?

I don't think mentioning System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver is relevant since the user is not directly calling it and probably has no idea why it's getting called. If you remove these attributes, do warnings still propagate from the DefaultJsonTypeInfoResolver? If so, we might as well just leave the original warning, since it's not actionable just yet.

Copy link
Member Author

Choose a reason for hiding this comment

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

Am I correct in understanding that after we have dotnet/sdk#29719, we'll update the JsonOptions static constructor to stop calling this method when the "EnsureJsonTrimmability" feature switch is set?

Exactly and the private method will be trimmed. Something like:

 TypeInfoResolver = TrimmingAppContextSwitches.EnsureJsonTrimmability ? null : CreateDefaultTypeResolver()

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 we're going to mention the feature switch, we'd probably also want to include the full name, "Microsoft.AspNetCore.EnsureJsonTrimmability", and link to https://learn.microsoft.com/dotnet/core/runtime-config/#runtimeconfigjson, but I think picking one of the msbuild properties and telling people to add it their csproj would be simpler.

Setting any one of "EnsureAspNetCoreJsonTrimmability", "PublishTrimmed", "IsTrimmable" or "PublishAot" properties to true should be sufficient, right?

Right, the idea is when EnsureAspNetCoreJsonTrimmability is set to true, that would be possible by setting directly or using the PublishTrimmed, PublishAot properties, the RuntimeHostConfigurationOption (Microsoft.AspNetCore.EnsureJsonTrimmability) will be set and the linker will be able to do the substitutions.

https://github.com/dotnet/aspnetcore/pull/45886/files

Will warning fire for unreferenced or dynamic code when these properties are not set?

Yes.

Copy link
Member Author

Choose a reason for hiding this comment

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

Also, once you have EnsureAspNetCoreJsonTrimmability set to true the TypeInfoResolver will default null when run (F5) or publish, so, developers can understand the behavior during the app development.

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 don't think mentioning System.Text.Json.Serialization.Metadata.DefaultJsonTypeInfoResolver is relevant since the user is not directly calling it and probably has no idea why it's getting called. If you remove these attributes, do warnings still propagate from the DefaultJsonTypeInfoResolver? If so, we might as well just leave the original warning, since it's not actionable just yet.

You are right and it will propagate. I will leave the original warning for now. Thanks.

@davidfowl
Copy link
Member

Are we calling a new API that isn't public? The source generator needs to copy this pattern as well.

cc @captainsafia

@brunolins16
Copy link
Member Author

Are we calling a new API that isn't public? The source generator needs to copy this pattern as well.

cc @captainsafia

I just merged #45593 that makes the APIs public.

@brunolins16
Copy link
Member Author

brunolins16 commented Jan 9, 2023

@eiriktsarpalis I believe I covered almost all the feedback. Do you have any additional concern before I merge it?

@brunolins16 brunolins16 merged commit 0020e0b into dotnet:main Jan 10, 2023
@brunolins16 brunolins16 deleted the brunolins16/issues/44852 branch January 10, 2023 05:51
@ghost ghost added this to the 8.0-preview1 milestone Jan 10, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Dec 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
old-area-web-frameworks-do-not-use *DEPRECATED* This label is deprecated in favor of the area-mvc and area-minimal labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants