Workflow Content Handler degrades performance of content creation and imports.

Topics: Core, Troubleshooting
Jun 7, 2014 at 2:35 PM
I have a custom module that defines its own content type, a large number of content items of this type in the system (1000s) and the module provides an operation to replicate these content items in bulk (100s at a time).

In order to do this, the module instantiates new content items, sets their properties then calls ContentManager.Create(contentItem) iteratively.

I recently observed (as the number of items being replicated at a time increased) that this process was quite slow. It was taking up to multiple minutes to replicate about 100s of content items. Upon investigating further I found the majority of this time was being spent within the DefaultContentManager.Create method when it called the "Created" and "Published" handlers.

I tracked theproblematic (slow) handler down to the workflows module. WorkFlowContentHandler calls WorkflowManager.TriggerEvent for each content item that is created, published, removed, etc. The WorkflowManager.TriggerEvent method queries the activityRepository and awaitingActivityRepository for every content item that is created/modified/deleted/etc. It is these repository searches that are slow. On my server each call to WorkflowManager.TriggerEvent is taking ~200ms in time just to hit these repos and discover there are actually no workflows that are interested in these content types. When performing a batch operation this time adds up significantly.

I believe this may also be a signficant factor effecting performance of imports of large numbers of content items.

We could look at improving the performance of the activity repo queries in WorkflowManager.TriggerEvent, but I'm not sure that there's much more we could do there?

Alternatively, we could add some kind of check in the WorkflowContentHandler that ignores (doesn't call WorkflowManager.TriggerEvent) for certain content types. Maybe some kind of call to an interface (pasing the content item) that module developers could then implement to prevent the trigger?

Any thoughts on best approach?

FYI. In my testing, I added a quick check at the start of WorkflowManager.TriggerEvent to return immediately if target.ContentItem.ContentType == "MyCustomContentType" and the operation that was taking minutes to copy 100s of content items reduced to about 10 seconds.

I have seen discussions previously around performance when importing large numbers of content items through the import/export module. I wonder if anyone has tried a large import with the Workflows module disabled vs enabled?
Jun 10, 2014 at 1:10 AM
I've added a check at the start of the WorkflowManager.TriggerEvent method that drops out early (before accessing the db) if the trigger is for a content activity (eg. ContentPublished, ContentCreated, etc) that would not apply. While this has no performance impact on single content item changes, it significantly improves performance when performing batch updates (eg. during import).

This code is added near the top of WorkflowManager.TriggerEvents (after the call to retrieve the activity by name and check it's not null)...
if (!PrecheckContentEvent(activity, target, tokens)) {
And this is the private method....
        private bool PrecheckContentEvent(IActivity activity, IContent target, IDictionary<string, object> tokens) {

            var contentEvents = new List<string>{ "ContentCreated", "ContentPublished", "ContentRemoved", "ContentVersioned", "ContentUpdated" };

            if (contentEvents.Contains(activity.Name) && target != null) {

                // to improve performance when affecting large numbers of content items (eg. import) we
                // store information about content activities to a local (per request) dictionary. 

                var contentActivityKey = activity.Name + "." + target.ContentItem.TypeDefinition.Name;

                if (!_contentActivityCount.ContainsKey(contentActivityKey)) {

                    _contentActivityCount[contentActivityKey] = 0;
                    var enabledWorkflows = new List<ActivityRecord>();

                    // look for enabled workflow definitions
                        x => x.Name == activity.Name && x.WorkflowDefinitionRecord.Enabled

                    foreach (var activityRecord in enabledWorkflows) {

                        var workflowContext = new WorkflowContext {
                            Content = target,
                            Tokens = tokens

                        var workflowRecord = new WorkflowRecord {
                            WorkflowDefinitionRecord = activityRecord.WorkflowDefinitionRecord,
                            State = "{}",
                            ContentItemRecord = workflowContext.Content == null || workflowContext.Content.ContentItem == null
                                    ? null
                                    : workflowContext.Content.ContentItem.Record

                        workflowContext.Record = workflowRecord;

                        var activityContext = CreateActivityContext(activityRecord, tokens);

                        // check the condition
                        try {
                            if (activity.CanExecute(workflowContext, activityContext)) {
                        catch (Exception e) {
                            Logger.Error("Error while evaluating an activity condition on {0}: {1}", activity.Name, e.ToString());



                return _contentActivityCount[contentActivityKey] > 0;

            return true;
You'll also need this private field added to the class....
private Dictionary<string, int> _contentActivityCount = new Dictionary<string, int>();
Is this something (or something similar) that would be worthwhile raising as a bug to have added to the workflow module?