donderdag 10 maart 2011

Turbo Castle ActiveRecord loading

Some users were complaining of long startup times of a piece of software.
The software used Castle ActiveRecord (a layer above NHibernate) as ORM, connected to 4 different databases (Oracle and SQL Server), and consulted more than 100 tables. The software was a FAT client architecture. On some slower computers it took Activerecord over 20 seconds to load! A solution was needed.

After a hint from the NHibernate community a solution was found (serialize the nhibernate configuration). After a little coding we could reduce the loading time to 2 seconds (THAT'S 10 TIMES FASTER!!!).
This is how it is done:

When running the code from VisualStudio we load ActiveRecord the normal way.

var normalActiveRecordLoading = Process.GetCurrentProcess().ProcessName.EndsWith(".vshost") && Debugger.IsAttached;
if (normalActiveRecordLoading)
{
     var configSource = new XmlConfigurationSource("../appconfig.xml");
     var activeRecordAssemblies = new List { };
     var asm = new Assembly[activeRecordAssemblies.Count];
     for (int i = 0; i < activeRecordAssemblies.Count; i++)
         asm[i] = Assembly.Load(activeRecordAssemblies[i]);
     var additionalActiveRecordTypes = new List() { typeof(Blog), typeof(Post), typeof(User) };
     ActiveRecordStarter.Initialize(asm, configSource, additionalActiveRecordTypes == null ? Type.EmptyTypes : additionalActiveRecordTypes.ToArray());
}

After initialization we can now binary serialize the configuration. But wait, what if we have multiple databases? Different NHibernate configurations will be generated per database. To know the different NHibernate configurations in ActiveRecord we need to register the OnRootTypeRegistered event per SessionFactoryHolder and store all the rootTypes.

var rootTypes = new List()
ActiveRecordStarter.SessionFactoryHolderCreated += (holder) =>
{
     holder.OnRootTypeRegistered += (sender, rootType) =>
     {
          rootTypes.Add(rootType);
     };
};

Once we know the rootTypes we can recieve the configuration per rootType and binary serialize the configuration and store them in the Resource folder.

//Store the configurations in the Resouce folder!!!!!!!!!!!!!!!!!!!!!!!
var holder = ActiveRecordMediator.GetSessionFactoryHolder();
var path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
var lastIndex = 0;
if ((lastIndex = path.LastIndexOf(@"\bin\debug")) > 0) //c#
     path = path.TrimToMaxSize(lastIndex);
else if ((lastIndex = path.LastIndexOf(@"\bin\release")) > 0) //c#
     path = path.TrimToMaxSize(lastIndex);
else
     path = path.TrimToMaxSize(path.LastIndexOf(@"\bin")); //vb.net

foreach (var rootType in rootTypes)
{
     var configuration = holder.GetConfiguration(rootType);
     var resource = string.Format("{0}{1}Resources{1}Configuration_{2}, {3}.serialized", path, Path.DirectorySeparatorChar, rootType.FullName, rootType.Assembly.GetName().Name);
     File.Delete(resource);
     using (var stream = File.OpenWrite(resource))
         serializer.Serialize(stream, configuration);
}

Now you need to add the generated resources to your visual studio as embedded resource.



Now you can do TURBO ACTIVERECORD LOADING when not running from Visual Studio. Just by deserializing the embedded resources and plugging them into ActiveRecord.

var serializedNHibernateConfigurations = new Dictionary() {
 {Type.GetType("Castle.ActiveRecord.ActiveRecordBase, Castle.ActiveRecord"), 
  entryAssembly.GetManifestResourceStream(entryAssembly.GetName().Name + ".Resources.Configuration_Castle.ActiveRecord.ActiveRecordBase, Castle.ActiveRecord.serialized")}
 };

if (!normalActiveRecordLoading)
{ //TURBO MODE
     var holder = new SessionFactoryHolder();

     foreach (var item in serializedNHibernateConfigurations)
     {
          var cfg = serializer.Deserialize(item.Value) as Configuration;
          holder.Register(item.Key, cfg);
     }

     var method = typeof(ActiveRecordStarter).GetMethod("CreateThreadScopeInfoImplementation", BindingFlags.Static | BindingFlags.NonPublic);
     holder.ThreadScopeInfo = method.Invoke(null, new object[] { configSource }) as IThreadScopeInfo;
     var field = typeof(ActiveRecordStarter).GetField("configSource", BindingFlags.Static | BindingFlags.NonPublic);
     field.SetValue(null, configSource);
     field = typeof(ActiveRecordBase).GetField("holder", BindingFlags.Static | BindingFlags.NonPublic);
     field.SetValue(null, holder);
}

Some caveats:

A modification is needed to the Activerecord source in the ActiveRecordBase class The check for the 'GetModel(type) == null' cannot be done because the ActiveRecordModel isn't generated anymore.

[Serializable]
 public abstract class ActiveRecordBase : ActiveRecordHooksBase
 {
  /// 
  /// The global holder for the session factories.
  /// 
  protected internal static ISessionFactoryHolder holder;

  #region internal static

  internal static void EnsureInitialized(Type type)
  {
   if (holder == null)
   {
    String message = String.Format("An ActiveRecord class ({0}) was used but the framework seems not " +
              "properly initialized. Did you forget about ActiveRecordStarter.Initialize() ?",
              type.FullName);
    throw new ActiveRecordException(message);
   }
   if (!typeof(ActiveRecordBase).IsAssignableFrom(type)/*type != typeof(ActiveRecordBase) && GetModel(type) == null*/)
   {
    String message = String.Format("You have accessed an ActiveRecord class that wasn't properly initialized. " +
              "There are two possible explanations: that the call to ActiveRecordStarter.Initialize() didn't include {0} class, or that {0} class is not decorated with the [ActiveRecord] attribute.",
              type.FullName);
    throw new ActiveRecordException(message);
   }
  }


Also the Count() and DeleteAll() methods cannot be used for the same reason as above (those 2 methods are also using the ActiveRecordModel).

Download source