Skip to content

Simulated Time Provider

SimulatedTimeProvider is a fully controllable implementation of .NET's TimeProvider abstraction. It solves the core problem of non-deterministic test timing: rather than waiting for real wall clock time to pass, you advance the simulated clock explicitly and drive all timers and timeouts in a predictable, reproducible order.

Core Concepts

SimulatedTimeProvider maintains two independent time dimensions:

  • Wall time — the simulated "current time" returned by GetUtcNow(). You can set this freely with SetUtcNow(), move it backward for clock-skew tests, or advance it with Advance().
  • Scheduler time — the monotonic clock that governs when timers fire. It advances only when you call Advance(), and drives all CreateTimer callbacks in due-time order.

The independence of these two clocks means you can simulate clock skew, time rewinding, and drift without affecting timer-firing semantics.

TIP

Scheduler time advances only via Advance(). Wall time can be modified independently for clock-skew and rewind simulations.

API

MethodDescription
SetUtcNow(DateTimeOffset)Set the wall clock to an arbitrary time
Advance(TimeSpan)Advance scheduler (and wall) time, firing all due timers in order
GetTimestamp()Returns the current monotonic scheduler timestamp
GetElapsedTime(...)Returns elapsed time since a previous GetTimestamp() value

Deterministic Timers

CreateTimer creates a timer whose callback is driven by simulated scheduler time. All due timers fire synchronously during Advance(), in ascending due-time order.

csharp
var tp = new SimulatedTimeProvider();

var fired = 0;
using var timer = tp.CreateTimer(_ => fired++, null, TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);

tp.Advance(TimeSpan.FromSeconds(1));
// fired == 1

Multiple timers with different delays fire in the correct order:

csharp
var tp = new SimulatedTimeProvider();
var log = new List<string>();

using var t1 = tp.CreateTimer(_ => log.Add("first"),  null, TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);
using var t2 = tp.CreateTimer(_ => log.Add("second"), null, TimeSpan.FromSeconds(2), Timeout.InfiniteTimeSpan);

tp.Advance(TimeSpan.FromSeconds(2));
// log == ["first", "second"]

Callback execution model

Timer callbacks are invoked synchronously by the thread calling Advance(), and are fired outside the internal scheduler lock. This makes simulations deterministic and avoids re-entrancy hazards inside the scheduler.

Changing and cancelling timers

The timer returned by CreateTimer(...) implements ITimer, so you can reschedule it using Change(...) or stop it by disposing it.

csharp
var tp = new SimulatedTimeProvider();
var fired = 0;

using var timer = tp.CreateTimer(_ => fired++, null, TimeSpan.FromSeconds(10), Timeout.InfiniteTimeSpan);

// Reschedule to fire sooner
timer.Change(TimeSpan.FromSeconds(1), Timeout.InfiniteTimeSpan);

tp.Advance(TimeSpan.FromSeconds(1));
// fired == 1

Periodic Timers

Periodic timers are rescheduled after each callback. By default, SimulatedTimeProvider coalesces periodic timers on large time jumps: if a single Advance() spans multiple periods, the callback fires once, and the next occurrence is scheduled from “now”.

csharp
var tp = new SimulatedTimeProvider();
var ticks = 0;

using var timer = tp.CreateTimer(_ => ticks++, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));

tp.Advance(TimeSpan.FromSeconds(5));
// ticks == 1 (coalesced)

If you want each period to be observed, advance in steps:

csharp
var tp = new SimulatedTimeProvider();
var ticks = 0;

using var timer = tp.CreateTimer(_ => ticks++, null, TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1));

tp.Advance(TimeSpan.FromSeconds(1));
tp.Advance(TimeSpan.FromSeconds(1));
tp.Advance(TimeSpan.FromSeconds(1));
// ticks == 3

Instrumentation

SimulatedTimeProvider exposes lightweight counters via tp.Statistics (see Instrumentation for the full list).

Running the Demo

bash
dotnet run --project demo/Clockworks.Demo -- simulated-time

This demo shows simulated timers, periodic timer coalescing, and scheduler statistics in action.

Released under the MIT License.