Tuesday, March 23, 2010

Improving performance of DataBinding, patience is gold

I came across a performance problem lately. I have a WPF Smart Client application connected to a Dynamics CRM back end. At some point in my application I have add a lot of lines in a data table held by an ObservableCollection class. The problem was each time I add a line into the collection a CollectionChanged event was raised and that triggered the update of some other fields, approximately 10 in my case.
So when was doing batch update the screen freezes for way too much time. I found out the problem was the cascading effect of recalculating all the dependent fields every time I add a ne line in the collection, thanks to the new Visual Studio 2010 performance analysis tools.
In this scenario, I’m only interested to get the totals right when I do the last insert, all other intermediate values are useless. I didn’t want to play with the data binding itself. I could have unbound the total calculation and have it rebinded at the end but I didn’t. Instead I chose to delay the calculation with some kind of sliding expiration timer. Such a timer, set with a timeout of 10 ms, would be reset every time a new add would be made, so if I add a bunch of line in batch odds that the delay between two line add be less than 10 ms is good. In fact it is so good that because it’s almost always true the calculation happens only once at the end.
So there is this magic DelayRun class with its DelayController:
using System;
using System.Collections.Generic;
using System.Threading;

namespace Infrastructure
{
    public interface IDelayRun
    {
        void Reset();
        void Start(int ms);
        event EventHandler<DelayRunEventArgs> EventCompleted;
    }

    public class DelayRun<T> : IDisposable, IDelayRun
    {
        private static readonly ManualResetEvent _resetEvent = new ManualResetEvent(true);
        private readonly Action<T> _action;
        private readonly T _target;
        private int _delay;
        private volatile Timer _timer;

        private DelayRun(T target, Action<T> action)
        {
            _action = action;
            _target = target;
        }

        #region IDelayRun Members

        public void Start(int ms)
        {
            if (ms == 0)
                throw new ArgumentOutOfRangeException("ms", ms, "ms should be grater than 0.");

            if (_delay != 0)
                throw new InvalidOperationException("Can't start, already started.");

            _delay = ms;
            _timer = new Timer(Execute, this, _delay, Timeout.Infinite);
        }

        public void Reset()
        {
            lock (_timer)
            {
                if (_timer == null)
                    throw new InvalidOperationException("Can't reset the timer, already completed.");

                _timer.Change(_delay, Timeout.Infinite);
            }
        }

        #endregion

        public static DelayRun<T> CreateNew(T target, Action<T> action)
        {
            return new DelayRun<T>(target, action);
        }

        public static DelayRun<T> StartNew(T target, Action<T> action, int ms)
        {
            DelayRun<T> delayRun = CreateNew(target, action);
            delayRun.Start(ms);
            return delayRun;
        }

        public void Execute(object state)
        {
            _resetEvent.WaitOne();
            lock (_timer)
            {
                _timer.Change(Timeout.Infinite, Timeout.Infinite);
            }

            _action.Invoke(_target);
            _resetEvent.Set();
        }

        #region Event Handling

        public event EventHandler<DelayRunEventArgs> EventCompleted;

        public void InvokeEventCompleted(DelayRunEventArgs e)
        {
            EventHandler<DelayRunEventArgs> handler = EventCompleted;
            if (handler != null) handler(this, e);
        }

        #endregion

        #region IDisposable

        public void Dispose()
        {
            lock (_timer)
            {
                if (_timer != null)
                    _timer.Dispose();
            }
        }

        #endregion
    }

    public class DelayRunEventArgs : EventArgs
    {
        public DelayRunEventArgs(IDelayRun delayAction)
        {
            DelayAction = delayAction;
        }

        public IDelayRun DelayAction { get; set; }
    }

    public static class DelayController
    {
        private static readonly IDictionary<string, IDelayRun> _actions = new Dictionary<string, IDelayRun>();

        public static void Add<T>(string key, T target, Action<T> action, int ms)
        {
            if (_actions.ContainsKey(key))
                _actions[key].Reset();
            else
            {
                DelayRun<T> delayRun = DelayRun<T>.CreateNew(target, _ => {});
                _actions[key] = delayRun;
                string localKey = key;
                delayRun.EventCompleted += (sender, args) =>
                {
                    _actions.Remove(localKey);
                    action.BeginInvoke(target, ar => ar.AsyncWaitHandle.WaitOne(), delayRun);
                };
                delayRun.Start(ms);
            }
        }
    }
}
To use this class all you need to do is call the DelayController. The DelayController will handle the creation and the reset process of the DelayRun class. It can handle many delay class at once if they have different key value. Each time the controller Add method is called it either create a new instance of a DelayRun Class of reset the running one.
Here is how you can call it:
DelayController.Add("ValuesOnCollectionChanged", this, m => Recalc(m), 10);
This call will delay the Recalc method call for 10 ms. In the mean time if your program add other values to recalc you can recall this line and the delay will be reset for another 10 ms and so on.
Tell me if this can be helpful in your own projects.

2 comments:

Anonymous said...

Good evenin Eric De C#

Thanks a lot for this great idea and your source code.

I tried to use it to delay call's to WPF's UpdateSource (realted to UpdateSourceTrigger) and I have noticed, that my action was never called. Then, I noticed, that your function
InvokeEventCompleted()
is never called, too.

I hope that you know where InvokeEventCompleted() should be called as I am not sure where to put it...

Thanks in advance,
kind regards,
Thomas

Anonymous said...

Good evening Eric De C#

I just wrote a question about the missing call to InvokeEventCompleted()

I think the problem is in Execute:

public void Execute(object state) {
  _resetEvent.WaitOne();
  lock (_timer) {
    _timer.Change(
      Timeout.Infinite,
      Timeout.Infinite);
  }

Original, but probably wrong:
    //_action.Invoke(_target);
New and probably OK:
    InvokeEventCompleted(null);

    _resetEvent.Set();
}

What do you think about this fix?

Thanks in advance, kind regards,
Thomas