Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: call partial methods within method bindings #1085

Closed
jonpryor opened this issue Feb 16, 2023 · 4 comments
Closed

Proposal: call partial methods within method bindings #1085

jonpryor opened this issue Feb 16, 2023 · 4 comments
Labels
generator Issues binding a Java library (generator, class-parse, etc.) proposal Issue raised for discussion, we do not even know if the change would be desirable yet
Milestone

Comments

@jonpryor
Copy link
Member

Context: dotnet/runtime#82121

Background: When should the GC run? If a "large" object is allocated, should that change the GC heuristics for when it runs?

If someone allocates a large Bitmap, should we GC more often on devices with low memory?

We've had questions around this for quite some time, but the only "reasonable" way to alter GC heuristics was by using GC.AddMemoryPressure() and GC.RemoveMemoryPressure(), but Mono didn't support these methods, so it was all moot: there was no way to inform the GC about "implicit" non-managed memory allocations, so we didn't.

That is now changing with dotnet/runtime#82121

Meaning there is now a way to tell our GC about implicit non-managed memory usage!

Background: Binding: Consider the current (abbreviated) binding for Android.Grpahics.Bitmap:

namespace Android.Graphics {
	public sealed partial class Bitmap {
		internal Bitmap (IntPtr javaReference, JniHandleOwnership transfer) : base (javaReference, transfer)
		{
		}

		public unsafe int AllocationByteCount {
			get {
				const string __id = "getAllocationByteCount.()I";
				try {
					var __rm = _members.InstanceMethods.InvokeAbstractInt32Method (__id, this, null);
					return __rm;
				} finally {
				}
			}
		}

		public static unsafe Android.Graphics.Bitmap? CreateBitmap (Android.Graphics.Bitmap src)
		{
			const string __id = "createBitmap.(Landroid/graphics/Bitmap;)Landroid/graphics/Bitmap;";
			try {
				JniArgumentValue* __args = stackalloc JniArgumentValue [1];
				__args [0] = new JniArgumentValue ((src == null) ? IntPtr.Zero : ((global::Java.Lang.Object) src).Handle);
				var __rm = _members.StaticMethods.InvokeObjectMethod (__id, __args);
				return global::Java.Lang.Object.GetObject<Android.Graphics.Bitmap> (__rm.Handle, JniHandleOwnership.TransferLocalRef);
			} finally {
				global::System.GC.KeepAlive (src);
			}
		}
	}
}

What would we like to do? We'd like to "alter" certain methods so that we can "reasonably" tell the GC "this instance has x bytes of associated memory".

How do we do that?

Proposal: Update generator to emit C# partial methods for each bound member and constructor, allowing partial classes to be written which allow "extra work" to be done when the bound method is executed.

Open question: How many partial methods should we have, and where should they be called from?

  • One, before/after the JNI call? (Which one?)
  • Two, on member entry & exit?
  • What should the name convention be? "Obvious" is _On prefix + member name + some kind of suffix. What about overloads? Should we care about overloads, or have all overloads invoke the same set of partial methods?

How many such partial methods do we need, anyway? While trying to write up the following example, it occurs to me that, while having Bitmap.CreateBitmap() call GC.AddMemoryPresure() "makes sense", it doesn't really make sense becaus it calls Object.GetObject<Bitmap>(), which means the Bitmap(IntPtr, JniHandleOwnership) constructor will be invoked. This suggests we may only need one such partial method here, for constructors, and all other members aren't needed for this use case.

Example: Bitmap with partial methods!

namespace Android.Graphics {
	public sealed partial class Bitmap {
		partial void _OnConstructor ();

		internal Bitmap (IntPtr javaReference, JniHandleOwnership transfer) : base (javaReference, transfer)
		{
			_OnConstructor ();
		}

		partial void _OnAllocationByteCount_Entry ();
		partial void _OnAllocationByteCount_Exit (int ret);

		public unsafe int AllocationByteCount {
			get {
				const string __id = "getAllocationByteCount.()I";
				try {
					_OnAllocationByteCount_Entry ();
					var __rm = _members.InstanceMethods.InvokeAbstractInt32Method (__id, this, null);
					var __ret = __rm;
					_OnAllocationByteCount_Exit (__ret);
					return __ret;
				} finally {
				}
			}
		}

		partial void _OnCreateBitmap_Entry (Android.Graphics.Bitmap src);
		partial void _OnCreateBitmap_Exit (Android.Graphics.Bitmap ret);

		public static unsafe Android.Graphics.Bitmap? CreateBitmap (Android.Graphics.Bitmap src)
		{
			_OnCreateBitmap_Entry (src);
			const string __id = "createBitmap.(Landroid/graphics/Bitmap;)Landroid/graphics/Bitmap;";
			try {
				JniArgumentValue* __args = stackalloc JniArgumentValue [1];
				__args [0] = new JniArgumentValue ((src == null) ? IntPtr.Zero : ((global::Java.Lang.Object) src).Handle);
				var __rm = _members.StaticMethods.InvokeObjectMethod (__id, __args);
				var __ret = global::Java.Lang.Object.GetObject<Android.Graphics.Bitmap> (__rm.Handle, JniHandleOwnership.TransferLocalRef);
				_OnCreateBitmap_Exit (__ret);
				return __ret;
			} finally {
				global::System.GC.KeepAlive (src);
			}
		}
	}
}

Once such partial methods exist, we can "reasonably" update Mono.Android.dll to implement required partial methods to Do Something Useful:

partial class Bitmap {

	partial void _OnConstructor ()
	{
		GC.AddMemoryPressure (ByteCount);
	}

	protected override void Dispose (bool disposing)
	{
		if (Handle != IntPtr.Zero) {
			GC.RemoveMemoryPressure (ByteCount);
		}
	}
}

Meta-question: is overriding Dispose() "sufficient enough" in practice for calling GC.RemoveMemoryPressure()? Or could this result in constantly increasing memory pressure?

@jonpryor jonpryor added this to the 8.0.0 milestone Feb 16, 2023
@jonpryor jonpryor added the enhancement Proposed change to current functionality label Feb 16, 2023
@jpobst jpobst removed their assignment Feb 17, 2023
@jpobst jpobst added proposal Issue raised for discussion, we do not even know if the change would be desirable yet and removed enhancement Proposed change to current functionality labels Feb 17, 2023
@jpobst
Copy link
Contributor

jpobst commented Feb 22, 2023

Putting aside whether or not calling GC.AddMemoryPresure() for Bitmap is a good idea (:man_shrugging:), it feels like there may not be a big enough need to warrant a generator change for this.

Could the Bitmap case be covered with some handwritten binding code instead?

Are there a non-trivial amount of other classes that would want to call GC.AddMemoryPresure()?

What would we like to do? We'd like to "alter" certain methods so that we can "reasonably" tell the GC "this instance has x bytes of associated memory".

Note that GC.AddMemoryPresure() does not take an object, so it does not tie memory to an object instance. It simply updates a global "extra memory somewhere" number.

@jonpryor
Copy link
Member Author

jonpryor commented Feb 23, 2023

@jpobst asked:

Could the Bitmap case be covered with some handwritten binding code instead?

Depends on how things play out. If we wanted to use the Bitmap(IntPtr, JniHandleOwnership) constructor, as demo'd above, then no, as there is no way to prevent that constructor from being emitted.

If we instead use things like Bitmap.CreateBitmap(), then yes, we could use metadata to rename that method and "replace" the public method. This in turn suggests "just replace every method that returns Bitmap", which sounds plausible until we realize that there are 85 such methods:

% grep 'return="android.graphics.Bitmap"' bin/BuildDebug/api/api-33.xml.in | wc -l
      85

Eep?

I suspect that a partial method for the "wrapper" constructor may be "enough", and should be fairly straightforward to implement (Famous Last Words™), and that it isn't necessary to provide partial methods for every bound member.

@jpobst
Copy link
Contributor

jpobst commented Feb 24, 2023

