-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
JsonNode trimmability improvements #52934
Conversation
Note regarding the This serves as a reminder for when your PR is modifying a ref *.cs file and adding/modifying public APIs, to please make sure the API implementation in the src *.cs file is documented with triple slash comments, so the PR reviewers can sign off that change. |
Tagging subscribers to this area: @eiriktsarpalis, @layomia Issue DetailsFixes #52773 Also some minor perf refactoring around using custom converters due to having the new per-type Verified no regression STJ.dll size in a Blazor app with trimming.
|
In order to validate this change, you should remove https://github.com/dotnet/runtime/blob/main/src/libraries/System.Text.Json/src/ILLink/ILLink.Suppressions.xml and ensure there are no ILLink warnings. |
/// <param name="options">Options to control the behavior.</param> | ||
/// <returns>The new instance of the <see cref="JsonValue"/> class that contains the specified value.</returns> | ||
public static JsonValue? Create<T>(T? value, JsonNodeOptions? options = null) | ||
public static JsonValue? Create<[DynamicallyAccessedMembers(JsonHelpers.MembersAccessedOnRead)]T>(T? value, JsonNodeOptions? options = null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought we were going to mark this as RequiresUnreferencedCode
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Only or in addition to DynamicallyAccessedMembers? I assume the same applies to the Add()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, it needs to be in addition to DynamicallyAccessedMembers
. But the DynamicallyAccessedMembers
will be removed with #52268.
I assume the same applies to the Add()
agreed
} | ||
|
||
return new JsonValue<T>(value, options); | ||
return new JsonValue<T>(value, jsonTypeInfo, options); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we create a new internal type, say JsonValueMetadataBased<T> : JsonValue
that has a different implementation of the WriteTo
method that doesn't root calls to options
-based JsonSerializer
methods? On this line, we would create and return an instance of the new type instead.
This is similar to the approach taken for a size-opts friendly JsonContent
type: #51544. See JsonContent<T>
implementation. This was done so that the new metadata-based System.Net.Http.Json methods do not root all converters/reflection-based code, like done in the existing methods.
I think this approach could help with #52943. From the analysis there:
- JsonNodeConverter creates new JsonValue
- JsonValue.ToString calls JsonSerializer.Serialize without source gen information
In this model, JsonNodeConverter
would instead create a new JsonValueMetadataBased<T>
whose WriteTo
/ToString
implementation does not root existing options-based overloads. We'd have to pass a hand-written JsonTypeInfo<T>
instance to the ctor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just have JsonValueMetadataBased<T> : JsonValue<T>
instead? This assumes the the override of WriteTo()\ToString()
doesn't call base.WriteTo()
or base.ToString()
and the linker detects that.
I'll need to prototype this and verify against the object-based repro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This assumes the the override of WriteTo()\ToString() doesn't call base.WriteTo() or base.ToString() and the linker detects that.
Unfortunately, this doesn't work. The linker will preserve the whole hierarchy of virtual/overrides of classes that are instantiated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also I believe ToString()
is fine based on previous conversation with @eerhardt
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately, this doesn't work. The linker will preserve the whole hierarchy of virtual/overrides of classes that are instantiated.
The second commit added two new classes (instead of one) so the safe and unsafe overrides are separated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, with one question
@@ -109,7 +110,7 @@ public void Add<T>(T? value) | |||
JsonNode? jNode = value as JsonNode; | |||
if (jNode == null) | |||
{ | |||
jNode = new JsonValue<T>(value); | |||
jNode = new JsonValueNotTrimmable<T>(value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So adding a T
value to a JSON array is not trim-safe. Should we add a safe version?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a safe version that takes JsonNode.
This overload was requested by the Azure SDK team. However, it is optional and could be removed since it does lead to unsafe-for-trimming paths.
@@ -518,7 +518,8 @@ public sealed partial class JsonArray : System.Text.Json.Nodes.JsonNode, System. | |||
public int Count { get { throw null; } } | |||
bool System.Collections.Generic.ICollection<System.Text.Json.Nodes.JsonNode?>.IsReadOnly { get { throw null; } } | |||
public void Add(System.Text.Json.Nodes.JsonNode? item) { } | |||
public void Add<T>(T? value) { } | |||
[System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This message is a little bit mis-leading since it says "Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or...". There is no overload that takes those types.
public JsonValueNotTrimmable(TValue value, JsonNodeOptions? options = null) : base(value, options) { } | ||
|
||
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026:RequiresUnreferencedCode", | ||
Justification = "The methods used to create this JsonValue are marked RequiresUnreferencedCode.")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't true. The ctor of this class needs to be marked as RequiresUnreferencedCode
.
I believe that will catch a bug in the JsonObject.Dynamic.cs
file where we are creating a JsonValueNotTrimmable
, but aren't marking that as RequiresUnreferencedCode
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I'll address this and other feedback in a new PR.
/// <summary> | ||
/// Initializes a new instance of the <see cref="JsonValue"/> class that contains the specified value. | ||
/// </summary> | ||
/// <param name="value">The value to add.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(nit) "The value to add." What is the value being added to?
@@ -38,13 +40,46 @@ public abstract partial class JsonValue : JsonNode | |||
return null; | |||
} | |||
|
|||
if (element.ValueKind == JsonValueKind.Object || element.ValueKind == JsonValueKind.Array) | |||
VerifyJsonElementIsNotArrayOrObject(ref element); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be creating a JsonValueTrimmable
instance in the case where it is a JsonElement instead?
/// </returns> | ||
/// <typeparam name="T">The type of value to create.</typeparam> | ||
/// <param name="value">The value to create.</param> | ||
/// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo"/> that is later used to serialize the value.</param> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be better to say
The <see cref="JsonTypeInfo"/> that will be used to serialize the value.
Follow up to dotnet#52934. - Using JsonNode in dynamic statements is not trim compatible. Add a LibraryBuild warning since there isn't a direct API to put the warning on. - Mark JsonValueNotTrimmable's ctor as unsafe - Fix up a few warning messages - minor doc fixup Contributes to dotnet#45623
Fixes #52773
Fixes #52943
Also some minor perf refactoring around using custom converters due to having the new per-type
Create()
methods that allow a converter to be specified at creation time. This improves perf 10%-20% on serialize when there are many instances of known-typeJsonValue
to serialize.Verified no regression STJ.dll size in a Blazor app with trimming.