DEV Community

Cover image for Hacking IDisposable: The Stopwatch Example
Jakub Kwaśniewski
Jakub Kwaśniewski

Posted on

Hacking IDisposable: The Stopwatch Example

Provides a mechanism for releasing unmanaged resources.

This is the first sentence you can read about IDisposable interface in the Microsoft documentation - and it definitely should be the primary usage of it. However, the C# language is cool enough to have a special using statement that comes along when using objects that implement IDisposable. There is something elegant and compact about this language structure, which makes me really like it - that's why I use it even for places that it was not designed for. Like, for example, wrapping up the Stopwatch.

Stopwatch

The Stopwatch class was introduced in .NET Framework 2.0 and since then became a de facto standard for measuring code execution time. If you were ever interested in checking how long does it takes to run a specific part of your program, you've probably stumbled into it.

The basic usage pattern of a Stopwatch class is as follows:

var stopwatch = new Stopwatch();
stopwatch.Start();

// Code which execution time we want to measure

stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed);
Enter fullscreen mode Exit fullscreen mode

There are a couple of caveats - static method Stopwatch.StartNew() can be used as a shortcut for creating Stopwatch instance and starting it at the same time. Also, the Elapsed property can be accessed without actually stopping the stopwatch first - it will keep on counting time.

So the structure is already there - we start the stopwatch, we execute the code we want to measure, we stop the stopwatch. Whenever and wherever you do it, the structure always looks similar to this. And since it's as simple as wrapping the "proper" code you want to measure with some init and teardown actions, the using statement might be a good fit.

Disposable stopwatch

So lets start with defining a class implementing IDisposable.

public class Timer : IDisposable
{
    private readonly Stopwatch _stopwatch;

    public Timer()
    {
        _stopwatch = Stopwatch.StartNew();
    }

    public void Dispose()
    {
        _stopwatch.Stop();
        Console.WriteLine(_stopwatch.Elapsed);
    }
}

// Usage example

using (new Timer())
{
    // Code which execution time we want to measure
}
Enter fullscreen mode Exit fullscreen mode

This is functionally identical to the previous example and for me looks better already, however there are a couple of things we can do to make it more useful.

First of all, writing to the console output is not necessarily the only thing we want to do with the measured time span. Sometimes it might be, but sometimes we'd rather write it to file or to an external system, eventually ending up in some kind of monitoring platform.

So we could parametrize the Timer and allow its clients to decide what to do with the measured execution time.

public class Timer : IDisposable
{
    private readonly Action<TimeSpan> _afterMeasuredAction;
    private readonly Stopwatch _stopwatch;

    public Timer(Action<TimeSpan> afterMeasuredAction)
    {
        _afterMeasuredAction = afterMeasuredAction ?? (_ => { });
        _stopwatch = Stopwatch.StartNew();
    }

    public void Dispose()
    {
        _stopwatch.Stop();
        _afterMeasuredAction.Invoke(_stopwatch.Elapsed);
    }
}

// Usage example

using (new Timer(measuredTime => Console.WriteLine(measuredTime)))
{
    // Code which execution time we want to measure
}
Enter fullscreen mode Exit fullscreen mode

There's something I still don't like about this usage pattern. Maybe because we're newing up an object here that we don't really care about - not even assigning it to any variable. Effectively there's nothing wrong about it, cause what we really want is to create it and dispose automatically at the end of the using block. But if we don't need to do any other thing with this object - why do we need to know its type? Well, we don't - knowing that it implements IDisposable should be more than enough for us. So instead of newing up an instance of a specific type, I'd rather hide it with some method and directly return just an IDisposable.

public class Measurement
{
    public static IDisposable Run(
        Action<TimeSpan> afterMeasuredAction = null) =>
            new Timer(afterMeasuredAction);

    private class Timer : IDisposable
    {
        private readonly Action<TimeSpan> _afterMeasuredAction;
        private readonly Stopwatch _stopwatch;

        public Timer(Action<TimeSpan> afterMeasuredAction)
        {
            _afterMeasuredAction = afterMeasuredAction ?? (_ => { });
            _stopwatch = Stopwatch.StartNew();
        }

        public void Dispose()
        {
            _stopwatch.Stop();
            _afterMeasuredAction.Invoke(_stopwatch.Elapsed);
        }
    }
}

// Usage example

using (Measurement.Run(measuredTime => Console.WriteLine(measuredTime))
{
    // Code which execution time we want to measure
}
Enter fullscreen mode Exit fullscreen mode

One last thing I'd like to enhance a bit. Since Measurement is a very generic purpose class - probably for use in many places, as we usually want to measure execution time of multiple methods or algorithms within our code base - we'd normally define this action after measure (like Console.WriteLine) many times. To avoid that we can create a specific class that uses the generic one.

public class LogMeasurement
{
    private readonly ILogger<LogMeasurement> _logger;

    public LogMeasurement(ILogger<LogMeasurement> logger)
    {
        _logger = logger;
    }

    public IDisposable Run() => Measurement.Run(
        timeSpan => _logger.LogTrace($"Measured time: {timeSpan}"));
}

// Usage example

using (_logMeasurement.Run())
{
    // Code which execution time we want to measure
}
Enter fullscreen mode Exit fullscreen mode

Note that this time we have an instance Run() method instead of a static one as previously. The reason is that I'd probably want to inject this class in all the places I'd like to use it, so it's ready for usage in a dependency injection scenario. That's also why it has a constructor with its own dependencies injected. In other words we now have one class to gather everything needed to do whatever we want to do in our system with measured code execution time.

And if you sometimes want to do something else with it - you just define another similar class, which uses Measurement.Run with a different action, and inject it instead of the LogMeasurement one!

Post Scriptum

This blog post is mostly about answering the "How to measure code execution time?" question. One thing I haven't and won't dig too deep into is an answer to "Where to measure it?" question. But just briefly - I do think that any code not related strictly to the domain of the class or method just adds noise and distraction. So adding some logging, monitoring or execution time measuring logic in-place with the domain code is not necessarily the best idea. I'd rather extract it to some kind of decorator class instead.

Also, sorry for the clickbaitish title - the provided example is not really hacking the interface in any way. But it can be considered a usage outside of the definition mentioned at the very beginning, so in this sense what we're really doing is exploring the undocumented ways of using it. Sounds hacky enough for me!

Just code

Hacking IDisposable: The Stopwatch Example on GitHub + Tests

Top comments (4)

Collapse
 
jacekmlynek profile image
jacekmlynek

Not too bad :) I am not sure if I am missing something but I cannot see quite obvious implicite bonus from using usage - measure time even if code will throw exception. That maybe quite useful in e.g. Http response time counters.

I also like the idea of post action callback sine it allow nicely to decouple your counter from the context usage.

Collapse
 
jakubkwa profile image
Jakub Kwaśniewski

True, the exception scenario is yet another benefit ;)

Collapse
 
kubapr profile image
Kuba P

Elegant, I like it :)

Collapse
 
johnnydoe profile image
Johnny Doe

Is it possible to call a async method within the LogMeasurement.Run() method. For example save some data to a database.

Would that require the static Measurement.Run() method parameter to change from a Action to a Func>