On Demand Modules
In K-3D, most functionality is implemented as plugins. Plugins are stored in shared libraries called modules, and dynamically loaded at runtime. Significant effort has been expended to develop the infrastructure to accomplish this. Modules are located and loaded at program startup time, so that the complete set of available plugins is always in memory and ready for use. As each module is loaded, it registers a set of plugin factories with the application. Each plugin factory can be queried for metadata describing the plugin type it represents, and is used to create instances of that plugin type. The set of available plugins and their capabilities is generated from the set of registered plugin factories. Great care has been taken to ensure that application code does not assume the availability of specific plugins or capabilities, and fails gracefully in their absence.
Although this system worked well for K-3D over many years, some drawbacks became obvious as the program grew. As plugin modules increased in size and number, application startup time increased. This was compounded by the growing number of plugin modules that had external dependencies on "third party" libraries. These dependencies were also loaded at startup, increasing the size of the application working-set and slowing startup further. The expanding set of runtime libraries became an even greater liability during debugging, when the debugger must load both dependent libraries and their debugging symbols before debugging can start. These issues finally reached a point where a system with 512Mb real and 512Mb virtual memory could not load a K-3D core dump into gdb.
The set of plugins actually used by a given document is usually a very narrow subset of all available plugins. For example, it would be very rare to have a document that contained readers and writers for every external file format supported by K-3D. Many features such as NURBS and image processing are not heavily used at the current time. On Demand Modules take advantage of this by loading a set of module proxies at startup, instead of loading the modules themselves. Each module proxy is an XML file that describes all of the plugin factories for a given module:
<?xml version="1.0" ?> <k3dml package="k3d" version="0.5.0.37" host="i686-pc-mingw32"> <module name="libk3daqsis.dll" class="00000000 00000000 00000000 00000000"> <plugins> <plugin name="AqsisSurfaceShaderLayer" class="261e244e d82947ba 9e56a9af 03d4cc0f" quality="experimental" type="document"> <short_description>Encapsulates an Aqsis surface shader layer</short_description> <categories> <category>Aqsis</category> </categories> <interfaces> <interface>k3d::ri::isurface_shader</interface> <interface>k3d::aqsis::isurface_layer</interface> </interfaces> </plugin> <plugin name="AqsisDisplacementShaderLayer" class="1802dce3 71004329 80cbdf0e bc7f142c" quality="experimental" type="document"> <short_description>Encapsulates an Aqsis displacement shader layer</short_description> <categories> <category>Aqsis</category> </categories> <interfaces> <interface>k3d::ri::idisplacement_shader</interface> <interface>k3d::aqsis::idisplacement_layer</interface> </interfaces> </plugin> <plugin name="Teapot" class="b761f071 f7ed4297 9449028d 2f6236f0" quality="stable" type="document"> <short_description>Renders a teapot primitive in render engines that support one (Aqsis!)</short_description> <categories> <category>Aqsis</category> </categories> <interfaces> <interface>k3d::itransform_source</interface> <interface>k3d::itransform_sink</interface> </interfaces> </plugin> </plugins> </module> </k3dml>
Module proxies are automatically generated by the build system using the k3d-make-module-proxy executable.
Each module proxy has the same path and name as the corresponding module, with ".proxy" appended, so a module "k3d-foo.module" will have a proxy "k3d-foo.module.proxy".
Before loading a module at runtime, K-3D checks for the existence of a corresponding proxy file. If the proxy file exists, the application uses it to register a set of plugin factory proxies, which use the serialized XML data to mimic the behavior of the loaded module's plugin factories. When a caller requests metadata from a plugin factory proxy, it receives the same results it would have received from the "real" plugin factory. When a caller requests instantiation of a plugin, the plugin factory proxy checks to see if the corresponding module has been loaded or not. If not, the module is loaded and the "real" plugin factory is used to complete the request.
Thus, only those plugins that get used have their plugin modules loaded into the application working set - "you don't pay for what you don't use". The proxying is transparent to calling code, which does not need to be modified to benefit from the new behavior.
If a proxy isn't available for a given module, the module is immediately loaded, as has been done in the past. This is useful for a narrow-class of special modules whose capabilities are determined at runtime instead of compile-time.
- Faster application startup.
- More memory available for your work.
- Faster debugging for developers.
- There may be a noticable pause the first time a given plugin is used.
- There is potential for a binary module and the corresponding proxy to get out-of-sync, causing "missing" plugins, missing functionality, or segfaults. This risk is minimized by the fact that proxies are automatically generated by the build process, are automatically installed, and automatically packaged.
For a given plugin type, developers should not assume that the plugin factory returned from k3d::application::plugins() is the same object as the factory returned by k3d::inode::factory(). To compare factory instances for equivalence, compare the results of their class_id() methods.
- As further experience is accumulated with On Demand Modules, we will likely want to "tune" the set of modules and proxies to further improve our management of the application working set.
- As an example, we might want to "split" an existing module into two-or-more smaller modules, so oft-used plugins don't cause rarely-used plugins to be loaded.
- Conversely, it may make sense to merge a set of ubiquitously-used plugins into a single module.
- For some ubiquitously-used modules it may make sense to avoid proxying altogether. As an example, proxying is disabled when loading user-interface modules, since the user-interface module is always loaded, or the program exits.
- The current design still assumes that the set of available plugins remains static once application startup is complete. Allowing plugin modules to be loaded or unloaded while the application is running may be worth considering.