[Mono.Android] Extend JNINativeWrapper.CreateBuiltInDelegate
exploration
#9309
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Note: This is just a POC exploration for discussion, it is not intended to be committed.
Note: All performance numbers mentioned are run on Android Emulator on a DevBox, so they are somewhat inflated.
Run a
Release
version of thedotnet new android
template and it takes1.385s
to start up. Now add the following code toMainActivity.cs
:Compile and run the app again, now it takes
1.608s
to start up, an increase of223ms
. What gives?It turns out that part of #6657 is a "marketing" performance optimization. By ensuring every delegate needed by our template is "built-in" we never hit a delegate that needs
System.Runtime.Emit
. However once a user adds most any other code they will hit a delegate that isn't "built-in" which needs SRE and they will take the considerable perf hit of initializing SRE and generating the first delegate.To avoid this, what if we took the concept of "built-in" delegates and formalized and expanded it? That is, each version of
netX.0-android
would contain a known set of built-in delegates that libraries could depend on.If we scan
Mono.Android
plus the ~630 AndroidX/etc. libraries we currently bind, we find that there are 1037 unique delegates in our "ecosystem". This PR adds all of them toJNINativeWrapper.CreateBuiltInDelegate
, thus avoiding initializing SRE and taking the performance hit.Tradeoffs
Per our
apk-diff
unit test on CI (BuildReleaseArm64
), adding an additional 1000 delegates and wrapper functions comes at a cost in.apk
size of ~61 KB:Unfortunately,
JNINativeWrapper.CreateBuiltInDelegate
acts as a "choke-point" method in that it references every built-in delegate and wrapper, so unused ones cannot be removed by the trimmer.Enhancements
In the long run, we can avoid the
.apk
size increase by making this process trimmer friendly. In addition toJNINativeWrapper.CreateDelegate
we could expose the individual delegate creation methods:Because the list of built-in delegates is a per-.NET level contract, a binding library targeting
net10.0-android
knows that it can replace calls toJNINativeWrapper.CreateDelegate
with calls to explicit delegate creator methods:This provides 2 benefits:
switch
statement inJNINativeWrapper.CreateBuiltInDelegate
.JNINativeWrapper.CreateDelegate
is no longer called, all unused delegates and wrappers will be trimmed out of the final application.What About SRE?
One will note this doesn't actually eliminate the original problem of needing SRE, it just makes it much less likely. If a user binds a library with a delegate we've never seen before they'll fall back to SRE. We can eliminate this by adding any missing delegates to their application assembly.
Before we compile the user application, we need to scan their referenced assemblies for any delegates not part of the supported "built-in" set. We then need to generate the C# delegate wrappers and add the generated code to their application. Lastly we need to pass a reference to this fall-back method to
JNINativeWrapper
on app startup so it can use the fall-back.Then at app startup we pass this to
JNINativeWrapper
:And
JNINativeWrapper.CreateDelegate
uses it as a fallback ifCreateBuiltInDelegate
fails:SRE Is Gone Now?
Almost!
There is still a case where SRE would be used: if the user is referencing a Classic binding assembly built before we changed from using
Action<T>
/Func<T>
to_Jni_Marshal_*
delegates. This feels like an acceptable limitation. We could add a build warning when this case is detected if desired.So SRE would be gone for pure .NET for Android applications.