Friday, September 7, 2007

Reflection: a Cautionary Tale

Back in the day, I needed a mechanism to execute a method for a class for each new subclass of that type. Class constructors were not sufficient, because they are only run once per type, not once for every subclass of that type.

I decided to annotate the init method with an attribute and use reflection to lookup the method. When we register a new GType, we scan the class hierarchy for private static methods with a GLib.ClassInitializerAttribute and invoke any that exist. We use a similar mechanism to hook overridden virtual methods into the GObject class vtables for signal default handlers using the GLib.DefaultSignalHandlerAttribute.

This strategy works, but it turns out to be pretty suboptimal, especially from a memory usage standpoint. In order to find the methods with [ClassInitializer] defined, we had to load all the private static methods in the type hierarchy, and then iterate over them to look for the attribute. This caused a pretty substantial memory spike for types like Gtk.Widget which have a ton of static method delegate implementations for signal marshaling.

The root of the error was in not recognizing that the Class init problem is a one-to-one relationship of class to method, where the signal delegate problem I borrowed from is a one-to-many scenario. There is a [DefaultSignalHandler] method for each signal defined by a class.

To improve the mechanism, I added a GLib.TypeInitializerAttribute which takes a type and method name argument. With this attribute applied to a type declaration, we can lookup the specific methods by name and avoid loading all the static methods in the entire class hierarchy. This feature is now committed to trunk and the branch svn.

Since we have released the GLib.ClassInitializerAttribute as public API in a stable release, I couldn't just remove the reflection for it, even though it is unlikely that anyone out there really discovered the feature and is using it. In order to make it possible for subclass authors to avoid the reflection step, I also added the GLib.IgnoreClassInitializersAttribute which can be applied to any assemblies which contain GLib.Object subclass declarations. Svn trunk and branch add this attribute to all the gtk-sharp and gnome-sharp assemblies, so that all the types defined by the packages avoid this reflection overhead. The internal usage of [ClassInitializer] has been ported to [TypeInitializer] as well.

Thanks to Lluis for identifying the issue and Paolo for his usual insightful feedback in alternative approaches to the functionality. Lluis also helped refine the implementation of the [ClassInitializer] back-compat mechanism. My understanding is that the recent changes reduced MonoDevelop startup memory usage by about a MB.