Migrate NuGet JSON readers to System.Text.Json for NativeAOT - #7601
Migrate NuGet JSON readers to System.Text.Json for NativeAOT#7601baronfel wants to merge 5 commits into
Conversation
Forward the runtime FeatureSwitchDefinitionAttribute while retaining the compatibility polyfill for older target frameworks. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0782b2a6-d495-4dd2-ade4-1039d29ae149
Preserve one-based line and UTF-8 byte-column positions across stream buffer boundaries and use them in package-spec parse diagnostics. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0782b2a6-d495-4dd2-ade4-1039d29ae149
Add source-generated metadata and explicit converters, preserve the public Newtonsoft surface and fallback, and select System.Text.Json through the standard feature switch or environment opt-in. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0782b2a6-d495-4dd2-ade4-1039d29ae149
Parse SDK resolver data incrementally with the shared buffered reader while preserving caching, diagnostics, Newtonsoft fallback, and the existing lazy-load behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0782b2a6-d495-4dd2-ade4-1039d29ae149
cf29f6b to
36956ea
Compare
Add opt-in V1, V2, and V3 packages.lock.json parsing with stream-based reading, preserve malformed-file diagnostics and stream ownership, and retain Newtonsoft as the default reader and writer. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 0782b2a6-d495-4dd2-ade4-1039d29ae149
36956ea to
1ef2127
Compare
baronfel
left a comment
There was a problem hiding this comment.
Some notes for the reader about why certain decisions were made
| #if NET9_0_OR_GREATER | ||
|
|
||
| using System.Diagnostics.CodeAnalysis; | ||
| using System.Runtime.CompilerServices; | ||
|
|
||
| // This is a supporting forwarder for an internal polyfill API. | ||
| [assembly: TypeForwardedTo(typeof(FeatureSwitchDefinitionAttribute))] | ||
|
|
||
| #else |
There was a problem hiding this comment.
This is porting of a pattern that @DustinCampbell corrected on MSBuild's polyfills.
|
|
||
| internal JsonTokenType TokenType => _reader.TokenType; | ||
|
|
||
| internal int LineNumber |
There was a problem hiding this comment.
Adding this tracking to the STJ parser means that error messages that used Newtonsoft's Line Info interfaces can keep line info on STJ parsing paths - we use this here.
| if (useSystemTextJson) | ||
| { | ||
| jsonStream = File.OpenRead(globalJsonPath); | ||
| } | ||
| else | ||
| { | ||
| json = File.ReadAllText(globalJsonPath); | ||
| } |
There was a problem hiding this comment.
This parser, like the other parsers used in this repo, chooses which implementation (NJ or STJ) to use. Here I'm also trying to be a little more performant by streaming the data for STJ instead of reading the whole global.json into a string and checking for the msbuild-sdks node via contains. We can reduce this diff a bit by loading the string in memory like we do for NJ, but that feels memory-inefficient to me.
| if (NuGetFeatureFlags.UseSystemTextJsonDeserializationFeatureSwitch) | ||
| { | ||
| return ReadRuntimeGraphWithSystemTextJson(stream); | ||
| } | ||
|
|
||
| if (NuGetFeatureFlags.IsSystemTextJsonDeserializationEnabledByEnvironment(environmentVariableReader)) | ||
| { | ||
| return ReadRuntimeGraph(streamReader); | ||
| return ReadRuntimeGraphWithSystemTextJson(stream); | ||
| } |
There was a problem hiding this comment.
STJ doesn't work great with a TextReader-based usage pattern. So for this parser, if we detect that STJ should be used, we pivot to using Stream directly so that we aren't forced to buffer the TextReader. This leads to a tiny bit of code duplication (if you use the TextReader-based call paths we do buffer when using STJ, which is not the best), but I think this is relatively understandable.
| { | ||
| RuntimeGraphJsonModel json = STJJsonSerializer.Deserialize( | ||
| stream, | ||
| JsonRuntimeFormatContext.Default.RuntimeGraphJsonModel) |
There was a problem hiding this comment.
We're using source-generated STJ serializers here instead of hand-rolling the parsing. Raise a flag if this is not desired, but I think we have all of the extensibility we need even with the auto-generated parsers.
| internal sealed class RuntimeGraphJsonModel | ||
| { | ||
| [JsonPropertyName("runtimes")] | ||
| [JsonConverter(typeof(RuntimeDescriptionCollectionJsonConverter))] |
There was a problem hiding this comment.
Having these custom converters lets us do the mapping of these complex types in a streaming fashion, without having to parse into intermediate Dictionary<string, object> mappings and then re-parse into the final structures. Should save some memory/intermediate allocations.
| PackagesLockFile lockFile; | ||
| if (NuGetFeatureFlags.UseSystemTextJsonDeserializationFeatureSwitch | ||
| || NuGetFeatureFlags.IsSystemTextJsonDeserializationEnabledByEnvironment()) | ||
| { | ||
| lockFile = ReadLockFileWithSystemTextJson(stream); | ||
| } | ||
| else | ||
| { | ||
| using var textReader = new StreamReader(stream); | ||
| lockFile = ReadLockFile(textReader); | ||
| } |
There was a problem hiding this comment.
Similar to the runtime graph format, STJ works better with streams instead of TextReaders. So if we detect that STJ is used, we do a slightly-parallel code path that works on Streams to lean into what STJ expects. Slight duplication with the "parse, the set Path, and return good errors" pattern, but worth it I think for the memory usage implications.
|
This PR has been automatically marked as stale because it has no activity for 7 days. It will be closed if no further activity occurs within another 30 days of this comment. If it is closed, you may reopen it anytime when you're ready again, as long as you don't delete the branch. |
Bug
Fixes: Part of dotnet/sdk#55497
Description
Migrate NuGet's read-side JSON paths used by the .NET SDK and .NET CLI from Newtonsoft.Json to System.Text.Json so NativeAOT and trimming can remove the Newtonsoft deserialization closure.
What changed
global.json, using the sharedUtf8JsonStreamReader.packages.lock.jsonformats V1, V2, and V3 behind NuGet's existing opt-in selection mechanism. Newtonsoft remains the default compatibility fallback, and writing remains Newtonsoft-based and intentionally out of scope.NuGet.UseSystemTextJsonDeserializationAppContext switch, theNUGET_USE_SYSTEM_TEXT_JSON_DESERIALIZATIONenvironment opt-in, then the Newtonsoft reader as the compatibility fallback.This does not remove all Newtonsoft references from NuGet. ProjectModel writers and legacy plugin DTO properties typed as
JObjectremain unchanged. The goal of this PR is to eliminate reachable Newtonsoft read/deserialization paths used by the NativeAOT CLI.NativeAOT .NET CLI impact
A controlled win-x64 Release experiment integrated this branch (
ace289d42) into the NativeAOT .NET CLI at SDK commit7e04840bcf. The baseline used NuGet7.10.0-rc.36417; the variant used packages built from this branch as7.10.0-rc.60002. These sizes are for debug builds so not the final assets, which are smaller.dotnet-aot.dllThe remaining Newtonsoft nodes come from out-of-scope write paths, plugin DTO type metadata, and metadata/vtable/generic roots. No reachable Newtonsoft read path remains through runtime graphs, SDK resolver
global.json,project.json,project.assets.json,packages.lock.json, NuGet.Protocol, or plugin payload deserialization.The smaller graph also reduced NativeAOT build times in general:
IlcCompileValidation
PR Checklist