Feel Good.

06 октября 2010

Пишем обертку для Stopwatch

Во время работы приложения, в качестве мониторинга времени выполнения метода или доступа к свойству используется класс Stopwatch из System.Diagnostics. Работает Stopwatch очень просто, он замеряет время прошедшее между вызовами Start* и Stop* методами. Приведу простой пример:

// Наш секундомер.

Stopwatch timer = new Stopwatch();           

timer.Start(); // Запустим секундомер

try

{

    Thread.Sleep(1234); // долгая операция

}

finally

{

    timer.Stop(); // Остановим секундомер

    // Показания секундомера:

    TimeSpan elapsed = timer.Elapsed;

    Console.WriteLine("Время выполнения {0}", elapsed);

}


В данном примере видны все неудобства использования Stopwatch, это необходимость явно создавать объект Stopwatch, это явный запуск и остановка секундомера, при этом, если возможны исключения, то нужно добавить и обработку исключений.

Решение данной проблемы очевидное, нужно применить интерфейс IDisposable в сочетании с блоком using. Для этого реализуем вспомогательный класс:

class Watcher : IDisposable

{

    Action<TimeSpan> _action;

    Stopwatch _timer;

 

    // закрытый конструктор

    Watcher(Action<TimeSpan> action)

    {

        _timer = new Stopwatch();

        _action = action;

        _timer.Start();

    }

 

    public void Dispose()

    {

        _timer.Stop();

        if (_action != null)

        {

            try

            {

                _action(_timer.Elapsed);

            }

            catch

            {

                // не забываем, что Dispose

                // безопасен относительно

                // исключений

            }

        }

    }

 

    public static Watcher Start(Action<TimeSpan> action)

    {

        return new Watcher(action);

    }

}


И пример использования:

using (Watcher.Start(t => Console.WriteLine(t)))

{

    Thread.Sleep(1234); // долгая операция

}


Получилась достаточно удобная конструкция, допускающая вложенность любой глубины и разделяющая код на блоки:

using (Watcher.Start(t => Console.WriteLine(t)))

{

    using (Watcher.Start(t => Console.WriteLine(t)))

    {

        // операция 1

    }

 

    using (Watcher.Start(t => Console.WriteLine(t)))

    {

        // операция 2

    }

}


Класс Watcher не является законченным, его можно расширять на вкус и цвет. Например, если ведется лог, можно сбрасывать информацию сразу в лог, который можно получать при помощи IoC and DI pattern. Либо добавить header/footer информацию, которая будет записываться в лог при входе или выходе из блока.

Ссылки:
  1. Stopwatch Class

7 комментариев:

  1. Что значит "Dispose безопасен относительно исключений"? Dispose не должен кидать исключения, если он вызывается несколько раз, но я впервые слышу, что Dispose должен игнорировать все исключения.

    ОтветитьУдалить
  2. Стоит избегать возникновения исключений в методе Dispose. Это хорошая практика и я стараюсь ее придерживаться.

    The "Framework Design Guidelines" (2nd ed) has this as (§9.4.1):
    "AVOID throwing an exception from within Dispose(bool) except under critical situations where the containing process has been corrupted (leaks, inconsistent shared state, etc.)"

    http://stackoverflow.com/questions/577607/should-you-implement-idisposable-dispose-so-that-it-never-throws

    ОтветитьУдалить
  3. спасибо, хорошая и простая идея!
    за 2 минуты прочитал и принял на вооружение

    ОтветитьУдалить
  4. В Guidelines речь идет о том, должен ли Dispose сам кидать исключения. Полностью согласен, что не должен.

    На stackoverflow обсуждается, должен ли Dispose игнорировать исключения, которые кидаются при освобождении дочерних ресурсов. Хоть это к нашему случаю и не относится, но правильный ответ - не должен. Если Dispose дочерних ресурсов следуют Guidelines, то они не будут кидать исключения, игнорировать нечего. Dispose(false) не должен освобождать управляемые ресурсы, значит опять игнорировать нечего.

    В нашем случае в Dispose вызывается пользовательский код. Если этот код, например, протоколирует банковскую транзакцию, а протоколы потом используются для аудита, то игнорировать исключение нельзя.

    Более общий вопрос - надо ли писать такой код:
    try
    {
    // Some code
    }
    catch
    {
    try
    {
    // Buggy cleanup
    }
    catch
    {
    // Ignore exceptions
    }

    throw;
    }

    На мой взгляд нельзя, т.к. баг в cleanup-е никогда не будет исправлен. Сначала надо исправить все баги в catch/finaly блоках, потом уже искать баги в try.

    В любом случае, игнорировать все исключения - очень плохо. Во-первых, нельзя игнорировать фатальные исключения (OutOfMemoryException, ...) Во-вторых, большая часть исключений являются следствием багов в коде, и эти баги надо исправлять.

    PS. Если action == null, то зачем создавать и запускать таймер? Имеет ли смысл вызов Start(null)?

    ОтветитьУдалить
  5. Спасибо!
    Простой и красивый способ!

    ОтветитьУдалить
  6. Да, можно доработать Watcher, поставив проверку в конструкторе на action != null.

    Я полностью согласен с Вами, что если мы протоколируем что-то очень важное, то ни в коем случае нельзя упускать никаких деталей, поэтому данный пример Watcher нужно доработать "под себя", добавив в catch дополнительную обработку,
    а данный Watcher демонстрирует просто идею.

    ОтветитьУдалить
  7. Ещё можно выставлять текущему потоку наивысший приоритет и возвращать предыдущее значение приоритета в Dispose()

    ОтветитьУдалить