I think this is a very niche need, so if we implement it I think it should require opting in via metadata so we don't generate the partial for every class.

Something like:

<attr path="package[@name='android.graphics']/class[@name='Bitmap']" name="constructorPartial">true</attr>

jonpryor added a commit to jonpryor/java.interop that referenced this issue Mar 14, 2023
Context: dotnet#1085
Context: dotnet/runtime#82121

Some Java objects are *big*, e.g. [`Bitmap`][0] instances, but as
far as MonoVM is concerned, the `Bitmap` instances are *tiny*: a
few pointers, and that's it.  MonoVM doesn't know that it could be
referencing several MB of data in the Java VM.

MonoVM is gaining support for [`GC.AddMemoryPressure()`][1] and
[`GC.RemoveMemoryPressure()`][2], which potentially allows for the
parent of all cross-VM GC integrations: using the `GC` methods to
inform MonoVM of how much non-managed memory has been allocated.
This could allow MonoVM to collect more frequently when total process
memory is low.

How should we call `GC.AddMemoryPressure()` and
`GC.RemoveMemoryPressure()`?

`GC.RemoveMemoryPressure()` is straightforward: a class can override
`Java.Lang.Object.Dispose(bool)`.

`GC.AddMemoryPressure()` is the problem: where and how can it be
called from a class binding?  This is trickier, as there was no way
to have custom code called by the bound type.  Instead, various
forms of "hacky workarounds" are often employed, e.g. copying
`generator`-emitted content into a `partial` class, then using
`metadata` to *prevent* `generator` from binding those members.
It's all around fugly.

Fortunately C# has a solution: [`partial` methods][3]!

We take our existing "peer constructor" generation code, a'la:

	partial class Bitmap : Java.Lang.Object {
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	    }
	}

and extend it to:

	partial class Bitmap : Java.Lang.Object {
	    partial void _OnBitmapCreated ();
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	        _OnBitmapCreated ();
	    }
	}

This allows a `partial class Bitmap` to do:

	// Hand-written code
	partial class Bitmap {
	    int _memoryPressure;

	    partial void _OnBitmapCreated ()
	    {
	        _memoryPressure = ByteCount;
	        GC.AddMemoryPressure (_memoryPressure);
	    }

	    protected override void Dispose (bool disposing)
	    {
	        if (_memoryPressure != 0) {
	            GC.RemoveMemoryPressure (_memoryPressure);
	            _memoryPressure = 0;
		}
	    }
	}

TODO: Are there any other places where such `partial` methods would
be useful?  This appears to be the minimum required for this scenario.

[0]: https://developer.android.com/reference/android/graphics/Bitmap
[1]: https://learn.microsoft.com/dotnet/api/system.gc.addmemorypressure?view=net-7.0
[2]: https://learn.microsoft.com/dotnet/api/system.gc.removememorypressure?view=net-7.0
[3]: https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/partial-method
jonpryor added a commit to jonpryor/java.interop that referenced this issue Mar 14, 2023
Context: dotnet#1085
Context: dotnet/runtime#82121

Some Java objects are *big*, e.g. [`Bitmap`][0] instances, but as
far as MonoVM is concerned, the `Bitmap` instances are *tiny*: a
few pointers, and that's it.  MonoVM doesn't know that it could be
referencing several MB of data in the Java VM.

MonoVM is gaining support for [`GC.AddMemoryPressure()`][1] and
[`GC.RemoveMemoryPressure()`][2], which potentially allows for the
parent of all cross-VM GC integrations: using the `GC` methods to
inform MonoVM of how much non-managed memory has been allocated.
This could allow MonoVM to collect more frequently when total process
memory is low.

How should we call `GC.AddMemoryPressure()` and
`GC.RemoveMemoryPressure()`?

`GC.RemoveMemoryPressure()` is straightforward: a class can override
`Java.Lang.Object.Dispose(bool)`.

