Repository navigation
Lazily initialize JCW delegates #9306
Description
Activity
- addedenhancementProposed change to current functionality.Proposed change to current functionality.Area: App RuntimeIssues in `libmonodroid.so`.Issues in `libmonodroid.so`.Area: PerformanceIssues with performance.Issues with performance.
on Sep 16, 2024 - addedneeds-triageIssues that need to be assigned.Issues that need to be assigned.
on Sep 16, 2024 - removedneeds-triageIssues that need to be assigned.Issues that need to be assigned.
on Sep 17, 2024 I like the idea in general, however there is one thing that has to be measured in a larger example is the impact of adding another
staticfield per each overridden method. Static state is cost that cannot be avoided by end user and I generally try to avoid it. Perhaps we could wrap the entire state in astructinstead?It would be interesting to see results of those tests on a real device. Marshal methods are, by design, 100% lazy - there is no overhead other than Java VM doing a symbol lookup the first time the method is called. 55ms sounds a bit much, but I guess it's Java VM calling
dlsym100 times onlibxamarin-app.so, I wonder if this could be somehow improved (probably not).Ideally, this could be done in the Java(?) layer as it could then improve all existing bindings
I'm not at all sure how to do that.
The problem is that
Get*Handler()is expected to return a delegate which can be directly registered withJNIEnv::RegisterNatives():android/src/Mono.Android/Android.Runtime/AndroidRuntime.cs
Lines 547 to 549 in b667af5
GetCallbackHandler connector = (GetCallbackHandler) Delegate.CreateDelegate (typeof (GetCallbackHandler), callbackDeclaringType, callbackString.ToString ()); callback = connector (); The only straightforward way to "automatically add" a layer of indirection would be for
JNINativeWrapper.CreateDelegate()to do that layer of indirection, which would require Systems.Reflection.Emit, thus defeating the entire idea.I think this needs to be done in bindings.
What I would prefer is: dotnet/runtime#108211
If/when we have "proper" dotnet/runtime construct that doesn't require System.Reflection.Emit, then we can update
generatorto make use of it. This would allow us to removeJNINativeWrapper.CreateDelegate()and its use of System.Reflection.Emit from method overrides entirely.(We would also need such a construct for eventual NativeAOT support. Exception handling must be baked into the binding assemblies, not delegated off to
JNINativeWrapper.CreateDelegate(), as the latter cannot possibly work with NativeAOT.)Building upon my earlier comment, I believe that this would be better, if "we" can get all of our proverbial ducks in a row: dotnet/java-interop#1258
- added a commit that references this issue
on Dec 4, 2024 This is largely fixed by removing SRE from our JCW delegates in .NET 10: dotnet/java-interop#1275.
- locked and limited conversation to collaborators
on Feb 7, 2025
Note: all performance numbers mentioned are run on Android Emulator on a DevBox, so they are somewhat inflated.
Imagine we have a Java type that contains a lot of virtual methods:
Now imagine we bind it, and override all of those methods:
Then we create an instance of this object in C#:
new MyDelegateTester().For a
Releaseconfig with marshal methods turned off, this object creation takes ~694ms. (As an aside, this case really shines with marshal methods.)The issue is that we call
mono.android.Runtime.registerfrom thestaticconstructor of the generated Java peer type, and this calls the C# generatedGet*Handlerimmediately for every overridden method (in this case 100) even if they aren't immediately used. If we could make these calls lazy instead it would likely improve app startup and navigating to a new activity, since these are places where objects are often created but are not used until later.Ideally, this could be done in the Java(?) layer as it could then improve all existing bindings. However it could also be done at the binding level.
Instead of:
We could add an extra level of indirection:
Switching to a test case of calling a single method instead of 100, we can see that
CreateDelegatecost has moved from the constructor to the first invoke of the method:Note: The cost is abnormally high in this example because it is the first call that uses S.R.E, causing the overhead of initializing those APIs.