servicetitan/Stl.Fusion

Stl.Interception gets trimmed away

AliveDevil opened this issue · 11 comments

Trying to get Stl.Interception TypedFactory to not get trimmed away in .NET 8.
image

Might need some C# 12 features to get working correctly: Interceptors, such that the proxy is known at compile time and isn't stripped away completely.

Not quite sure when I can take a moment to implement this, but trimming reduces the self-contained single file publish from 90 MiB to 20 MiB.

Hi, yes, there was no IL trimming markup.

I've just added one: 12e586d

The downside is: prob. it still won't work, coz interceptor types aren't referenced. One solution would be to generate module initializers to "hold" them.

For now you can manually reference these types - try TypeExt.MarkUsed<T>(), I added it for this purpose.

Honestly, I don't understand why they even came up with NativeAOT:

  • Static analysis isn't good enough to resolve even simple things - e.g. you pass ICommand<T> & want to construct CommandContext<T>. How can you do this, assuming you pass TCommand or ICommand (base type)?
  • Stl.Rpc & Fusion use way more complex transforms in some cases - e.g. RpcInboundCall<TResult> is used for any incoming call with result type TResult; ArgumentList<TArg1, TArg2, CancellationToken> is transformed to ArgumentList<TArg1, TArg2>, etc. - all these decisions are made relying on Reflection.
  • IL Emit is used in a decent number of scenarios as well - e.g. to generate fast getters for properties which aren't known at compile time.
  • Things like DbEntityResolver use expression trees to build pretty advanced queries.
  • Etc.

In other words, I don't see how purely static AOT is even usable anywhere but in some toy projects.

What they should really focus on is profile-guided AOT - AOT is almost solely about startup time, and all you need is to know what runs on startup. And it's easy - just run & record it. And it's 100% fine if you compile just this & interpret or JIT-compile the rest - in fact, they do exactly this on MAUI Android.

One other thing with NativeAOT is: you may try to play with it if you start something new. But imagine a huge app that's already there - what kind of mindset you are supposed to have to decide to try it, if it's a huge investment, and "all or nothing" in the end, but most likely "nothing"? Just look at these 2.5K of code in above commit - what I was doing is ~ "ok, let's do the best we can & see what stuff I can't resolve statically". And I can name like 10+ pretty complex scenarios I simply couldn't cover (some are listed above). And that's for a single library.

Long story short... IDK who came up with NativeAOT idea, but this guy definitely doesn't know how real-world apps look like.

I kinda angry about this b/c they invest so much into this crap vs focusing on what really matters - i.e. profile-based AOT. All this shit with [DynamicallyAccessedMembers] no one really wants to see - why, why to even pollute the code with all of that stuff, if it doesn't really work?

And it's the same about trimming. Just decide what you want to keep in AOT vs MSIL form based on stored startup profile, that's it!

And it would be way more useful if, instead of focusing on what to trim, they'd focus on how to split the code into 3 "pieces":

  • Absolutely necessary (comes as AOT/native code, loaded before app startup)
  • Reachable in 2-3 hops (comes as MSIL, loaded before app startup)
  • Maybe necessary (comes as MSIL, loaded in the background after your app starts).

Think I've struck a nerve here 😅

For now you can manually reference these types - try TypeExt.MarkUsed<T>(), I added it for this purpose.

Nice, will check it.

And it would be way more useful if, instead of focusing on what to trim, they'd focus on how to split the code into 3 "pieces":

That's been tried with ReadyToRun, and requires double the application size for both the managed and AOT compiled bits - which imo wasn't viable from the beginning.

why, why to even pollute the code with all of that stuff, if it doesn't really work?

Checked the commit, and I do agree: That's just a burden.

I'm not that involved in the wider .NET core world, as I'm still dependent on .NET framework … I might be influenced by shiny new things for my personal/hobby projects though - and may very well run that specific app with --self-contained -p:PublishSingleFile=True without ever enabling NativeAot or trimming (the target environment this app should run on isn't constrained in any way).

Just to confirm for future enhancements: All-of Stl should be trimming-friendly, not just parts of it.

I'm fine with a non-solution closure, just to keep this topic in here for future reference.

Nice, will check it.

[DynamicDependency(...All, type(...))] actually does the same, I removed the method :)

Just to confirm for future enhancements: All-of Stl should be trimming-friendly, not just parts of it.

Speaking of which, I just added [DynamicDependency] to every builder indicating what should be kept. So now you should prob. just reference proxy types with [DynamicDependency] from somewhere, later I'll start generating a module initializer for these types to make sure they're kept no matter what.

As for NativeAOT, the main issue is that Reflection and IL emit is broken there. So all they need is to add an interpreter to handle this - it's kinda fine in most of scenarios, even if it's slower.

I see. So the generated proxy still gets trimmed, but the factory interceptor is kept in the trimmed build.

Module initializer is definitely the easiest solution here.

@AliveDevil closing this, + just want to let you know that the most up-to-date version of Fusion is here: https://github.com/ActualLab/Fusion , and it's much more trim-compatible.