`GC.AddMemoryPressure()` is the problem: where and how can it be
called from a class binding?  This is trickier, as there was no way
to have custom code called by the bound type.  Instead, various
forms of "hacky workarounds" are often employed, e.g. copying
`generator`-emitted content into a `partial` class, then using
`metadata` to *prevent* `generator` from binding those members.
It's all around fugly.

Fortunately C# has a solution: [`partial` methods][3]!

We take our existing "peer constructor" generation code, a'la:

	partial class Bitmap : Java.Lang.Object {
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	    }
	}

and extend it to:

	partial class Bitmap : Java.Lang.Object {
	    partial void _OnBitmapCreated ();
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	        _OnBitmapCreated ();
	    }
	}

This allows a `partial class Bitmap` to do:

	// Hand-written code
	partial class Bitmap {
	    int _memoryPressure;

	    partial void _OnBitmapCreated ()
	    {
	        _memoryPressure = ByteCount;
	        GC.AddMemoryPressure (_memoryPressure);
	    }

	    protected override void Dispose (bool disposing)
	    {
	        if (_memoryPressure != 0) {
	            GC.RemoveMemoryPressure (_memoryPressure);
	            _memoryPressure = 0;
		}
	    }
	}

TODO: Are there any other places where such `partial` methods would
be useful?  This appears to be the minimum required for this scenario.

[0]: https://developer.android.com/reference/android/graphics/Bitmap
[1]: https://learn.microsoft.com/dotnet/api/system.gc.addmemorypressure?view=net-7.0
[2]: https://learn.microsoft.com/dotnet/api/system.gc.removememorypressure?view=net-7.0
[3]: https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/partial-method
jonpryor added a commit to jonpryor/java.interop that referenced this issue Mar 30, 2023
Context: dotnet#1085
Context: dotnet/runtime#82121

Some Java objects are *big*, e.g. [`Bitmap`][0] instances, but as
far as MonoVM is concerned, the `Bitmap` instances are *tiny*: a
few pointers, and that's it.  MonoVM doesn't know that it could be
referencing several MB of data in the Java VM.

MonoVM is gaining support for [`GC.AddMemoryPressure()`][1] and
[`GC.RemoveMemoryPressure()`][2], which potentially allows for the
parent of all cross-VM GC integrations: using the `GC` methods to
inform MonoVM of how much non-managed memory has been allocated.
This could allow MonoVM to collect more frequently when total process
memory is low.

How should we call `GC.AddMemoryPressure()` and
`GC.RemoveMemoryPressure()`?

`GC.RemoveMemoryPressure()` is straightforward: a class can override
`Java.Lang.Object.Dispose(bool)`.

`GC.AddMemoryPressure()` is the problem: where and how can it be
called from a class binding?  This is trickier, as there was no way
to have custom code called by the bound type.  Instead, various
forms of "hacky workarounds" are often employed, e.g. copying
`generator`-emitted content into a `partial` class, then using
`metadata` to *prevent* `generator` from binding those members.
It's all around fugly.

Fortunately C# has a solution: [`partial` methods][3]!

Add support for a `peerConstructorPartialMethod` metadata entry,
applicable to `<class/>` elements, which contains the name of a
`partial` method to both declare and invoke from the "peer constructor":

	<attr
	    path="//class[@name='Bitmap']"
	    name="peerConstructorPartialMethod"
	>_OnBitmapCreated</attr>

This will alter our existing "peer constructor" generation code, a'la:

	partial class Bitmap : Java.Lang.Object {
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	    }
	}

to instead become:

	partial class Bitmap : Java.Lang.Object {
	    partial void _OnBitmapCreated ();
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	        _OnBitmapCreated ();
	    }
	}

This allows a hand-written `partial class Bitmap` to do:

        // Hand-written code
        partial class Bitmap {
            int _memoryPressure;

            partial void _OnBitmapCreated ()
            {
                _memoryPressure = ByteCount;
                GC.AddMemoryPressure (_memoryPressure);
            }

            protected override void Dispose (bool disposing)
            {
                if (_memoryPressure != 0) {
                    GC.RemoveMemoryPressure (_memoryPressure);
                    _memoryPressure = 0;
                }
            }
        }

