diff --git a/runtime/coreclr-bridge.m b/runtime/coreclr-bridge.m index a8d6ee68aa..01bb19a642 100644 --- a/runtime/coreclr-bridge.m +++ b/runtime/coreclr-bridge.m @@ -11,6 +11,7 @@ #include #include "product.h" +#include "runtime-internal.h" #include "xamarin/xamarin.h" #include "xamarin/coreclr-bridge.h" @@ -19,6 +20,132 @@ unsigned int coreclr_domainId = 0; void *coreclr_handle = NULL; +#if defined (TRACK_MONOOBJECTS) + +// To enable tracking of MonoObject* instances, uncomment the TRACK_MONOOBJECTS define in: +// * runtime/runtime-internal.h +// * src/ObjCRuntime/Runtime.CoreCLR.cs +// Both defines must be uncommented for tracking to work. Once enabled, you can opt-in to +// capturing the stack trace of when the MonoObject* was created, by setting the +// MONOOBJECT_TRACKING_WITH_STACKTRACES environment variable. + +#include +#include + +static int _Atomic monoobject_created = 0; +static int _Atomic monoobject_destroyed = 0; +static CFMutableDictionaryRef monoobject_dict = NULL; +static pthread_mutex_t monoobject_lock = PTHREAD_MUTEX_INITIALIZER; + +struct monoobject_tracked_entry { + char *managed; + void *addresses [128]; + int frames; + char *native; +}; + +static char * +get_stacktrace (void **addresses, int frames) +{ + // get the symbols for the addresses + char** strs = backtrace_symbols (addresses, frames); + + // compute the total length of all the symbols, adding 1 for every line (for the newline) + size_t length = 0; + int i; + for (i = 0; i < frames; i++) + length += strlen (strs [i]) + 1; + length++; + + // format the symbols as one long string with newlines + char *rv = (char *) calloc (1, length); + char *buffer = rv; + size_t left = length; + for (i = 0; i < frames; i++) { + snprintf (buffer, left, "%s\n", strs [i]); + size_t slen = strlen (strs [i]) + 1; + left -= slen; + buffer += slen; + } + free (strs); + + return rv; +} + +void +xamarin_bridge_log_monoobject (MonoObject *mobj, const char *stacktrace) +{ + // add stack traces if we have them / they've been been requested + if (monoobject_dict != NULL) { + // create a new entry + struct monoobject_tracked_entry *value = (struct monoobject_tracked_entry *) calloc (1, sizeof (struct monoobject_tracked_entry)); + + value->managed = stacktrace ? xamarin_strdup_printf ("%s", stacktrace) : NULL; + value->frames = backtrace ((void **) &value->addresses, sizeof (value->addresses) / sizeof (&value->addresses [0])); + + // insert into our dictionary of monoobjects + pthread_mutex_lock (&monoobject_lock); + CFDictionarySetValue (monoobject_dict, mobj, value); + pthread_mutex_unlock (&monoobject_lock); + } + + atomic_fetch_add (&monoobject_created, 1); +} + +void +xamarin_bridge_dump_monoobjects () +{ + if (monoobject_dict != NULL) { + // dump the monoobject's that haven't been freed (max 10 entries). + pthread_mutex_lock (&monoobject_lock); + + // get the keys and values + unsigned int length = (unsigned int) CFDictionaryGetCount (monoobject_dict); + MonoObject** keys = (MonoObject **) calloc (1, sizeof (void*) * length); + char** values = (char **) calloc (1, sizeof (char*) * length); + CFDictionaryGetKeysAndValues (monoobject_dict, (const void **) keys, (const void **) values); + + // is there anything left in the dictionary? if so, show that + unsigned int items_to_show = length > 10 ? 10 : length; + if (items_to_show > 0) { + fprintf (stderr, "⚠️ There were %i MonoObjects created, %i MonoObjects freed, so %i were not freed.\n", (int) monoobject_created, (int) monoobject_destroyed, (int) (monoobject_created - monoobject_destroyed)); + fprintf (stderr, "Showing the first %i (of %i) MonoObjects:\n", items_to_show, length); + for (unsigned int i = 0; i < items_to_show; i++) { + MonoObject *obj = keys [i]; + struct monoobject_tracked_entry *value = (struct monoobject_tracked_entry *) values [i]; + fprintf (stderr, "Object %i/%i %p RC: %i\n", i + 1, (int) length, obj, (int) obj->reference_count); + if (value->managed && *value->managed) + fprintf (stderr, "\tManaged stack trace:\n%s\n", value->managed); + if (value->native == NULL && value->frames > 0) + value->native = get_stacktrace (value->addresses, value->frames); + if (value->native && *value->native) + fprintf (stderr, "\tNative stack trace:\n%s\n", value->native); + } + fprintf (stderr, "⚠️ There were %i MonoObjects created, %i MonoObjects freed, so %i were not freed.\n", (int) monoobject_created, (int) monoobject_destroyed, (int) (monoobject_created - monoobject_destroyed)); + } else { + fprintf (stderr, "✅ There were %i MonoObjects created, %i MonoObjects freed, so no leaked MonoObjects.\n", (int) monoobject_created, (int) monoobject_destroyed); + } + pthread_mutex_unlock (&monoobject_lock); + + free (keys); + free (values); + } else { + fprintf (stderr, "There were %i MonoObjects created, %i MonoObjects freed, so %i were not freed.\n", (int) monoobject_created, (int) monoobject_destroyed, (int) (monoobject_created - monoobject_destroyed)); + } +} + +static void +monoobject_dict_free_value (CFAllocatorRef allocator, const void *value) +{ + struct monoobject_tracked_entry* v = (struct monoobject_tracked_entry *) value; + xamarin_free (v->managed); + if (v->native) + free (v->native); + free (v); +} + +#endif // defined (TRACK_MONOOBJECTS) + void xamarin_bridge_setup () { @@ -27,6 +154,26 @@ xamarin_bridge_setup () void xamarin_bridge_initialize () { +#if defined (TRACK_MONOOBJECTS) + // Only capture the stack trace if requested explicitly, it has a very significant perf hit (monotouch-test is 3x slower). + const char *with_stacktraces = getenv ("MONOOBJECT_TRACKING_WITH_STACKTRACES"); + if (with_stacktraces && *with_stacktraces) { + // Create a dictionary to store the stack traces + CFDictionaryValueCallBacks value_callbacks = { 0 }; + value_callbacks.release = monoobject_dict_free_value; + monoobject_dict = CFDictionaryCreateMutable (kCFAllocatorDefault, 0, NULL, &value_callbacks); + + fprintf (stderr, "Stack traces enabled for MonoObject tracking.\n"); + } +#endif // defined (TRACK_MONOOBJECTS) +} + +void +xamarin_bridge_shutdown () +{ +#if defined (TRACK_MONOOBJECTS) + xamarin_bridge_dump_monoobjects (); +#endif } void @@ -158,6 +305,15 @@ xamarin_mono_object_release (MonoObject **mobj_ref) xamarin_free (mobj->struct_value); // allocated using Marshal.AllocHGlobal. xamarin_free (mobj); // allocated using Marshal.AllocHGlobal. + +#if defined (TRACK_MONOOBJECTS) + if (monoobject_dict != NULL) { + pthread_mutex_lock (&monoobject_lock); + CFDictionaryRemoveValue (monoobject_dict, mobj); + pthread_mutex_unlock (&monoobject_lock); + } + atomic_fetch_add (&monoobject_destroyed, 1); +#endif } *mobj_ref = NULL; diff --git a/runtime/monotouch-main.m b/runtime/monotouch-main.m index b4a1f39252..745dbb0350 100644 --- a/runtime/monotouch-main.m +++ b/runtime/monotouch-main.m @@ -484,5 +484,7 @@ xamarin_main (int argc, char *argv[], enum XamarinLaunchMode launch_mode) xamarin_mono_object_release (&assembly); + xamarin_bridge_shutdown (); + return rv; } diff --git a/runtime/monovm-bridge.m b/runtime/monovm-bridge.m index 46653b8f35..3ff3514983 100644 --- a/runtime/monovm-bridge.m +++ b/runtime/monovm-bridge.m @@ -89,6 +89,11 @@ xamarin_bridge_initialize () mono_install_assembly_preload_hook (xamarin_assembly_preload_hook, NULL); DEBUG_LAUNCH_TIME_PRINT ("\tJIT init time"); } + +void +xamarin_bridge_shutdown () +{ +} #endif // !LEGACY_XAMARIN_MAC static MonoClass * @@ -417,6 +422,19 @@ xamarin_mono_object_retain (MonoObject *mobj) // Nothing to do here } +#if defined (TRACK_MONOOBJECTS) +// This function is needed for the corresponding managed P/Invoke to not make +// the native linker fail due to an unresolved symbol. This method should +// never end up being called (it'll be linked away by the native linker if the +// managed linker removes the P/Invoke, and never called from managed code +// otherwise). +void +xamarin_bridge_log_monoobject (MonoObject *mobj, const char *stacktrace) +{ + xamarin_assertion_message ("%s is not available on MonoVM", __func__); +} +#endif // defined (TRACK_MONOOBJECTS) + #endif // DOTNET /* diff --git a/runtime/runtime-internal.h b/runtime/runtime-internal.h index 91017edccb..7fc2b645f9 100644 --- a/runtime/runtime-internal.h +++ b/runtime/runtime-internal.h @@ -25,6 +25,13 @@ #define DEBUG_LAUNCH_TIME_PRINT(...) #endif +// Uncomment the TRACK_MONOOBJECTS define to show a summary at process exit of +// the MonoObjects that were created, and if any were not freed. If there are +// leaked MonoObjects, a list of them will be printed. +// This has an equivalent variable in src/ObjCRuntime/Runtime.CoreCLR.cs, +// which must be set for tracking to work. +//#define TRACK_MONOOBJECTS + #ifdef __cplusplus extern "C" { #endif diff --git a/runtime/xamarin/runtime.h b/runtime/xamarin/runtime.h index 7d234f3642..a67df5d83e 100644 --- a/runtime/xamarin/runtime.h +++ b/runtime/xamarin/runtime.h @@ -201,6 +201,7 @@ uint32_t xamarin_find_protocol_wrapper_type (uint32_t token_ref); void xamarin_release_block_on_main_thread (void *obj); void xamarin_bridge_setup (); // this is called very early, before parsing the command line arguments void xamarin_bridge_initialize (); // this is called a bit later, after parsing the command line arguments. +void xamarin_bridge_shutdown (); // this is called just before returning from xamarin_main unsigned char * xamarin_load_aot_data (MonoAssembly *assembly, int size, gpointer user_data, void **out_handle); void xamarin_free_aot_data (MonoAssembly *assembly, int size, gpointer user_data, void *handle); MonoAssembly* xamarin_assembly_preload_hook (MonoAssemblyName *aname, char **assemblies_path, void* user_data); @@ -291,6 +292,7 @@ typedef id (*xamarin_get_handle_func) (MonoObject *info); MonoToggleRefStatus xamarin_gc_toggleref_callback (uint8_t flags, id handle, xamarin_get_handle_func get_handle, MonoObject *info); void xamarin_gc_event (MonoGCEvent event); +void xamarin_bridge_log_monoobject (MonoObject *obj, const char *stacktrace); /* * In MonoVM MonoObjects are tracked in memory/the stack directly by the GC, but that doesn't * work for CoreCLR, so we make it ref-counted. All code must use the functions below to retain/release diff --git a/src/ObjCRuntime/Runtime.CoreCLR.cs b/src/ObjCRuntime/Runtime.CoreCLR.cs index 48511862e9..2d97ada7db 100644 --- a/src/ObjCRuntime/Runtime.CoreCLR.cs +++ b/src/ObjCRuntime/Runtime.CoreCLR.cs @@ -6,6 +6,14 @@ // // Copyright 2021 Microsoft Corp. + +// Uncomment the TRACK_MONOOBJECTS define to show a summary at process exit of +// the MonoObjects that were created, and if any were not freed. If there are +// leaked MonoObjects, a list of them will be printed. +// This has an equivalent variable in runtime/runtime-internal.m +// which must be set for tracking to work. +//#define TRACK_MONOOBJECTS + #if NET && !COREBUILD using System; @@ -62,6 +70,10 @@ namespace ObjCRuntime { public MonoObject* Parameters; } +#if TRACK_MONOOBJECTS + static bool? track_monoobject_with_stacktraces; +#endif + // Comment out the attribute to get all printfs [System.Diagnostics.Conditional ("UNDEFINED")] static void log_coreclr (string message) @@ -259,6 +271,13 @@ namespace ObjCRuntime { log_coreclr ($"GetMonoObjectImpl ({obj.GetType ()}) => 0x{rv.ToString ("x")} => GCHandle=0x{handle.ToString ("x")}"); +#if TRACK_MONOOBJECTS + // Only capture the stack trace if requested explicitly, it has a very significant perf hit (monotouch-test is 3x slower). + if (!track_monoobject_with_stacktraces.HasValue) + track_monoobject_with_stacktraces = !string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("MONOOBJECT_TRACKING_WITH_STACKTRACES")); + xamarin_bridge_log_monoobject (rv, track_monoobject_with_stacktraces.Value ? Environment.StackTrace : null); +#endif + return rv; } @@ -278,6 +297,11 @@ namespace ObjCRuntime { } } +#if TRACK_MONOOBJECTS + [DllImport ("__Internal", CharSet = CharSet.Auto)] + static extern void xamarin_bridge_log_monoobject (IntPtr mono_object, string stack_trace); +#endif + static IntPtr MarshalStructure (T value) where T: struct { var rv = Marshal.AllocHGlobal (Marshal.SizeOf (typeof (T)));