winforms icon indicating copy to clipboard operation
winforms copied to clipboard

Size of WinForms with PublishAot

Open MichalStrehovsky opened this issue 1 year ago • 47 comments

I had a look at the size of WinForms app with PublishAot. I think there's a potential to make the size of AOT-compiled self-contained WinForms apps very attractive (single-digit MB range, fully self-contained).

If I add <PublishAot>true</PublishAot> and <_SuppressWinFormsTrimError>true</_SuppressWinFormsTrimError> to a WinForms default template and dotnet publish it I get a 54 MB executable. One might say “still better than Electron” and call it good, but it can actually be much better.

Drilling into the size with Sizoscope one can immediately see something that should not be there: PresentationFramework.

image

Half of WPF gets dragged in because WinForms started using a WPF interface: ICommand. And ICommand has a bunch of custom attributes on it that drag in WPF.

We can avoid dragging in WPF if we can make sure the string within the custom attribute doesn't actually resolve. One can hack around it by putting this in the csproj for example:

  <Target Name="RemoveWPFReference" BeforeTargets="WriteIlcRspFileForCompilation">
    <ItemGroup>
      <IlcReference Remove="@(IlcReference)" Condition="'%(Filename)' == 'PresentationFramework'" />
    </ItemGroup>
  </Target>

We cannot ship this hack. The only way I can currently think of to fix this would be to divorce WPF and WinForms and stop putting them into the same NuGet/SDK.

With this out of the way, the size of the executable drops to 25.5 MB. We can do better. One thing that Sizoscope will point out is networking stack. It gets brought in from two places – PictureBox and XML. We already have feature switches in place for both PictureBox and XML. Add this to the csproj to disable these:

<PropertyGroup>
  <XmlResolverIsNetworkingEnabledByDefault>false</XmlResolverIsNetworkingEnabledByDefault>
</PropertyGroup>
  <ItemGroup>
  <!-- We really should introduce a first class property for this -->
    <RuntimeHostConfigurationOption Include="System.Windows.Forms.PictureBox.UseWebRequest" Value="false" Trim="true" />
  </ItemGroup>

With this, the size drops to 21.7 MB.

Looking at what's left in Sizoscope, designers stand out. There's many types that come out of DesignerAttribute and EditorAttribute. Can we make a feature switch to strip them? I experimentally stripped them out and the size drops to 15.2 MB. (Tracked in https://github.com/dotnet/runtime/issues/92043)

A thing that Sizoscope currently doesn't show is manifest resources. Out of the 15.2 MB above, 7.5 MB are embedded resources. They currently cannot be trimmed automatically. Clicking through the manifest resources in WinForms assemblies that are part of the application based on Sizoscope, I see that most (all?) are designer related. Can we get rid of them? If so, the size would drop to ~7 MB.

I think there would be potential to shrink this further (e.g. the TypeConverter on TableLayoutSetting+StyleConverter brings in the entire XML stack with DtdParser and everything), but 7 MB for a fully self-contained WinForms app is already quite encouraging.

MichalStrehovsky avatar Sep 14 '23 04:09 MichalStrehovsky

Also worth noting that including PresentationCore (see here), causes a visible behaviour change, since the DPI to be forced to SystemAware for all WinForms apps which don't declare it in the manifest, even if they set it in Main to something else, if they don't declare [DisableDpiAwareness] (this happens since the module initialiser runs earlier on NAOT, this sort of stuff shouldn't even be in a module initialiser imo). This is not ideal, so we should avoid including Wpf for any WinForms apps, that don't actually use Wpf.

hamarb123 avatar Sep 14 '23 09:09 hamarb123

@JeremyKuhne and @lonitra are the wizards focusing on Winforms AOT for .NET9

Is the issue with the design attributes because they live in the design assembly? So it pulls in the whole thing?

elachlan avatar Sep 14 '23 12:09 elachlan

Thanks for the details @MichalStrehovsky!

ICommand has a bunch of custom attributes on it that drag in WPF.

I had no idea the strings would resolve during compilation. Do you know precisely how this happens? Maybe we could get a feature that is more granular that would allow us to ignore specific usages wherever this is being figured out?

Note that we're currently focused on getting the key COM pieces converted to ComWrappers so that Accessibility and OLE (clipboard/dragdrop in particular) will work in this scenario. All and any suggestions and help in the AOT/Trimming space are very much appreciated.

@lonitra

JeremyKuhne avatar Sep 14 '23 17:09 JeremyKuhne

I had no idea the strings would resolve during compilation. Do you know precisely how this happens? Maybe we could get a feature that is more granular that would allow us to ignore specific usages wherever this is being figured out?

The reason why this happens is that the attributes have an annotation instructing trimming to keep things on the types:

[TypeConverter("System.Windows.Input.CommandConverter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
public interface ICommand
{
}

public sealed class TypeConverterAttribute : Attribute
{
    public TypeConverterAttribute([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] string typeName) { }
}

The annotation on the TypeConverterAttribute constructor instructs trimming to resolve the string and keep the constructor. The constructor brings a massive closure of WPF with it.

We could ask trimming to drop all TypeConverters, but I assume WinForms actually use type converter and do require this trimming behavior. It just might not be desirable here.

Cc @vitek-karas @sbomer for ideas

MichalStrehovsky avatar Sep 14 '23 20:09 MichalStrehovsky

Is the issue with the design attributes because they live in the design assembly? So it pulls in the whole thing?

We don't pull in whole assemblies (unless something forces it, like setting <TrimMode>partial</TrimMode> in the project, or something like that). The designers get pulled in for each individual control that has designer attributes on it.

The Sizoscope tool is really good at showing the paths to roots for these, try it - it's a WinForms app.

MichalStrehovsky avatar Sep 14 '23 20:09 MichalStrehovsky

For the designer related attributes: If we can make an assumption that these attributes are not useful when running the app, then there are ways to remove all of those attributes (and what they depend on along with them). Similarly for the resources - if there are embedded resources which are there solely for designers and we can say with confidence that they're not going to be needed by the app, we can also remove them. It's just that currently none of this is automatic because the trimmer tools are not smart enough to figure out if these things are actually used by the app or not (for some of these it's really difficult to do even in theory).

For the type converter: I can't think of a way to solve this super cleanly. There is a direct dependency all the way to all of the WPF functionality. Typically, the only way we can break such a chain is through feature switches - similar to what Michal used to get rid of the networking stack. But that depends on usability - if the code dependency is there, but in reality almost no WinForms app actually runs that code, then maybe a feature switch would be a solution.

I fear TypeConverter as I know that it's used a lot in WinForms - and it's not very trimming friendly.

vitek-karas avatar Sep 14 '23 20:09 vitek-karas

There are some apps that use the designers at runtime. So it would have to be behind a switch, unless the team decided that designers are not supported in AOT scenarios.

Could CommandConverter live in a different assembly that would avoid PresentationFramework? I know its a breaking change, but it seems like structuring dependencies properly helps with trimming? Please correct me if I am wrong, I am still learning AOT/Trimming.

elachlan avatar Sep 14 '23 21:09 elachlan

Could CommandConverter live in a different assembly that would avoid PresentationFramework?

It's not about what assembly CommandConverter lives in. It's about what it's constructor depends on.

Clicking through the dependency graph in Sizoscope might help building intuition around this. Here I double clicked why the Grid constructor (WPF Grid) is included in an empty WinForms app template.

It doesn't even fit in a single screen so this is just a part.

But basically CommandConverter implements ConvertFrom virtual method and since that one is called, we need ConvertFromHelper, that calls GetKnownCommand,... then suddenly we need XamlReader and that has built in support for a bunch of controls.

image

MichalStrehovsky avatar Sep 14 '23 21:09 MichalStrehovsky

ICommand doesn't live in presentationframework, and we're not using presentationframework currently from the runtime in the context of DataBinding.

image

But yes, the attributes are there, but we're actually not using them in the runtime context (and also not in the Designer context, because those would not work, anyway.)

But apart from that: We will be enabling the ElemenHost at one point, and then at the latest, we would need to have the reference to presentationframework, anyway.

KlausLoeffelmann avatar Sep 16 '23 20:09 KlausLoeffelmann

I am wondering, how the other UI stacks do that. I mean, the whole ViewModel concept is based on ICommand, and they all use that ICommand, don't they? ICommand is even .NET Standard 1.0 compatible, is it not?

image

KlausLoeffelmann avatar Sep 16 '23 20:09 KlausLoeffelmann

I am wondering, how the other UI stacks do that

The custom attribute is harmless if the project being built doesn't provide a way to resolve the PresentationFramework assembly. WinForms is inflicting it on itself by being packaged together with WPF. Granted it would be better if the ICommand interface wasn't annotated in a way that violated layering/separation of concerns but I don't know if anything can be done about that

MichalStrehovsky avatar Sep 17 '23 07:09 MichalStrehovsky

If we look at it from the other direction, WPF would require these attributes so that trimming/AOT works for them right? So we can't remove the annotations without breaking AOT for them?

elachlan avatar Sep 18 '23 23:09 elachlan

If we look at it from the other direction, WPF would require these attributes so that trimming/AOT works for them right? So we can't remove the annotations without breaking AOT for them?

The attribute is in the official docs: https://learn.microsoft.com/en-us/dotnet/api/system.windows.input.icommand?view=net-7.0. The official docs make it seem this is a WPF-specific interface with some special behavior in WinRT with no mention of any other use for this interface.

MichalStrehovsky avatar Sep 18 '23 23:09 MichalStrehovsky

The dependency on it was added in #4895 to enable modernized model binding. No work was done to decouple it from PresentationFramework, which wouldn't have been obvious at the time. I imagine they would need to be moved to a different assembly and removes the dependencies on WPF. Or WPF writes its own ICommand wrapper and uses that internally with the TypeConverter?

elachlan avatar Sep 18 '23 23:09 elachlan

I distinctly remember chatting with @KlausLoeffelmann mentioning that we're bringing the WPF stuff in... :)

WRT: WPFt-rimming - why not make an assumption here - if no <UseWpf>true</UseWpf> present, then purge it all away?

WRT: attributes/typeconverter trimming - the PropertyGrid makes heavy use of those, so care has to be taken here.

RussKie avatar Sep 19 '23 13:09 RussKie

/cc: @kant2002

RussKie avatar Sep 19 '23 13:09 RussKie

WRT: WPFt-rimming - why not make an assumption here - if no <UseWpf>true</UseWpf> present, then purge it all away?

That sounds reasonable. As soon as someone drags ElementHost into the game, we need to change the vbproj (SCNR) and enable <UseWpf>true</UseWpf>. Should work in those cases!

KlausLoeffelmann avatar Sep 19 '23 15:09 KlausLoeffelmann

@vitek-karas How difficult would it be to add configuration to ignore resolution of specified types? Say, if we indicated that we don't want ICommand parsed?

JeremyKuhne avatar Sep 19 '23 17:09 JeremyKuhne

@vitek-karas How difficult would it be to add configuration to ignore resolution of specified types? Say, if we indicated that we don't want ICommand parsed?

Can we fix the Sdk to not pass the WPF assembly references if UseWpf is not specified? This would fix the problem too and work exactly the same between publish and F5 debug.

Trimming flags that cause differences in behavior between F5 debug and a published app (without generating a warning about it at publish time) are typically reserved as last ditch effort when nothing else can work. Users don't like when that happens.

MichalStrehovsky avatar Sep 19 '23 21:09 MichalStrehovsky

Can we fix the Sdk to not pass the WPF assembly references if UseWpf is not specified?

This could likely also fix another issue I didn't mention: an empty WinForms app published with PublishAot also dumps 5 native DLLs into publish output (D3DCompiler_47_cor3.dll, PenImc_cor3.dll, PresentationNative_cor3.dll, vcruntime140_cor3.dll, wpfgfx_cor3.dll). I assume these are all WPF. They are doubling the size of a NativeAOT publish (provided we can fix all the issues discussed in top-post) - while the WinForms app can be 7 MB in size in theory, all of these together cost another 8 MB.

MichalStrehovsky avatar Sep 20 '23 07:09 MichalStrehovsky

So there are winforms changes and SDK changes required here? Do we have an SDK tracking issue?

elachlan avatar Oct 17 '23 00:10 elachlan

So there are winforms changes and SDK changes required here? Do we have an SDK tracking issue?

I didn't file one because the SDK repo backlog is filled with untriaged issues from years ago that nobody is looking at. In my experience the way to get work done in the SDK repo is to submit a pull request. The actual fix might also be in https://github.com/dotnet/windowsdesktop - I'm not quite sure what component decides the assembly references and native files we need.

MichalStrehovsky avatar Oct 17 '23 08:10 MichalStrehovsky

I was looking into this and it looks like there is already a mechanism for targeting packs to only reference a subset of the WindowsDesktop files depending on the values of UseWPF and UseWindowsForms . Perhaps something similar could be done for the runtime packs.

Existing mechanism for targeting packs

In the .NET 8 RC2 SDK's Sdks/Microsoft.NET.Sdk.WindowsDesktop/targets/Microsoft.NET.Sdk.WindowsDesktop.props file there is:

    <FrameworkReference Include="Microsoft.WindowsDesktop.App" IsImplicitlyDefined="true"
                        Condition="('$(UseWPF)' == 'true') And ('$(UseWindowsForms)' == 'true')"/>

    <FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" IsImplicitlyDefined="true"
                        Condition="('$(UseWPF)' == 'true') And ('$(UseWindowsForms)' != 'true')"/>

    <FrameworkReference Include="Microsoft.WindowsDesktop.App.WindowsForms" IsImplicitlyDefined="true"
                        Condition="('$(UseWPF)' != 'true') And ('$(UseWindowsForms)' == 'true')"/>

So there are different ReferenceReferences depending on what combination of UseWPF and UseWindowsForms properties are set.

In the .NET 8 RC2 SDK's Microsoft.NETCoreSdk.BundledVersions.props file you can see that these difference KnownFrameworkReference share the same targeting packages and runtime packs, but have different settings for Profile:

    <KnownFrameworkReference Include="Microsoft.WindowsDesktop.App"
                              TargetFramework="net8.0"
                              RuntimeFrameworkName="Microsoft.WindowsDesktop.App"
                              DefaultRuntimeFrameworkVersion="8.0.0-rc.2.23479.10"
                              LatestRuntimeFrameworkVersion="8.0.0-rc.2.23479.10"
                              TargetingPackName="Microsoft.WindowsDesktop.App.Ref"
                              TargetingPackVersion="8.0.0-rc.2.23479.10"
                              RuntimePackNamePatterns="Microsoft.WindowsDesktop.App.Runtime.**RID**"
                              RuntimePackRuntimeIdentifiers="win-x64;win-x86;win-arm64"
                              IsWindowsOnly="true"
                              />

    <KnownFrameworkReference Include="Microsoft.WindowsDesktop.App.WPF"
                              TargetFramework="net8.0"
                              RuntimeFrameworkName="Microsoft.WindowsDesktop.App"
                              DefaultRuntimeFrameworkVersion="8.0.0-rc.2.23479.10"
                              LatestRuntimeFrameworkVersion="8.0.0-rc.2.23479.10"
                              TargetingPackName="Microsoft.WindowsDesktop.App.Ref"
                              TargetingPackVersion="8.0.0-rc.2.23479.10"
                              RuntimePackNamePatterns="Microsoft.WindowsDesktop.App.Runtime.**RID**"
                              RuntimePackRuntimeIdentifiers="win-x64;win-x86;win-arm64"
                              IsWindowsOnly="true"
                              Profile="WPF"
                              />

    <KnownFrameworkReference Include="Microsoft.WindowsDesktop.App.WindowsForms"
                              TargetFramework="net8.0"
                              RuntimeFrameworkName="Microsoft.WindowsDesktop.App"
                              DefaultRuntimeFrameworkVersion="8.0.0-rc.2.23479.10"
                              LatestRuntimeFrameworkVersion="8.0.0-rc.2.23479.10"
                              TargetingPackName="Microsoft.WindowsDesktop.App.Ref"
                              TargetingPackVersion="8.0.0-rc.2.23479.10"
                              RuntimePackNamePatterns="Microsoft.WindowsDesktop.App.Runtime.**RID**"
                              RuntimePackRuntimeIdentifiers="win-x64;win-x86;win-arm64"
                              IsWindowsOnly="true"
                              Profile="WindowsForms"
                              />

In the targeting pack's data/FrameworkList.xml file, the <File /> tags have a Profile attribute. The ResolveTargetingPack task considers the profile when adding references to assemblies.

Idea for runtime pack

The runtime pack Nuget package has a file called data/RuntimeList.xml. If the <File /> element had a Profile attribute one them like the ones in the targeting pack's FrameworkList.xml file, the ResolveRuntimePack task could be changed to consider the Profile attribute when choosing which files to publish.

To add the Profile element to the RuntimeList.xml file, I think you need to add FrameworkListFileClass elements to https://github.com/dotnet/windowsdesktop/blob/main/src/windowsdesktop/src/sfx/Microsoft.WindowsDesktop.App.Runtime.sfxproj . I believe this is how the targeting pack sets them in https://github.com/dotnet/windowsdesktop/blob/main/src/windowsdesktop/src/sfx/Microsoft.WindowsDesktop.App.Ref.sfxproj .

AustinWise avatar Oct 18 '23 18:10 AustinWise

Similar issue raised in 2020: #3723

elachlan avatar Nov 24 '23 00:11 elachlan

A thing that Sizoscope currently doesn't show is manifest resources. Out of the 15.2 MB above, 7.5 MB are embedded resources.

Just an update that when using .NET 9 and latest Sizoscope, manifest resources now do show up so this can now be visualized properly.

image

MichalStrehovsky avatar Nov 30 '23 21:11 MichalStrehovsky

@MichalStrehovsky any standouts? I think we have quite a few icons and even more string localizations.

elachlan avatar Dec 01 '23 01:12 elachlan

quite a few icons and even more string localizations.

Yes, that :).

You can crossreference what Sizoscope reports with ILSpy on the original managed assembly (Sizoscope doesn't see the contents).

MichalStrehovsky avatar Dec 01 '23 09:12 MichalStrehovsky

TrimTest project, which we are tracking as part of WinForms trimming, gives a similar view as above from sizoscope.

LakshanF avatar Feb 02 '24 13:02 LakshanF

.

NCLnclNCL avatar Apr 17 '24 01:04 NCLnclNCL

3a11734ba30d5a161b3f966c4586e3c5

With the latest nightly build, the size of nativeaot winforms app comes to 19mb.

Note that the published winforms app comes with unnecessary native dependency dll from wpf, while you can delete those dll manually.

hez2010 avatar May 11 '24 03:05 hez2010