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

Remove PackageDependencyGroupConverter #5872

Merged
merged 17 commits into from
Sep 12, 2024

Conversation

martinrrm
Copy link
Contributor

@martinrrm martinrrm commented Jun 26, 2024

Bug

Fixes: NuGet/Home#13445

Regression? Last working version:

Description

The goal of this PR is to avoid using JsonUtilities.LoadJson, for that we are removing the converter and let Newtonsoft.Json handle the deserialization of PackageDependencyGroup

This converter is used in the PM UI when we are deserializing the registration API, specifically when we select a package and we retrieve all the packages versions. https://learn.microsoft.com/en-us/nuget/api/registration-base-url-resource#package-dependency

PR Checklist

  • PR has a meaningful title

  • PR has a linked issue.

  • Described changes

  • Tests

    • Automated tests added
    • OR
    • Test exception - Perf
    • OR
    • N/A
  • Documentation

    • Documentation PR or issue filled
    • OR
    • N/A

@martinrrm martinrrm requested a review from a team as a code owner June 26, 2024 03:55
@martinrrm
Copy link
Contributor Author

This is my first time doing a perf issue, what should do to confirm this is an improvement?

Also, I tried doing something similar like Parse the assets file with System.Text.Json by jgonz120 · Pull Request #5627 · NuGet/NuGet.Client (github.com), but unfortunately if we want to stop using Newtonsoft.json it will require more work in PackageSearchMetadata
.

@martinrrm martinrrm requested a review from jgonz120 June 27, 2024 00:02
@kartheekp-ms kartheekp-ms dismissed their stale review June 27, 2024 03:02

approved by mistake.

@kartheekp-ms
Copy link
Contributor

This is my first time doing a perf issue, what should do to confirm this is an improvement?

@martinrrm A PerfView trace before and after the fix would help to understand the performance improvement resulting from the changes proposed in this PR.

@martinrrm
Copy link
Contributor Author

Before

image

After

image

Steps:

  1. Load Nuget.Client solution
  2. Open PM UI for Solution
  3. Select first installed package

I'm not 100% sure if I'm reading this right, but looks like there is an improvement 😄

@martinrrm martinrrm requested a review from kartheekp-ms June 27, 2024 20:21
@zivkan
Copy link
Member

zivkan commented Jul 1, 2024

@martinrrm did you take that from PerfView's CPU Stacks view, or GC Heap Alloc view?

In any case, I think a BenchmarkDotNet app would be a simpler comparison, as you can microbenchmark just the converter, and the memory diagnoser will tell you exactly how much memory is allocated.

@dotnet-policy-service dotnet-policy-service bot added the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Jul 8, 2024
@dotnet-policy-service dotnet-policy-service bot removed the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Jul 10, 2024
@dotnet-policy-service dotnet-policy-service bot added the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Jul 19, 2024
@martinrrm martinrrm removed the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Jul 19, 2024
@dotnet-policy-service dotnet-policy-service bot added the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Jul 26, 2024
@martinrrm
Copy link
Contributor Author

martinrrm commented Aug 12, 2024

Hi! I'm reopening the issue, I was finally able to work on it and run some benchmarks. Sorry for delay 😞

Benchmarks

Method Mean Error StdDev Gen0 Gen1 Allocated
ConverterWithoutLoadJson 19.16 us 0.134 us 0.125 us 0.8545 - 21.18 KB
ConverterUsingLoadJson 28.62 us 0.129 us 0.121 us 1.2512 0.0610 31.11 KB

Looking at the numbers by removing the usage of LoadJson and avoid using NJ.LINQ we can improve allocations by 31.91%

Branch with the benchmarks: dev-martinrrm-benchmarks

@martinrrm martinrrm reopened this Aug 12, 2024
@dotnet-policy-service dotnet-policy-service bot removed the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Aug 12, 2024
@martinrrm martinrrm requested review from zivkan and jeffkl August 12, 2024 18:51
@martinrrm martinrrm requested a review from kartheekp-ms August 21, 2024 17:40
@dotnet-policy-service dotnet-policy-service bot added the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Aug 30, 2024
@martinrrm martinrrm removed the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Aug 30, 2024
Copy link
Member

@zivkan zivkan left a comment

Choose a reason for hiding this comment

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

Can you please

  1. Update the PR title and description to something more accurate for what this PR has become (especially since it's isolated to one specific type. it doesn't fix all usage of LoadJson across the entirety of NuGet.Client repo)
    • Be careful when merging the PR too. The default commit message is set by the PR's title at the time the PR was opened, not when the merge button is clicked.
  2. Provide fresh benchmark results for deserializing some registration blob

thanks, this PR makes me happy! less custom code.

Comment on lines +22 to +23
[JsonConstructor]
private PackageDependencyGroup(NuGetFramework targetFramework)
Copy link
Member

Choose a reason for hiding this comment

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

I don't understand how the JSON deserialization sets the packages list, if this is the json constructor. But there are tests that pass? 😕

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's weird, since there is already a constructor that takes targetFramework and dependencies and null checks them I didn't want to change the behavior of it.

Previous converter would initialize an empty list for dependencies when it was null (no dependencies property in the json) thats why I added a constructor that takes only the TargetFramework that helps me create the empty list, this constructor is called only when deserialization happens. If we add a [JsonConstructor] it first attempts to use it and then populate the rest of the properties that have [JsonProperty]. JsonProperty doesn't needs a constructor to work.

Copy link
Member

Choose a reason for hiding this comment

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

The properties are get only, and they're not even auto-props. If Newtonsoft.Json is using reflection to set the value directly on the backing store, I don't understand how it can determine what the backing field name is. 🤷

{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.ToString());
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: this is... complicated 😆

The TFM value most people are used to reading, is not the value returned by .ToString(), but the value returned by .GetShortFoldername(). And for some reason, .nuspec files use a 3rd format, which is what we'll probably see from package sources when deserializing dependency groups.

But since this is a generic NuGetFramework converter, it's hard to say what's "correct" to use. However, I don't know of any scenario where NuGet will serialize this, so this method will probably never be used.

I wonder if we can use this overload of JsonConverterAttribute to pass data to the converter about which format type is preferred for the specific file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like your comment but since this is targeted for 17.2 P3 I believe I don't have enough time for this at this time, do you want me to investigate this further for next release?

Copy link
Member

Choose a reason for hiding this comment

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

Honestly, I think my preference would be to throw on write, like the old converter did. If anything needs to convert a NuGetFramework to a string in the future, then we can either investigate that "parameterized" converter, or create one converter per string format.

But I can also live with using ToString.

Copy link
Contributor

@kartheekp-ms kartheekp-ms Sep 11, 2024

Choose a reason for hiding this comment

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

Should we throw a not implemented exception here if there are going to be no callers for this method? We can address Andy's feedback in a follow up PR.

The existing code throw not implemented exception for the WriteJson method.

https://github.com/NuGet/NuGet.Client/pull/5872/files/f14e6f769612dbaf2e04eb9a702e7b3dcbe8b52b#diff-4e6ff8097dc206e72c42f58c234a65f2875f468c5bd9593c630a4cade52caf0aL49

@martinrrm
Copy link
Contributor Author

martinrrm commented Sep 10, 2024

New benchmarks, I moved some things, so object initialization is not considered in the benchmarks. With these new changes looks like performance is better 🥳

Method Mean Error StdDev Gen0 Gen1 Allocated
ConverterWithThatUsesLoadJson 15.660 us 0.0328 us 0.0274 us 0.7019 0.0305 17.55 KB
RemovedPackageDependencyGroupConverter 8.631 us 0.0611 us 0.0542 us 0.3204 - 8 KB

@martinrrm martinrrm requested a review from zivkan September 10, 2024 20:57
@martinrrm martinrrm changed the title Avoid using LoadJson and iterate by token Remove PackageDependencyGroupConverter Sep 10, 2024
@kartheekp-ms
Copy link
Contributor

Please consider updating the PR description to reflect the current state of the proposed code changes.

// Act
using var stringReader = new StringReader(PackageRegistrationDependencyGroupsJson_NoTargetFramework);
using var jsonReader = new JsonTextReader(stringReader);
jsonReader.Read();
Copy link
Contributor

Choose a reason for hiding this comment

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

Please help me understand why we have to invoke the Read method in the tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is because the first token is null and doing the Read() moves the iterator to the json object, I'm not 100% sure why this happens but looks like we are doing it in other places.

zivkan
zivkan previously approved these changes Sep 11, 2024
Copy link
Member

@zivkan zivkan left a comment

Choose a reason for hiding this comment

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

just small nitpicks, or nice to haves. No important comments.

Comment on lines +22 to +23
[JsonConstructor]
private PackageDependencyGroup(NuGetFramework targetFramework)
Copy link
Member

Choose a reason for hiding this comment

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

The properties are get only, and they're not even auto-props. If Newtonsoft.Json is using reflection to set the value directly on the backing store, I don't understand how it can determine what the backing field name is. 🤷

{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value.ToString());
Copy link
Member

Choose a reason for hiding this comment

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

Honestly, I think my preference would be to throw on write, like the old converter did. If anything needs to convert a NuGetFramework to a string in the future, then we can either investigate that "parameterized" converter, or create one converter per string format.

But I can also live with using ToString.

jgonz120
jgonz120 previously approved these changes Sep 11, 2024
@martinrrm martinrrm merged commit fef6562 into dev Sep 12, 2024
28 checks passed
@martinrrm martinrrm deleted the dev-martinrrm-perf-packagedependencygroup-converter branch September 12, 2024 22:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants