dotnet / runtimelab

This repo is for experimentation and exploring new ideas that may or may not make it into the main dotnet/runtime repo.
MIT License
1.42k stars 198 forks source link

ResourceManager not working in reflection-free mode #1327

Closed kant2002 closed 3 years ago

kant2002 commented 3 years ago

I try to run simplest reading from resources using codegen from Resx files.

With this configuration

<ItemGroup>
  <RuntimeHostConfigurationOption Include="Switch.System.Reflection.Assembly.SimulatedLocationInBaseDirectory" Value="true" />
  <RuntimeHostConfigurationOption Include="Switch.System.Reflection.Disabled.DoNotThrowForNames" Value="true" />
  <RuntimeHostConfigurationOption Include="Switch.System.Reflection.Disabled.DoNotThrowForAssembly" Value="true" />
</ItemGroup>

It fails at this line

https://github.com/dotnet/runtimelab/blob/b4c3760e16d8add69df5e5ae4ed6ac18aa3098b4/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Resources/ManifestBasedResourceGroveler.CoreRT.cs#L21

with NotImplementedException.

My massage skills fails at this point.

MichalStrehovsky commented 3 years ago

Reflection free mode doesn't have access to manifest resources, so there's no chance of it working. We disable framework resource strings in reflection free mode, which is how it works for framework code:

https://github.com/dotnet/runtimelab/blob/b4c3760e16d8add69df5e5ae4ed6ac18aa3098b4/src/coreclr/nativeaot/BuildIntegration/Microsoft.NETCore.Native.targets#L45

Non-framework code will have to define feature switches like framework code does.

I don't have high hopes of resource manager working in reflection free mode ever (even if we add type-level reflection and manifest resources). Resource manager reads custom attributes and that's just full unavoidable reflection.

kant2002 commented 3 years ago

@MichalStrehovsky Can I speculate about that one issue a bit more.

Issue which I hit was when loading satellite assemblies. Let's say we have WinForms application without localization, but who use resources. All I want for reflection-free method is that it assumes that no satellite assemblies present (as current limitation) and just go and continue working as usual. Then next location were I expect issues would be ComponentResouceManager.ApplyResources which I thought I can augment using source generators. But, then I see ((System.Drawing.Image)(resources.GetObject("toolStripSplitButton3.Image"))) in my code, and realize that all hopes are lost.

Anyway, let me ask, is it possible to disable this codepath entirely using new RuntimeHostConfigurationOption? Maybe that at least give some limited progress on the matter.

Do you aware about any work/plans/experiments to have AOT-friendly resources management? Xamarin/MAUI seems to be solved that by using just only string resources in RESX files.

MichalStrehovsky commented 3 years ago

There's a difference between AOT friendly and reflection-free-mode friendly. I don't think there's going to be a concentrated effort for reflection-free-friendly anytime soon.

We can make it so that the reflection-free-mode can say "there are no custom attributes on anything" (instead of throwing), but that's the extent of it.

The current reflection free mode is very restrictive and I don't think we should invest in it much further. We need to replace System.Private.DisabledReflection with the real reflection stack that has certain codepaths stubbed out so that this effort is sustainable. S.P.DisabledReflection is a tech demo more than anything else.

kant2002 commented 3 years ago

I'm more then happy with opt-out mode, and reflection-free is not a goal in itself. I use it just to push some things to extreme. For example currently it drop size of WinForms app from 33Mb to 13Mb, which I believe interesting property. So I continue flying like a sparrow, chipping here and there. Thanks for your support.

jkotas commented 3 years ago

drop size of WinForms app from 33Mb to 13Mb

Make sure to enable trimming for WinForms .dlls. I suspect that this large drop is caused by reflection-free mode enabling trimming for WinForms .dlls as a side-effect. You should be able to get most of it by just enabling trimming for WinForms .dlls.

kant2002 commented 3 years ago

I start with following setup in project file

<PropertyGroup>
    <OutputType>WinExe</OutputType>
    <TargetFramework>net6.0-windows</TargetFramework>
    <UseWindowsForms>true</UseWindowsForms>
    <TrimMode>link</TrimMode>
    <DisableImplicitNamespaceImports>true</DisableImplicitNamespaceImports>
    <IlcGenerateDgmlFile>false</IlcGenerateDgmlFile>

    <IlcGenerateStackTraceData>false</IlcGenerateStackTraceData>
    <IlcInvariantGlobalization>true</IlcInvariantGlobalization>
    <IlcFoldIdenticalMethodBodies>true</IlcFoldIdenticalMethodBodies>
    <IlcOptimizationPreference>Size</IlcOptimizationPreference>
    <IlcDisableReflection>false</IlcDisableReflection>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
  </PropertyGroup>

  <ItemGroup>
    <RdXmlFile Include="rd.xml" />
  </ItemGroup>

  <ItemGroup Condition="$(IlcDisableReflection) == true">
    <RuntimeHostConfigurationOption Include="Switch.System.Reflection.Assembly.SimulatedLocationInBaseDirectory" Value="true" />
    <RuntimeHostConfigurationOption Include="Switch.System.Reflection.Disabled.DoNotThrowForNames" Value="true" />
    <RuntimeHostConfigurationOption Include="Switch.System.Reflection.Disabled.DoNotThrowForAssembly" Value="true" />
  </ItemGroup>

Then once I see your comment I start playing around and add following

<PropertyGroup>
    <PublishTrimmed>true</PublishTrimmed>
    <TrimmerDefaultAction>link</TrimmerDefaultAction>
  </PropertyGroup>

<ItemGroup>
    <TrimmableAssembly Include="System.Windows.Forms" />
    <TrimmableAssembly Include="System.Windows.Forms.Primitives" />
  </ItemGroup>

<Target Name="ConfigureTrimming" BeforeTargets="PrepareForILLink">
  <ItemGroup>
    <ManagedAssemblyToLink Condition="'%(Filename)' == 'System.Windows.Forms'">
      <IsTrimmable>true</IsTrimmable>
    </ManagedAssemblyToLink>
    <ManagedAssemblyToLink Condition="'%(Filename)' == 'System.Windows.Forms.Primitives'">
      <IsTrimmable>true</IsTrimmable>
    </ManagedAssemblyToLink>
  </ItemGroup>
</Target>

And only Reflection-Free mode drop size by 20Mb. Am I miss some other flag which I should turn on?

MichalStrehovsky commented 3 years ago

It's easiest to see by investigating the RSP file passed to the compiler with one and the other project settings.

If you see any --conditionalroot or --root arguments, it's reflection-rooting things.

kant2002 commented 3 years ago

That's patch between RSP files. Reflection - 33Mb, No Reflection -13Mb.

224d223
< -r:packages\runtime.win-x64.microsoft.dotnet.ilcompiler\6.0.0-rc.1.21370.1\sdk\System.Private.Reflection.Core.dll
234d232
< --initassembly:System.Private.StackTraceMetadata
236c234
< --initassembly:System.Private.Reflection.Execution
---
> --initassembly:System.Private.DisabledReflection
238a237,239
> --appcontextswitch:Switch.System.Reflection.Assembly.SimulatedLocationInBaseDirectory=true
> --appcontextswitch:Switch.System.Reflection.Disabled.DoNotThrowForNames=true
> --appcontextswitch:Switch.System.Reflection.Disabled.DoNotThrowForAssembly=true
239a241
> --appcontextswitch:System.Diagnostics.Tracing.EventSource.IsSupported=false
241a244
> --appcontextswitch:System.Resources.UseSystemResourceKeys=true
251a255
> --feature:System.Diagnostics.Tracing.EventSource.IsSupported=false
253a258
> --feature:System.Resources.UseSystemResourceKeys=true
261d265
< --scanreflection
263a268,269
> --disablereflection
> --feature:System.Collections.Generic.DefaultComparers=false

