Description
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 ofpartial
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?