Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[generator] emit faster overloads for non-virtual Formatted props (#1101
) Context: dotnet/maui#12130 Context: #994 Context: 79a8e1e Context: xamarin/monodroid@23f4212 When binding members which have parameter types or return types which are `java.lang.CharSequence`, the member is "overloaded" to replace `CharSequence` with `System.String`, and the "original" member has a `Formatted` *suffix*. For example, consider [`android.widget.TextView`][1], which has [`getText()`][1] and [`setText()`][2] methods which have parameter types and return types which are `java.lang.CharSequence`: // Java /* partial */ class TextView extends View { public CharSequence getText(); public final void setText(CharSequence text); } When bound, this results in *two* properties: // C# partial class TextView : View { public Java.Lang.ICharSequence? TextFormatted {get => …; set => …; } public string? Text {get => …; set => …; } } This is also done for methods; see also 79a8e1e. The "non-`Formatted` overload" works by creating `String` temporaries to invoke the `Formatted` overload: partial class TextView { public string? Text { get => TextFormatted?.ToString (); set { var jls = value == null ? null : new Java.Lang.String (value); TextFormatted = jls; jls?.Dispose (); } } } *Why* was this done? Because [C# 4.0][3] didn't allow interfaces to provide conversion operators. ([C# 8.0][4] would add support for interfaces to contain operators.) "Overloading" in this fashion made it easier to use `System.String` literals with `ICharSequence` members; compare: view.Text = "string literal"; // vs. view.TextFormatted = new Java.Lang.String("string literal"); // …and who would know how to do this? A problem with the this approach is performance: creating a new `Java.Lang.String` instance requires: 1. Creating the managed peer (the `Java.Lang.String` instance), 2. Creating the native peer (the `java.lang.String` instance), 3. And *registering the mapping* between (1) and (2) which feels a bit "silly" when we immediately dispose of the value. This is particularly noticeable with .NET MAUI apps. Consider the [angelru/CvSlowJittering][5] app, which uses XAML to set `Text` properties, which eventually hit `TextView.Text`. Profiling shows: 653.69ms (6.3%) mono.android!Android.Widget.TextView.set_Text(string) 198.05ms (1.9%) mono.android!Java.Lang.String..ctor(string) 121.57ms (1.2%) mono.android!Java.Lang.Object.Dispose() *6.3%* of scrolling time is spent in the `TextView.Text` property setter! *Partially optimize* this case: if the `*Formatted` member is (1) a property, and (2) *not* `virtual`, then we can directly call the Java setter method. This avoids the need to create a managed peer and to register a mapping between the peers: // New hotness partial class TextView { public string? Text { get => TextFormatted?.ToString (); // unchanged set { const string __id = "setText.(Ljava/lang/CharSequence;)V"; JniObjectReference native_value = JniEnvironment.Strings.NewString (value); try { JniArgumentValue* __args = stackalloc JniArgumentValue [1]; __args [0] = new JniArgumentValue (native_value); _members.InstanceMethods.InvokeNonvirtualVoidMethod (__id, this, __args); } finally { JniObjectReference.Dispose (ref native_value); } } } } [The result][6]? | Method | Mean | Error | StdDev | Allocated | |-------------------- |---------:|----------:|----------:|----------:| | Before SetFinalText | 6.632 us | 0.0101 us | 0.0079 us | 112 B | | After SetFinalText | 1.361 us | 0.0022 us | 0.0019 us | - | The `TextView.Text` property setter invocation time is reduced to 20% of the previous average invocation time. Note: We *cannot* do this "inlining" if the "`Formatted` overload" is [`virtual`][7], as that will result in broken semantics that make sense to *nobody*. TODO: Consider Optimizing the "`virtual` overload" scenario? This could be done by updating `Java.Lang.String`: partial interface ICharSequence { public static String FromJniObjectReference (ref JniObjectReference reference, JniObjectReferenceOptions options); } Then updating the "non-`Formatted` overload" to use `String.FromJniHandle()`: partial class TextView { public string? Text { get => TextFormatted?.ToString (); // unchanged set { JniObjectReference native_value = JniEnvironment.Strings.NewString (value); var java_value = Java.Lang.ICharSequence.FromJniObjectReference (ref native_value, JniObjectReferenceOptions.CopyAndDoNotRegister); TextFormatted = java_value; java_value?.Dispose (); } } } [0]: https://developer.android.com/reference/android/widget/TextView [1]: https://developer.android.com/reference/android/widget/TextView#getText() [2]: https://developer.android.com/reference/android/widget/TextView#setText(java.lang.CharSequence) [3]: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-40 [4]: https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-version-history#c-version-80 [5]: https://github.com/angelru/CvSlowJittering [6]: https://github.com/jonathanpeppers/BenchmarkDotNet-Android/tree/Android.Widget.TextView [7]: #994 (comment)
- Loading branch information