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

CopyUsed with new TrimMode #3039

Open
adirh3 opened this issue Sep 16, 2022 · 14 comments
Open

CopyUsed with new TrimMode #3039

adirh3 opened this issue Sep 16, 2022 · 14 comments

Comments

@adirh3
Copy link

adirh3 commented Sep 16, 2022

Previously, TrimMode allowed using link or copyused modes for assemblies that opted in for trimming.
Since .NET 7 Preview 7 (I believe #2856), TrimMode now has partial or full to enable/disable trimming of all assemblies, but now there is no way (unless I missed it) to copy assemblies as is (previously copyused) while not trimming libraries that did not opt in for trimming (currently partial).

Is there any way to achieve copyused and partial mode at the same time?

Thanks!

@sbomer
Copy link
Member

sbomer commented Sep 17, 2022

In .NET 6, this was accomplished by setting TrimmerDefaultAction=copy and TrimMode=copyused, but in .NET 7 TrimmerDefaultAction will be ignored if you try to set it (the build should produce a warning).

If you need a workaround, you can try setting TrimMode=copyused and _TrimmerDefaultAction=copy, but note that this is not supported. The only officially supported settings in .NET 7 are TrimMode=full or TrimMode=partial.

@adirh3
Copy link
Author

adirh3 commented Sep 17, 2022

If you need a workaround, you can try setting TrimMode=copyused and _TrimmerDefaultAction=copy, but note that this is not supported. The only officially supported settings in .NET 7 are TrimMode=full or TrimMode=partial.

Thanks, it worked! Will it be officially supported in .NET 7 (or later)? or is the copyused feature deprecated? (If so, I think it's worth mentioning in the breaking changes)

@vitek-karas
Copy link
Member

Thanks @adirh3 - I'm adding it to the list.

We don't plan to add back support for "copyused" going forward. It's a flaky/hacky approach - if it's needed it means there are almost certainly warnings produced and using "copuysed" is a way to sort of randomly include enough in the app to make it work. For some apps it may work, but it's basically just a guess. We would rather invest in mechanisms which are more predictable in behavior.

@sbomer
Copy link
Member

sbomer commented Sep 19, 2022

I agree that copyused is worth mentioning. We weren't explicit enough about it in the code change that deprecated TrimmerDefaultAction.

It is strange that today if you set TrimMode=copyused and target net7.0, the assemblies without IsTrimmable metadata will still get full trimming. I think that TrimMode=copyused when targeting .net7 should produce a warning too, and maybe it should even set the default action to copy.

@stevefan1999-personal
Copy link

stevefan1999-personal commented Sep 28, 2023

Thanks @adirh3 - I'm adding it to the list.

We don't plan to add back support for "copyused" going forward. It's a flaky/hacky approach - if it's needed it means there are almost certainly warnings produced and using "copuysed" is a way to sort of randomly include enough in the app to make it work. For some apps it may work, but it's basically just a guess. We would rather invest in mechanisms which are more predictable in behavior.

I don't think it is a flaky/hacky approach and it is sometimes a must -- you really want to just account for all the assemblies in so that there is no need to worry about a big list of TrimmerRootAssembly, for example:

	<ItemGroup>
		<TrimmerRootAssembly Include="Cocona" />
		<TrimmerRootAssembly Include="Serilog" />
		<TrimmerRootAssembly Include="Serilog.Sinks.Async" />
		<TrimmerRootAssembly Include="Serilog.Sinks.Spectre" />
		<TrimmerRootAssembly Include="Serilog.Expressions" />
		<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore" />
		<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Relational" />
		<TrimmerRootAssembly Include="Microsoft.EntityFrameworkCore.Sqlite" />
	</ItemGroup>

This is because I used this for Serilog in appsettings.json:

{
  "Serilog": {
    "Using": [ "Serilog.Sinks.Async", "Serilog.Sinks.Spectre" ],
    "MinimumLevel": "Debug",
    "WriteTo": [
      {
        "Name": "Async",
        "Args": {
          "configure": [
            {
              "Name": "Spectre"
            }
          ]
        }
      }
    ],
    "Enrich": [ "FromLogContext" ]
  }
}

And this would never be scanned by the trimmer for potential reference. We need to be more conservative here regarding dynamic code. 

This is just from a toy project of mine who tried to naively include Serilog and I already have to figure a lot of things out to make Native AOT somewhat usable. Consider if I have a project with more than 100 dependencies this quickly becomes a nightmareish burden.

I don't care about the compile time or binary size, I just want it to run.

@vitek-karas
Copy link
Member

I don't care about the compile time or binary size, I just want it to run.

What benefits are you looking for using NativeAOT for this project?

Our general approach in NativeAOT is to heavily rely on the static analysis, meaning that apps are guaranteed to work only if they produce 0 warnings. That obviously comes with lot of limitations and lot of things don't work. On the other hand, it's the only solution we could come up with which is predictable, all of the other solutions can break at any time and diagnosing why they broke was very difficult.

@stevefan1999-personal
Copy link

stevefan1999-personal commented Oct 3, 2023

I don't care about the compile time or binary size, I just want it to run.

What benefits are you looking for using NativeAOT for this project?

Our general approach in NativeAOT is to heavily rely on the static analysis, meaning that apps are guaranteed to work only if they produce 0 warnings. That obviously comes with lot of limitations and lot of things don't work. On the other hand, it's the only solution we could come up with which is predictable, all of the other solutions can break at any time and diagnosing why they broke was very difficult.

Maybe give us a suggestion list of assemblies for adding into the TrimmerRootAssembly? It is quite hard to figure out the right combination to add in. In golang you can have a wildcard import* so that we can force add the needed code into the assembler. With dotnet not so much

*:

import _ "time/tzdata"

@vitek-karas
Copy link
Member

You could root all assemblies if you wanted, but then the binary size and compile time will be really bad. I guess I don't understand the goal here. If you need all this dynamism, why not use normal CoreCLR to run the app? Why do you want NativeAOT?

@stevefan1999-personal
Copy link

stevefan1999-personal commented Oct 3, 2023

You could root all assemblies if you wanted, but then the binary size and compile time will be really bad. I guess I don't understand the goal here. If you need all this dynamism, why not use normal CoreCLR to run the app? Why do you want NativeAOT?

Indeed...I used self-contained + single file + compression before to try and achieve a golang like experience...but the performance is terrible. This is for a private CLI application I wrote to convert game music into MP3 and the boot up time is like 3 seconds (alas, still better than NodeJS CLI apps which I used to take 10 seconds to launch but that's not very fair to compare with a scripted language)

If I put this solution onto a full-fledged game engine, I bet the boot up time would be even worse. Having NativeAOT also helps with some platform limitation, for example I can export functions as dynamic libraries symbols.

@stevefan1999-personal
Copy link

You could root all assemblies if you wanted

Also what is the best way to do that without manual cherrypicking?

@vitek-karas
Copy link
Member

Sorry - forgot to answer your other question:

Maybe give us a suggestion list of assemblies for adding into the TrimmerRootAssembly?

I don't know how. What if you have code like:

void Test(object value)
{
    value.GetType().GetProperty("Prop");
}

Without more info, the only solution to make this work always would be to keep the Prop property on all types in the entire application. This would quickly grow into very large sizes of application.

And that's not even the worst. Some plugin systems (I'm not sure but I think Serilog does something like this):

foreach (var asm in GetAllAssemblies())
{
    foreach (var t in asm.GetTypes())
    {
        if (t.IsAssignableTo(typeof(IInteresting)))
            DoSomething(t);
    }
}

The only way to make this work is to preserve everything everywhere, not a solution for trying to make the app small.

@vitek-karas
Copy link
Member

Also what is the best way to do that without manual cherrypicking?

@sbomer - I vaguely remember that there's a point in SDK where we know all the assemblies as input to publish... do you know what it is?

@sbomer
Copy link
Member

sbomer commented Oct 3, 2023

_ComputeAssembliesToPostprocessOnPublish is the place that ILLink hooks into:
https://github.com/dotnet/sdk/blob/49ece0429dce4b77917fdeb9d72b7f237d1e8d2c/src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Publish.targets#L880C98-L881.

Note that it's a "private" (by convention since it starts with _) target, so the SDK is allowed to change it (I wouldn't recommend relying on it).

@MichalStrehovsky
Copy link
Member

Indeed...I used self-contained + single file + compression before to try and achieve a golang like experience...but the performance is terrible.

Did you set <PublishReadyToRun>true</PublishReadyToRun>? Without that, the startup time is terrible, but PublishReadyToRun brings you half way to PublishAot.

Maybe give us a suggestion list of assemblies for adding into the TrimmerRootAssembly? It is quite hard to figure out the right combination to add in

I think you'll be best off by just setting <TrimMode>partial</TrimMode> instead of trying to come up with a list of assemblies. This is the legacy "we'll root a bunch of random things and cross fingers that it works" mode.

When publishing your app you should see feedback in the form of warnings when the library you're using is not compatible with trimming/AOT. Ignoring the warnings and rooting random assemblies is not recommended because unless you know exactly what the code in the library is doing, you're running the risk of either the app being broken off the bat, or being broken in some subtle/non obvious ways (e.g. only in exceptional paths).

We have these warnings because it's fundamentally not possible to just take existing .NET code (that has access to many scripting-like facilities) and run trimming/AOT on it. Golang lacks those facilities (there's no Assembly.Load or Type.GetType equivalent). .NET is a lot more expressive than Golang, but it comes with a cost that not all of .NET is compatible with trimming/AOT. If you have an app that is not compatible with trimming, it's generally better not to trim it - it is the preferred way to run .NET code unless you have a need for small size/AOT.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants