C#
Stopwatch
delegates
lambda expressions
performance optimization

Wrapping StopWatch timing with a delegate or lambda?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

Stopwatch is easy to use for one code block, but repetitive timing code quickly becomes inconsistent across a project. Wrapping timing in a delegate or lambda helper gives you a single place for formatting, error handling, and thresholds. This approach keeps business methods clean while still producing useful performance data.

Why a Wrapper Is Better Than Repeated Inline Timing

Inline timing code usually starts simple and then grows with logging, tags, and exception behavior.

Typical drift problems:

  • one file logs milliseconds, another logs ticks
  • some paths forget to stop timing after exceptions
  • operation names are inconsistent and hard to aggregate

A wrapper removes those differences. Teams can then improve timing behavior centrally without editing every call site.

Synchronous Helper with Func<T>

A practical baseline is a generic method that times an operation and returns its result.

csharp
1using System;
2using System.Diagnostics;
3
4public static class Timer
5{
6    public static T Measure<T>(string operation, Func<T> work, Action<string>? log = null)
7    {
8        if (work is null) throw new ArgumentNullException(nameof(work));
9
10        var sw = Stopwatch.StartNew();
11        try
12        {
13            return work();
14        }
15        finally
16        {
17            sw.Stop();
18            (log ?? Console.WriteLine).Invoke(
19                $"{operation} took {sw.Elapsed.TotalMilliseconds:F2} ms");
20        }
21    }
22}
23
24public class Demo
25{
26    public static void Main()
27    {
28        int sum = Timer.Measure("sum-loop", () =>
29        {
30            int acc = 0;
31            for (int i = 0; i < 1_000_000; i++) acc += i;
32            return acc;
33        });
34
35        Console.WriteLine(sum);
36    }
37}

The finally block ensures metrics are emitted even when the operation throws.

Overload for Action

Many operations do not return values. Add an overload to avoid dummy return values.

csharp
1using System;
2
3public static class TimerExtensions
4{
5    public static void Measure(string operation, Action work, Action<string>? log = null)
6    {
7        if (work is null) throw new ArgumentNullException(nameof(work));
8        Timer.Measure<object?>(operation, () =>
9        {
10            work();
11            return null;
12        }, log);
13    }
14}

This keeps call sites readable for side-effect-only methods.

Async Helper for Task and Task<T>

For network and I O workloads, timing wrappers should support async execution.

csharp
1using System;
2using System.Diagnostics;
3using System.Threading.Tasks;
4
5public static class AsyncTimer
6{
7    public static async Task<T> MeasureAsync<T>(string operation, Func<Task<T>> work, Action<string>? log = null)
8    {
9        if (work is null) throw new ArgumentNullException(nameof(work));
10
11        var sw = Stopwatch.StartNew();
12        try
13        {
14            return await work().ConfigureAwait(false);
15        }
16        finally
17        {
18            sw.Stop();
19            (log ?? Console.WriteLine).Invoke(
20                $"{operation} took {sw.Elapsed.TotalMilliseconds:F2} ms");
21        }
22    }
23}
24
25public class AsyncDemo
26{
27    public static async Task Main()
28    {
29        string value = await AsyncTimer.MeasureAsync("delayed-call", async () =>
30        {
31            await Task.Delay(120);
32            return "ok";
33        });
34
35        Console.WriteLine(value);
36    }
37}

Avoid wrapping async code with .Result or .Wait(), because that can distort timings and block threads.

Threshold Logging to Reduce Noise

In high-throughput services, logging every operation can flood logs. Add a minimum threshold.

csharp
1public static T MeasureIfSlow<T>(string operation, Func<T> work, double minMs, Action<string>? log = null)
2{
3    var sw = Stopwatch.StartNew();
4    try
5    {
6        return work();
7    }
8    finally
9    {
10        sw.Stop();
11        if (sw.Elapsed.TotalMilliseconds >= minMs)
12        {
13            (log ?? Console.WriteLine).Invoke(
14                $"SLOW {operation}: {sw.Elapsed.TotalMilliseconds:F2} ms");
15        }
16    }
17}

This keeps instrumentation useful without overwhelming observability systems.

Integrating with Structured Logging

Free-form strings are easy at first, but structured fields make analysis easier later. Instead of one message string, log fields such as operation name and elapsed time. This helps dashboards group and compare operations by name.

A simple pattern is to pass a logger delegate from your application layer, so the timer helper stays framework-agnostic.

Testing Timing Helpers

You usually do not test exact durations, since execution time varies by machine and load. Instead, test behavior:

  • wrapper returns operation result
  • wrapper logs once per invocation
  • wrapper logs on exceptions
  • threshold logic suppresses fast calls

These tests give confidence without brittle millisecond assertions.

Common Pitfalls

  • Measuring very small code blocks and assuming nanosecond-level accuracy.
  • Timing async operations with blocking calls.
  • Forgetting finally, which drops metrics on exceptions.
  • Using vague operation names that cannot be aggregated.
  • Logging every call in hot paths without thresholds.

Summary

  • Delegate and lambda wrappers make Stopwatch instrumentation consistent.
  • Use finally to ensure timing is captured for both success and failure.
  • Provide sync and async wrappers so all code paths follow one pattern.
  • Add thresholds and structured logging for production-scale observability.
  • Test behavior of the wrapper, not exact elapsed values.

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.