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

JsonNode trimmability improvements #52934

Merged
merged 3 commits into from
May 20, 2021
Merged

Conversation

steveharter
Copy link
Member

@steveharter steveharter commented May 18, 2021

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-type JsonValue to serialize.

Verified no regression STJ.dll size in a Blazor app with trimming.

@steveharter steveharter added this to the 6.0.0 milestone May 18, 2021
@steveharter steveharter requested review from eerhardt and layomia May 18, 2021 20:16
@steveharter steveharter self-assigned this May 18, 2021
@dotnet-issue-labeler
Copy link

Note regarding the new-api-needs-documentation label:

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.

@ghost
Copy link

ghost commented May 18, 2021

Tagging subscribers to this area: @eiriktsarpalis, @layomia
See info in area-owners.md if you want to be subscribed.

Issue Details

Fixes #52773

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-type JsonValue to serialize.

Verified no regression STJ.dll size in a Blazor app with trimming.

Author: steveharter
Assignees: steveharter
Labels:

area-System.Text.Json

Milestone: 6.0.0

@eerhardt
Copy link
Member

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)
Copy link
Member

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.

Copy link
Member Author

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()

Copy link
Member

@eerhardt eerhardt May 19, 2021

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);
Copy link
Contributor

@layomia layomia May 18, 2021

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.

Copy link
Member Author

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.

Copy link
Member

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.

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 I believe ToString() is fine based on previous conversation with @eerhardt

Copy link
Member Author

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.

@steveharter steveharter changed the title JsonNode linker-safe improvements JsonNode trimmability improvements May 19, 2021
Copy link
Contributor

@layomia layomia left a 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);
Copy link
Contributor

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?

Copy link
Member Author

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.

@steveharter steveharter merged commit 595c122 into dotnet:main May 20, 2021
@steveharter steveharter deleted the NodeApiTrimming branch May 20, 2021 16:39
@@ -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.")]
Copy link
Member

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.")]
Copy link
Member

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.

Copy link
Member Author

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>
Copy link
Member

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);
Copy link
Member

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>
Copy link
Member

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.

eerhardt added a commit to eerhardt/runtime that referenced this pull request Jun 1, 2021
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
eerhardt added a commit that referenced this pull request Jun 2, 2021
Follow up to #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 #45623
@ghost ghost locked as resolved and limited conversation to collaborators Jun 19, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
3 participants