Question

When using IProgress<T> to report progress, should it be

  • the responsibility of the code reporting progress to limit its progress reports to a frequency that is "reasonable", -or-
  • the responsibility of the specific implementation of IProgress<T> to be aware that the reporting of progress might be at a higher frequency than is reasonable for the way it will be presenting this progress.

The context of the question is I have some code which uses IProgress<T> to report progress, and it reports progress at a very high rate. I want to display progress with a UI progress bar. If I use the provided Progress<T> implementation (which posts progress to the UI SyncronizationContext), then it causes the UI to be unresponsive (i.e. there are so many messages sent to the message queue, that the user can't even click the "Cancel" button on the dialog).

So,

  • I could fix this by reporting less, but what if I had an IProgress<T> implementation that just wrote progress to a log file (and could handle the high reporting frequency). -or-
  • I could fix this by creating my own specific IProgress<T> implementation that limited how often I processed/reported progress. Presumably, this implementation would record the latest progress on a non-UI thread, and then (perhaps) the UI would updated based on a timer.
Was it helpful?

Solution

Write a decorator which throttles the calls. This way you separate the logic of throttling and the actual reporting, and you can use it for any other IProgress<T> implementation.

Use this decorator when you want to throttle the progress reporting. Simple wrap your progress reportor with instance of the class below.

I've left the throttling logic up to you. You can make it time-based, amount-of-calls-based or some other criteria.

public class ProgressThrottler<T>: IProgress<T> {
    public ProgressThrottler(IProgress<T> progress) {
        _progress = progress ?? throw new ArgumentNullException("progress");
    }

    private readonly IProgress<T> _progress;

    public void Report(T value) {
        // Throttles the amount of calls
        bool reportProgressAfterThrottling = ...;

        if (reportProgressAfterThrottling) {
            _progress.Report(value);
        }
    }
}

OTHER TIPS

Here is a custom Progress<T> implementation that enforces a minimum interval policy between consecutive progress reports. When a report message is emitted, it initiates a period of silence during which all subsequent report messages are dropped (ignored), except from the last one. The last report message of each period is buffered and emitted when the period ends, and then a new silent period is initiated etc. The duration of each silent period is configurable (dueTime argument).

public class ThrottledProgress<T> : Progress<T>
{
    private readonly TimeSpan _dueTime;
    private readonly object _locker = new object();
    private (T Value, bool HasValue) _current;
    private Task _task;

    public ThrottledProgress(Action<T> handler, TimeSpan dueTime) : base(handler)
    {
        if (dueTime < TimeSpan.Zero || dueTime.TotalMilliseconds > Int32.MaxValue)
            throw new ArgumentOutOfRangeException(nameof(dueTime));
        _dueTime = dueTime;
    }

    protected override void OnReport(T value)
    {
        lock (_locker)
        {
            if (_task == null)
            {
                base.OnReport(value);
                _task = Task.Run(async () =>
                {
                    while (true)
                    {
                        await Task.Delay(_dueTime);
                        lock (_locker)
                        {
                            if (_current.HasValue)
                            {
                                base.OnReport(_current.Value);
                                _current = (default, false);
                            }
                            else
                            {
                                _task = null;
                                break;
                            }
                        }
                    }
                });
            }
            else
            {
                _current = (value, true);
            }
        }
    }

    public void Flush()
    {
        lock (_locker)
        {
            if (_current.HasValue)
            {
                base.OnReport(_current.Value);
                _current = (default, false);
            }
        }
    }
}

Usage example, based on code posted in a recent duplicate question:

async void Button_Click(object sender, RoutedEventArgs e)
{
    _cts = new CancellationTokenSource();
    var progress = new ThrottledProgress<string>(msg => TextBox.Text += msg,
        TimeSpan.FromMilliseconds(50));
    var tasks = Enumerable.Range(1, 10)
        .Select(i => Task.Run(() => Worker(i, _cts.Token, progress)));
    await Task.WhenAll(tasks);
    progress.Flush();
}

The Flush method can be called after the completion of the asynchronous operation, so that any progress message that may still be buffered and scheduled for future emission (presumably the last progress message), to be emitted immediately.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top