Building a High-Precision .NET Stopwatch: Best Practices and Examples

Implementing a Custom .NET Stopwatch with Lap and Pause Support

Overview

A custom .NET stopwatch extends System.Diagnostics.Stopwatch (or wraps it) to add lap recording, pause/resume, and a user-friendly API. Key goals: high-resolution timing, accurate elapsed calculation across pauses, efficient lap storage, thread-safety, and clear formatting of results.

Core design

  • Use System.Diagnostics.Stopwatch internally for high-resolution ticks.
  • Track running state (running/paused/stopped).
  • Maintain accumulated elapsed TimeSpan to include time before pauses.
  • Record laps as TimeSpan values representing either split (since start) or lap (since previous lap).
  • Provide methods: Start(), Stop(), Pause(), Resume(), Reset(), Lap(), GetElapsed(), GetLaps().
  • Make public API synchronous and simple; avoid blocking operations.

Minimal data model

  • private readonly Stopwatch _sw;
  • private TimeSpan _accumulated; // elapsed while not running
  • private List _laps; // cumulative or interval laps
  • private object _lock; // for thread-safety

Behavior specifics

  • Start(): if not started, set _accumulated = TimeSpan.Zero, clear laps, start _sw.
  • Pause(): stop _sw, add _sw.Elapsed to _accumulated.
  • Resume(): start _sw again (do not reset _sw.Elapsed, or Reset() and reuse accumulated).
  • Stop(): equivalent to Pause() but mark stopped; subsequent Start() resets by default or provide overload.
  • Reset(): stop and clear accumulated and laps.
  • Lap(): capture current elapsed (accumulated + _sw.Elapsed if running, else accumulated), then store either:
    • Split lap: store total elapsed since start; or
    • Interval lap: store delta from previous lap (calculate difference).

Thread-safety

  • Lock around state changes and lap reads/writes to avoid races in multi-threaded use.

Accuracy notes

  • Rely on Stopwatch.Frequency and Stopwatch.IsHighResolution for precision.
  • Avoid DateTime.UtcNow for timing.
  • When pausing, capture sw.Elapsed immediately before stopping to avoid race.

Example implementation (C#)

csharp

using System; using System.Collections.Generic; using System.Diagnostics; public class CustomStopwatch { private readonly Stopwatch _sw = new Stopwatch(); private TimeSpan _accumulated = TimeSpan.Zero; private readonly List<TimeSpan> _laps = new List<TimeSpan>(); private readonly object _lock = new object(); private bool _started = false; public void Start() { lock (_lock) { _accumulated = TimeSpan.Zero; _laps.Clear(); _sw.Reset(); _sw.Start(); _started = true; } } public void Pause() { lock (_lock) { if (!_sw.IsRunning) return; _sw.Stop(); _accumulated += _sw.Elapsed; _sw.Reset(); } } public void Resume() { lock (_lock) { if (_sw.IsRunning) return; _sw.Start(); } } public void Stop() { lock (_lock) { if (_sw.IsRunning) { _sw.Stop(); _accumulated += _sw.Elapsed; _sw.Reset(); } _started = false; } } public void Reset() { lock (_lock) { _sw.Reset(); _accumulated = TimeSpan.Zero; _laps.Clear(); _started = false; } } public TimeSpan GetElapsed() { lock (_lock) { return _accumulated + (_sw.IsRunning ? _sw.Elapsed : TimeSpan.Zero); } } // Records interval laps (time since previous lap) public void Lap() { lock (_lock) { var total = GetElapsed(); var last = _laps.Count == 0 ? TimeSpan.Zero : Sum(_laps); var interval = total - last; _laps.Add(interval); } } private static TimeSpan Sum(IEnumerable<TimeSpan> items) { long ticks = 0; foreach (var t in items) ticks += t.Ticks; return TimeSpan.FromTicks(ticks); } public IReadOnlyList<TimeSpan> GetLaps() { lock (_lock) { return _laps.AsReadOnly(); } } }

API variations and enhancements

  • Return lap index and timestamps.
  • Store both split (cumulative) and interval lap values.
  • Expose events (LapRecorded, Paused, Resumed).
  • Provide formatted output (mm:ss.fff) or culture-aware formatting.
  • Make it IDisposable if using timers or unmanaged resources.
  • Add cancellation/timeout helpers for async scenarios.

Usage example

  • Start(), perform work, Lap(), Pause(), Resume(), Lap(), Stop(), then read GetLaps() and GetElapsed().

Testing

  • Unit test Start/Stop/Pause/Resume transitions, lap intervals, and concurrent access.
  • Test accuracy against raw Stopwatch in tight loops.

When to use vs. Stopwatch directly

  • Use custom class when you need lap semantics, pause/resume accumulation, or richer API. For simple measurements, System.Diagnostics.Stopwatch alone is sufficient.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *