Introduction
Our code deals with date and time quite often. For example, we might need to do some work at specified dates and times (say at 10:00 AM every Monday); or some operation must be repeated every 5 minutes. .NET platform provides useful types for this including System.DateTime
, System.TimeSpan
, System.Diagnostics.Stopwatch
, as well as different timer classes. But there are cases where we need functionality which is not available in .NET "out of the box".
Consider our application does some operation by schedule every day (or every hour - it doesn't matter). If we need to debug the application, then we definitely would not like to wait until the operation time comes "naturally". Several options exist which may help in this situation. If the application logic is controlled by some editable settings (e.g., stored in a configuration file), then special debug settings could be created; or we may change current system time to some moment "before event". A different option is described here: enable date and time "virtualization" by using a date and time source (or say provider) interface. The article presents my vision of such interface. Two implementations of the interface are suggested as well. One implementation provides system date and time; it's an alternative to the direct use of .NET types (DateTime
, Stopwatch
). Another implementation allows to specify a time acceleration (deceleration) factor so the time effectively goes faster or slower compared to the system time for the code which uses it. There is also an option to specify an arbitrary date and time value as a starting point. This feature can be used to reproduce time-dependent results when debugging or developing some simulation code.
Using the Code
The Downloadable Archive Contents
The archive contains Visual Studio 2013 solution DateTimeSource
. The date and time source interface IDateTimeSource
and two of its implementations SystemDateTimeSource
and SimulatedDateTimeSource
are defined in one source file DateTimeSource.cs. The solution contains two test projects as well: UnitTests console test program and TestSimDateTimeGui
WinForms application.
IDateTimeSource Interface
Getting the Current Date and Time
The interface provides a method to get the current (in terms of the class which implements the interface) UTC date and time as a DateTime
value. This value may be equal or not equal to the "real" date and time (i.e., current system time) depending on the semantics of the interface implementation. Use DateTime.ToLocalTime
method to convert the value returned to local time.
public interface IDateTimeSource
{
DateTime GetUtcNow();
This GetUtcNow
method shall be used instead of DateTime.Now
or DateTime.UtcNow
in all code that depends on the current date and time if you want to get benefits from using a customizable date and time source, for example:
void DoSomethingAt10AM(IDateTimeSource dtSrc)
{
TimeSpan localTime = dtSrc.GetUtcNow().ToLocalTime().TimeOfDay;
if (localTime.Hours == 10)
{
}
}
Using ticks-related Methods
The interface provides several methods to work with "ticks" which can be used to measure time intervals. Ticks represent date and time in arbitrary integer units. The exact number of ticks per second (i.e., the frequency in Hertz) depends on the interface implementation. It can be obtained with the help of GetFrequency
method.
public interface IDateTimeSource
{
long GetTicks();
long GetFrequency();
The tick-related methods can be used to repeat some actions with a specified time interval. Consider we need to implement a function which returns a current measurement from some sensor (e.g., a thermometer). Let's assume that getting these measurements is a relatively time-consuming operation; at the same time, we have a priori knowledge that the measured parameter cannot change too fast. In this case, we may improve performance by caching the last received value and returning it to a caller until the value is obsolete:
readonly TimeSpan minCheckInterval = TimeSpan.FromMilliseconds(500);
long lastCheckTicks = -1;
double lastValue;
double GetMeasurement(IDateTimeSource dtSrc, MySensor sensor)
{
if ((lastCheckTicks < 0) || (dtSrc.TimeElapsedFrom(lastCheckTicks) >= minCheckInterval))
{
lastCheckTicks = dtSrc.GetTicks();
lastValue = sensor.GetValue();
}
return lastValue;
}
The function can be made simpler with the help of IsTimeElapsedFrom
method:
double GetMeasurement(IDateTimeSource dtSrc, MySensor sensor)
{
if (dtSrc.IsTimeElapsedFrom(ref lastCheckTicks, minCheckInterval))
{
lastValue = sensor.GetValue();
}
return lastValue;
}
There is a method which converts ticks to DateTime
:
public interface IDateTimeSource
{
DateTime GetDateTimeUtc(long ticks);
After running this code fragment...
DateTime dt1 = dtSrc.GetUtcNow();
long ticks = dtSrc.GetTicks();
DateTime dt2 = dtSrc.GetDateTimeUtc(ticks);
...we may expect that dt2
is almost equal to dt1
normally, i.e., in case the code execution was not interrupted for a considerable amount of time (which is always possible in multitasking environment) and the system time hasn't been corrected after executing line [1] but before [2] or [3].
SystemDateTimeSource
The class is IDateTimeSource
implementation based on the current system time.
Its GetUtcNow
method simply returns DateTime.UtcNow
.
Its GetTicks
and other ticks-related methods are based on Stopwatch
:
public interface IDateTimeSource
{
public long GetTicks()
{
return Stopwatch.GetTimestamp();
}
There is no need to create multiple instances of SystemDateTimeSource
. Static
class SystemDateTime
implements the singleton pattern for it:
DateTime utcNow = SystemDateTime.Instance.GetUtcNow();
SimulatedDateTimeSource
This IDateTimeSource
implementation demonstrates the real power of date and time virtualization. The class allows to accelerate/decelerate its virtual "clock" comparing to the system time and also to specify an arbitrary start (initial) date and time.
Using Time Acceleration
The class constructors allow to specify the time acceleration factor and optionally the start date and time:
public class SimulatedDateTimeSource : StopwatchDateTimeSource, IDateTimeSource
{
public SimulatedDateTimeSource(double timeAcceleration, DateTime? start = null)
Consider the following code fragment:
var dtSrc = new SimulatedDateTimeSource(2.0);
DateTime dt1 = dtSrc.GetUtcNow();
DateTime dt2 = SystemDateTime.Instance.GetUtcNow();
TimeSpan diff = dt1 - dt2;
Console.WriteLine(diff);
Thread.Sleep(1000);
dt1 = dtSrc.GetUtcNow();
dt2 = SystemDateTime.Instance.GetUtcNow();
diff = dt1 - dt2;
Console.WriteLine(diff);
After line [1], the value of diff
will be close to TimeSpan.Zero
normally (i.e., dt1
and dt2
almost equal) with the same reservations as previously (code execution not interrupted, system time not corrected).
After line [2], the value of diff
is expected to be about +1 second because the "clock" inside this simulated date/time source instance runs twice as fast as the real system time.
Setting Start Date and Time
We may specify an arbitrary start date and time for our date/time source as well:
var startDt = new DateTime(1900, 1, 1, 12, 0, 0, DateTimeKind.Utc);
var dtSrc = new SimulatedDateTimeSource(1.0, startDt);
Console.WriteLine(dtSrc.GetUtcNow().ToString(CultureInfo.InvariantCulture));
Thread.Sleep(1000);
Console.WriteLine(dtSrc.GetUtcNow().ToString(CultureInfo.InvariantCulture));
The expected output:
01/01/1900 12:00:00
01/01/1900 12:00:01
There is a second constructor which allows to specify another IDateTimeSource
as the base for this SimulatedDateTimeSource
. It makes it possible to create "chained" date/time sources.
public class SimulatedDateTimeSource : StopwatchDateTimeSource, IDateTimeSource
{
public SimulatedDateTimeSource
(IDateTimeSource source, double timeAcceleration, DateTime? start = null)
Limitations
Date/time Resolution Decreases If Using Acceleration
Current SimulatedDateTimeSource
implementation uses SystemDateTimeSource
as the base date/time source by default; in its turn SystemDateTimeSource
returns DateTime.UtcNow
in its GetUtcNow
. The resolution of DateTime.UtcNow
(DateTime.Now
) is not great. I executed this code on my system to find it:
DateTime dt2;
int count = 0;
var dt1 = DateTime.UtcNow;
do
{
dt2 = DateTime.UtcNow;
++count;
}
while (dt1 == dt2);
Console.WriteLine("Count: {0}. Diff: {1}", count, (dt2 - dt1));
The typical results observed:
Count: 128419. Diff: 00:00:00.0050000
Count: 136151. Diff: 00:00:00.0050000
Count: 137519. Diff: 00:00:00.0050001
The date/time resolution found is about 5 milliseconds.
Here is the GetUtcNow
implementation from SimulatedDateTimeSource
:
public DateTime GetUtcNow()
{
TimeSpan diff = source.GetUtcNow() - startDateTime;
double ticks = (double)startDateTime.Ticks + correction.Ticks + (diff.Ticks * acceleration);
if (ticks >= DateTime.MaxValue.Ticks)
{
return DateTime.MaxValue;
}
if (ticks <= DateTime.MinValue.Ticks)
{
return DateTime.MinValue;
}
return new DateTime((long)ticks, DateTimeKind.Utc);
}
You see the time difference ticks are multiplied by acceleration
which deteriorates the resolution. I ran the modified test code:
var dtSrc = new SimulatedDateTimeSource(3.0);
DateTime dt2;
int count = 0;
var dt1 = dtSrc.GetUtcNow();
do
{
dt2 = dtSrc.GetUtcNow();
++count;
}
while (dt1 == dt2);
Console.WriteLine("Count: {0}. Diff: {1}", count, (dt2 - dt1));
The typical results:
Count: 52274. Diff: 00:00:00.0150016
Count: 46914. Diff: 00:00:00.0149888
Count: 46679. Diff: 00:00:00.0150016
Now the date/time resolution is about 15 milliseconds (3 times worse, proportionally to acceleration
).
Time Acceleration Factor Limitations
The current SimulatedDateTimeSource
implementation does not accept time acceleration factors if the resulting frequency will not fit to the range [1..long.MaxValue
]. Below is a part of SimulatedDateTimeSource.Reset
method which is used by both SimulatedDateTimeSource
constructors and also may be called anytime after constructing an instance of the class to change the simulation parameters:
public void Reset(IDateTimeSource source, double timeAcceleration, DateTime? start = null)
{
double f = source.GetFrequency() * timeAcceleration;
if ((f > long.MaxValue) || (f < 1))
{
throw new ArgumentOutOfRangeException("timeAcceleration");
}
Another important fact is every time a SystemDateTimeSource
instance converts ticks to TimeSpan
(this happens in GetTimeSpan
, TimeElapsedFrom
, IsTimeElapsedFrom
, GetDateTimeUtc
methods), it clips the result to the range [TimeSpan.MinValue..TimeSpan.MaxValue
]. It's not of much importance when using SystemDateTimeSource
by itself but shall be taken into account if using SimulatedDateTimeSource
(based on SystemDateTimeSource
by default) with big time acceleration factors: in this case TimeSpan.MaxValue
value can be practically reached.
Points of Interest
Working with date and time and measuring time intervals is not as easy as it seems. Many factors complicate it: hardware limitations (e.g., system real-time clock drift), CPU power saving modes, multitasking effects, multiprocessor/multicore effects, non-trivial system time synchronization algorithms. There are interesting articles at CodeProject that cover these topics, for example, A Tale of Two Timers.
History
- December 12, 2016: First revision