Only one --root and no --conditionalroot in both cases.

--root:obj\Release\net6.0-windows\win-x64\SampleWindowsForms.dll
MichalStrehovsky commented 3 years ago

Interesting. I'll have to see what's going on. Such big size diff is not expected. No reflection 13 MB, and with reflection ~19 MB would be more in line with my expectations.

kant2002 commented 3 years ago

Maybe you can re-open? It seems to be it can be fruitful in some sense.

MichalStrehovsky commented 3 years ago

The specific line of ResourceManager will work when dotnet/runtime#67193 is done because we will have metadata about assemblies and types. Maybe at that time we can revisit making it work with reflection-free mode, but as of now it's beyond saving.

kant2002 commented 3 years ago

I'm interesting more in your findings about size. More-usable reflection-free mode is more then fine for me. If I want it to work, some leg work on me.

kant2002 commented 3 years ago

Now that I can use PerfView I compare two applications

Reflection 33Mb

image

Reflection-free 13Mb

image

First two lines in Reflection mode is _embeded_metadata_End and _embeded_metadata and they take a lot of space.

MichalStrehovsky commented 3 years ago

The PerfView view might be assuming .NET Native name mangling and such.

_embeded_metadata_End is a symbol that simply points at the fist byte after the metadata blob. It has "0 size". It makes me trust less in what the rest is showing.

The size of the metadata blob is better visible in the *.map file generated by ILCompiler.

You can decrease the size of the metadata blob by adding --reflectedonly switch to ILC command line. It might help a bit.

Also, is there a difference in stack trace data setting between no-reflection and reflection? I see that --initassembly:System.Private.StackTraceMetadata is also in your diff, which would indicate no-reflection doesn't have stack trace metadata either (also there's a bunch of --feature: switches that give an advantage to the reflection-free mode and can also be enabled in the reflection-enabled mode.

kant2002 commented 3 years ago

So still something left which takes 13Mb.

When I compare using map.xml files, I have easy explanation for 7Mb, __embedded_metadata => 1,168,912 and __embedded_resourcedata => 6,634,396, but other changes from code trimming.

MichalStrehovsky commented 3 years ago

Ah, the embedded resources are pretty big at 6.6 MB. The bad news is that these are the thing ResourceManager reads, so the fact we're stripping them is currently breaking the app, but we don't know that because the reflection that gets to that data is broken in the first place. The only reason why we can strip it right now is that in reflection-free mode there's no API to get to that data.

Reflection-free size will grow by at least 6.6 MB once we fix that.

Then you're looking at a size difference of 19.6 MB (13 MB + 6.6 MB) vs 26 MB and that difference doesn't look so terrible.

Basically once we do dotnet/runtime#67193, size of the WinForms app will be somewhere between 19.6 and 26 MB.

kant2002 commented 3 years ago

Thanks, at least now it make sense that reflection-free is not a free lunch.

MichalStrehovsky commented 3 years ago

It might be worthwhile to look into the 6.6 MB of data though - I assume this is all from Windows.Forms, not from BCL. We drop pretty much all resources from the BCL when <UseSystemResourceKeys>true</UseSystemResourceKeys> is specified in the project thanks to extra annotations in the BCL that tell the compiler to drop the resources.

I assume some of the resources in Windows.Forms are resource strings for exception messages - we could drop them with similar annotations. But 6.6 MB is a lot to be just the exception messages - it's possible there's more stuff, but because of the nature of resources, none of it can be trimmed even if it's unused.

kant2002 commented 3 years ago

I have easier target to trim WinForms app. If I make a switch to PictureBox to not use remote loading of the images (they still use WebRequest), that may trim all networking stack from the application. And I also see some XML parts which probably can be trimmed as well. I'm not ready yet to make that part of my carpaccio.