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

System.CommandLine defaults have large dependency closure #1794

Closed
jkotas opened this issue Jul 15, 2022 · 9 comments · Fixed by #2127
Closed

System.CommandLine defaults have large dependency closure #1794

jkotas opened this issue Jul 15, 2022 · 9 comments · Fixed by #2127
Assignees
Labels
Area-Performance bug Something isn't working
Milestone

Comments

@jkotas
Copy link
Member

jkotas commented Jul 15, 2022

Repro

dotnet publish -c Release -r win-x64 /p:PublishTrimmed=true

using System.CommandLine;

internal class Program
{
    private static int Main(string[] args)
    {
        var fileOption = new Option<FileInfo?>(
            name: "--file",
            description: "The file to read and display on the console.");

        var rootCommand = new RootCommand("Sample app for System.CommandLine");
        rootCommand.AddOption(fileOption);

        rootCommand.SetHandler(
            (FileInfo file) => ReadFile(file),
            fileOption);
        return rootCommand.Invoke(args);
    }

    private static void ReadFile(FileInfo file)
        => File.ReadLines(file.FullName).ToList()
            .ForEach(line => Console.WriteLine(line));
}

Actual result:

The trimmed application has referenced to many types and binaries that should not be needed:

System.Private.Uri.dll
System.Net.Primitives.dll
System.ObjectModel.dll
System.ComponentModel.dll
System.ComponentModel.Primitives.dll
System.ComponentModel.TypeConverter.dll
System.Collections.NonGeneric.dll
System.Runtime.Serialization.Formatters.dll
System.Diagnostics.Process.dll
...

Expected result:

The trimmed application should only reference types and binaries that are actually needed.

@jkotas jkotas changed the title System.CommandLine System.CommandLine defaults have large dependency closure Jul 15, 2022
@jkotas
Copy link
Member Author

jkotas commented Jul 15, 2022

Context: dotnet/runtime#72082 (comment)

@jonsequitur jonsequitur added bug Something isn't working Area-Performance labels Jul 16, 2022
@jonsequitur jonsequitur self-assigned this Jul 16, 2022
@adamsitnik
Copy link
Member

We have made some progress recently, but we are definitely not done yet:

System.Private.Uri.dll
System.Net.Primitives.dll
- System.ObjectModel.dll
- System.ComponentModel.dll
- System.ComponentModel.Primitives.dll
- System.ComponentModel.TypeConverter.dll
- System.Collections.NonGeneric.dll
System.Runtime.Serialization.Formatters.dll
- System.Diagnostics.Process.dll

@adamsitnik
Copy link
Member

My guess is that all of the redundant references come from here:

private static readonly Dictionary<Type, TryConvertString> _stringConverters = new()

which is just a dictionary of parsers that in theory can be used by the app:

[typeof(IPAddress)] = (string token, out object? value) =>
{
if (IPAddress.TryParse(token, out var ip))
{
value = ip;
return true;
}
value = default;
return false;
},

Since Argument and Option are generic now, we could introduce a parse method similar to:

internal bool TryParse(string token, out T value)
{
    if (typeof(T) == typeof(IPAddress))
    {
        if (IPAddress.TryParse(token, out var ip))
        {
            value = (T)(object)ip;
            return true;
        }

        value = default;
        return false;
    }
    // else if (typeof(T)...
}

@jkotas Does trimmer recognize such typeof checks? If not, how can I get rid of these references?

@am11
Copy link
Member

am11 commented Mar 28, 2023

@adamsitnik, very nice! Should/can we update https://github.com/dotnet/runtime/blob/f561a058b13523c4b045803c9876e823b4cfe0fc/eng/Versions.props#L174 to latest revision? It's four months old and seems like we don't have a darc subscription in runtime repo for command-line-api (dotnet/format, dotnet/sdk etc. have such a subscription).

@jkotas
Copy link
Member Author

jkotas commented Mar 28, 2023

Does trimmer recognize such typeof checks?

Trimmer does not recognize patterns like these.

If not, how can I get rid of these references?

This is yet another deserializer. You can look at e.g. how we have solved this problem for System.Text.Json deserializer. The general shape of the solution is:

  • Avoid collections of built-in deserializers or discovering deserializers using reflection.
  • Expose each built-in deserializer as a public entrypoint with concrete types.
  • Use source generators to generate the glue code if the entrypoints with concrete types are cumbersome to use.

@adamsitnik
Copy link
Member

Should/can we update

We should, but I should not do it myself, as I introduced all the breaking changes and they are reasonable and easy to fix for me.

I am about to write a doc that lists all the breaking changes, explains the reasoning behind them and how to solve the issues.

@am11 would you be interested in updating dotnet/runtime once I have the doc ready? And share some feedback about whether the changes are move in the right direction or not?

@adamsitnik
Copy link
Member

With #2127 the only dependency is System.Linq

And updated perf numbers for startup scenario with default settings (all features enables, https://github.com/adamsitnik/commandline-perf/tree/update):

Method Args Mean Error StdDev Median Ratio RatioSD
Empty 65.59 ms 0.297 ms 0.875 ms 65.44 ms 0.79 0.01
EmptyAOT 12.95 ms 0.132 ms 0.388 ms 12.88 ms 0.16 0.01
Corefxlab 82.84 ms 0.347 ms 1.022 ms 82.77 ms 1.00 0.02
SystemCommandLine2021 113.17 ms 0.704 ms 2.075 ms 112.73 ms 1.36 0.03
SystemCommandLine2022 94.72 ms 0.420 ms 1.240 ms 94.37 ms 1.14 0.02
SystemCommandLineNow 82.94 ms 0.346 ms 1.019 ms 82.81 ms 1.00 0.00
SystemCommandLineNowR2R 77.37 ms 0.357 ms 1.054 ms 77.22 ms 0.93 0.02
SystemCommandLineNowAOT 14.55 ms 0.118 ms 0.348 ms 14.54 ms 0.18 0.00
Empty --bool -s test 65.06 ms 0.409 ms 1.205 ms 64.84 ms 0.74 0.02
EmptyAOT --bool -s test 13.01 ms 0.139 ms 0.411 ms 12.94 ms 0.15 0.00
Corefxlab --bool -s test 84.97 ms 0.791 ms 2.333 ms 84.66 ms 0.97 0.03
SystemCommandLine2021 --bool -s test 117.71 ms 0.624 ms 1.841 ms 117.39 ms 1.34 0.02
SystemCommandLine2022 --bool -s test 99.46 ms 0.375 ms 1.107 ms 99.15 ms 1.14 0.02
SystemCommandLineNow --bool -s test 87.58 ms 0.332 ms 0.979 ms 87.46 ms 1.00 0.00
SystemCommandLineNowR2R --bool -s test 79.16 ms 0.341 ms 1.006 ms 79.17 ms 0.90 0.02
SystemCommandLineNowAOT --bool -s test 14.88 ms 0.170 ms 0.503 ms 14.85 ms 0.17 0.01

@adamsitnik adamsitnik self-assigned this Mar 29, 2023
@adamsitnik adamsitnik added this to the 2.0 GA milestone Mar 29, 2023
@KalleOlaviNiemitalo
Copy link

Expose each built-in deserializer as a public entrypoint with concrete types.

This doesn't seem to have been done, so anyone using Argument<Uri> will have to set a custom parser function and custom localization of error messages.

@adamsitnik
Copy link
Member

This doesn't seem to have been done, so anyone using Argument will have to set a custom parser function and custom localization of error messages.

That is true. But in the long term (.NET 9) we are going to provide a source-generator for System.CommandLine and all parsable types will be supported out of the box.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Performance bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants