Improving Sitecore Package Installation Performance

Occasionally one finds Sitecore packages taking a long time to run . . . like an hour or longer . . . there are some standard practices like disabling indexing or limiting the versions of items in your packages (most update packages don’t need previous versions of content items, for example).  There are some operations in the Sitecore packaging process that have been recently improved and there’s one in particular worth examining: the SqlLinkDatabase.UpdateItemVersionLinks method.

The method in question appears as follows:

protected override void UpdateItemVersionLinks(Item item, ItemLink[] links)
{
    Action action = null;
    lock (this.locks.GetLock(item.ID))
    {
        if (action == null)
        {
            action = delegate {
                using (DataProviderTransaction transaction = this.DataApi.CreateTransaction())
                {
                    this.RemoveItemVersionLinks(item);
                    for (int j = 0; j < links.Length; j++)
                    {
                        ItemLink link = links[j];
                        if (!link.SourceItemID.IsNull && ((((link.SourceItemLanguage == Language.Invariant) && (link.SourceItemVersion == Version.Latest)) || ((link.SourceItemLanguage == item.Language) && (link.SourceItemVersion == Version.Latest))) || ((link.SourceItemLanguage == item.Language) && (link.SourceItemVersion == item.Version))))
                        {
                            this.AddLink(item, link);
                            DataCount.LinksDataUpdated.Increment(1L);
                        }
                    }
                    transaction.Complete();
                }
            };
        }
        Factory.GetRetryer().ExecuteNoResult(action);
    }
}

This can be a package installation performance problem since in order to update a few dependencies, the code is removing all links to the item in the Sitecore Link Database (RemoveItemVersionLinks(item)) and then adding them all back in again — it’s not a surgical update, it’s a brute force approach.

There is a better way of managing the link logic, however, and in the latest releases of Sitecore it’s folded in as part of the Sitecore.Kernel (I believe the updates in the last couple weeks all have it — but I haven’t check all of them).  Here’s the alternative that only operates on the changed links; see the outdatedLinks list variable below:

  protected override void UpdateItemVersionLinks(Item item, ItemLink[] links)
{
    Action action = null;
    List<ItemLink> versionLinks = this.GetVersionLinks(item, new object[] { "itemID", item.ID.ToGuid(), "sourceLanguage", item.Language.ToString(), "sourceVersion", item.Version.Number, "invariantLanguage", Language.Invariant.ToString(), "latestVersion", Version.Latest.Number, "database", this.GetString(item.Database.Name, 50) });
    List<ItemLink> outdatedLinks = versionLinks.Except<ItemLink>(links, new ItemLinkEqualityComparer()).ToList<ItemLink>();
    IEnumerable<ItemLink> newLinks = links.Except<ItemLink>(versionLinks, new ItemLinkEqualityComparer());
    LockSet set = (LockSet) lockSetField.GetValue(this);
    lock (set.GetLock(item.ID))
    {
        if (action == null)
        {
            action = delegate {
                using (DataProviderTransaction transaction = this.DataApi.CreateTransaction())
                {
                    try
                    {
                        this.RemoveOutdatedItemVersionLinks(outdatedLinks);
                    }
                    catch (InvalidCastException exception)
                    {
                        Log.Error("Failed to cast ItemLink collection to custom ItemLinkWithId collection.", exception, this);
                        transaction.Dispose();
                        return;
                    }
                    foreach (ItemLink link in newLinks)
                    {
                        ItemLink link2 = link;
                        if (!link2.SourceItemID.IsNull && ((((link2.SourceItemLanguage == Language.Invariant) && (link2.SourceItemVersion == Version.Latest)) || ((link2.SourceItemLanguage == item.Language) && (link2.SourceItemVersion == Version.Latest))) || ((link2.SourceItemLanguage == item.Language) && (link2.SourceItemVersion == item.Version))))
                        {
                            this.AddLink(item, link2);
                            LinkCounters.DataUpdated.Increment();
                        }
                    }
                    transaction.Complete();
                }
            };
        }
        Factory.GetRetryer().ExecuteNoResult(action);
    }
}

If you needed to, you’d swap in your improved logic via a configuration patch like:

<?xml version="1.0" encoding="UTF-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
   <sitecore>
      <LinkDatabase type="Sitecore.Data.$(database).$(database)LinkDatabase, Sitecore.Kernel">
         <patch:attribute name="type">CustomAssembly.Data.SqlServer.SqlServerLinkDatabase, CustomAssembly</patch:attribute>
      </LinkDatabase>
   </sitecore>
</configuration>