TODO: "extend" this for `<method/>`s as well?

[0]: https://developer.android.com/reference/android/graphics/Bitmap
[1]: https://learn.microsoft.com/dotnet/api/system.gc.addmemorypressure?view=net-7.0
[2]: https://learn.microsoft.com/dotnet/api/system.gc.removememorypressure?view=net-7.0
[3]: https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/partial-method
jonpryor added a commit that referenced this issue Mar 30, 2023
Context: #1085
Context: dotnet/runtime#82121

Some Java objects are *big*, e.g. [`Bitmap`][0] instances, but as
far as MonoVM is concerned, the `Bitmap` instances are *tiny*: a
few pointers, and that's it.  MonoVM doesn't know that it could be
referencing several MB of data in the Java VM.

MonoVM is gaining support for [`GC.AddMemoryPressure()`][1] and
[`GC.RemoveMemoryPressure()`][2], which potentially allows for the
parent of all cross-VM GC integrations: using the `GC` methods to
inform MonoVM of how much non-managed memory has been allocated.
This could allow MonoVM to collect more frequently when total process
memory is low.

How should we call `GC.AddMemoryPressure()` and
`GC.RemoveMemoryPressure()`?

`GC.RemoveMemoryPressure()` is straightforward: a class can override
`Java.Lang.Object.Dispose(bool)`.

`GC.AddMemoryPressure()` is the problem: where and how can it be
called from a class binding?  This is trickier, as there was no way
to have custom code called by the bound type.  Instead, various
forms of "hacky workarounds" are often employed, e.g. copying
`generator`-emitted content into a `partial` class, then using
`metadata` to *prevent* `generator` from binding those members.
It's all around fugly.

Fortunately C# has a solution: [`partial` methods][3]!

Add support for a `peerConstructorPartialMethod` metadata entry,
applicable to `<class/>` elements, which contains the name of a
`partial` method to both declare and invoke from the "peer constructor":

	<attr
	    path="//class[@name='Bitmap']"
	    name="peerConstructorPartialMethod"
	>_OnBitmapCreated</attr>

This will alter our existing "peer constructor" generation code, a'la:

	partial class Bitmap : Java.Lang.Object {
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	    }
	}

to instead become:

	partial class Bitmap : Java.Lang.Object {
	    partial void _OnBitmapCreated ();
	    internal Bitmap (IntPtr h, JniHandleOwnership t) : base (h, t)
	    {
	        _OnBitmapCreated ();
	    }
	}

This allows a hand-written `partial class Bitmap` to do:

        // Hand-written code
        partial class Bitmap {
            int _memoryPressure;

            partial void _OnBitmapCreated ()
            {
                _memoryPressure = ByteCount;
                GC.AddMemoryPressure (_memoryPressure);
            }

            protected override void Dispose (bool disposing)
            {
                if (_memoryPressure != 0) {
                    GC.RemoveMemoryPressure (_memoryPressure);
                    _memoryPressure = 0;
                }
            }
        }

TODO: "extend" this for `<method/>`s as well?

[0]: https://developer.android.com/reference/android/graphics/Bitmap
[1]: https://learn.microsoft.com/dotnet/api/system.gc.addmemorypressure?view=net-7.0
[2]: https://learn.microsoft.com/dotnet/api/system.gc.removememorypressure?view=net-7.0
[3]: https://learn.microsoft.com/dotnet/csharp/language-reference/keywords/partial-method
@jpobst jpobst added the generator Issues binding a Java library (generator, class-parse, etc.) label Apr 11, 2023
@jpobst
Copy link
Contributor

jpobst commented Aug 21, 2023

Implemented in #1087.

@jpobst jpobst closed this as completed Aug 21, 2023
@github-actions github-actions bot locked and limited conversation to collaborators Apr 12, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
generator Issues binding a Java library (generator, class-parse, etc.) proposal Issue raised for discussion, we do not even know if the change would be desirable yet
Projects
None yet
Development

No branches or pull requests

2 participants