Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clean up Stopwatch a bit #111834

Merged
merged 2 commits into from
Jan 26, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,22 @@

namespace System.Diagnostics
{
// This class uses high-resolution performance counter if the installed
// hardware supports it. Otherwise, the class will fall back to DateTime
// and uses ticks as a measurement.

[DebuggerDisplay("{DebuggerDisplay,nq}")]
public partial class Stopwatch
{
private long _elapsed;
private long _startTimeStamp;
private bool _isRunning;

// "Frequency" stores the frequency of the high-resolution performance counter,
// if one exists. Otherwise it will store TicksPerSecond.
// The frequency cannot change while the system is running,
// so we only need to initialize it once.
public static readonly long Frequency = QueryPerformanceFrequency();
public static readonly bool IsHighResolution = true;

// performance-counter frequency, in counts per ticks.
// This can speed up conversion from high frequency performance-counter
// to ticks.
// This can speed up conversion to ticks.
private static readonly double s_tickFrequency = (double)TimeSpan.TicksPerSecond / Frequency;

public Stopwatch()
{
Reset();
}

public void Start()
Expand All @@ -43,7 +33,7 @@ public void Start()

public static Stopwatch StartNew()
{
Stopwatch s = new Stopwatch();
Stopwatch s = new();
s.Start();
return s;
}
Expand All @@ -53,29 +43,16 @@ public void Stop()
// Calling stop on a stopped Stopwatch is a no-op.
if (_isRunning)
{
long endTimeStamp = GetTimestamp();
long elapsedThisPeriod = endTimeStamp - _startTimeStamp;
_elapsed += elapsedThisPeriod;
_elapsed += GetTimestamp() - _startTimeStamp;
_isRunning = false;

if (_elapsed < 0)
{
// When measuring small time periods the Stopwatch.Elapsed*
// properties can return negative values. This is due to
// bugs in the basic input/output system (BIOS) or the hardware
// abstraction layer (HAL) on machines with variable-speed CPUs
// (e.g. Intel SpeedStep).

_elapsed = 0;
}
Comment on lines -61 to -70
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe we still support CPUs that could have this issue on Linux, although such processors are increasingly rare.

The Intel Architecture Manual covers that RDTSC is tied to the processor frequency for Pentium M processors (family [06H], models [09H, 0DH]); for Pentium 4 processors, Intel Xeon processors
(family [0FH], models [00H, 01H, or 02H]); and for P6 family processors

For a constant rate that is not tied to the CPU frequency, you instead need: Pentium 4 processors, Intel Xeon processors (family [0FH], models [03H and higher]); for Intel Core Solo
and Intel Core Duo processors (family [06H], model [0EH]); for the Intel Xeon processor 5100 series and Intel
Core 2 Duo processors (family [06H], model [0FH]); for Intel Core 2 and Intel Xeon processors (family [06H],
DisplayModel [17H]); for Intel Atom processors (family [06H], DisplayModel [1CH]).

But even more so, it is technically dependent on querying that the CPU supports Invariant TSC by querying CPUID 0x8000_0007 and then reading EDX bit 8.

QPC on Windows and CLOCK_MONOTONIC on Unix is supposed to normalize this, but historically there have been bugs.

}
}

public void Reset()
{
_elapsed = 0;
_isRunning = false;
_startTimeStamp = 0;
_isRunning = false;
}

// Convenience method for replacing {sw.Reset(); sw.Start();} with a single sw.Restart()
Expand All @@ -94,32 +71,30 @@ public void Restart()
/// </returns>
public override string ToString() => Elapsed.ToString();

public bool IsRunning
{
get { return _isRunning; }
}
public bool IsRunning => _isRunning;

public TimeSpan Elapsed
{
get { return new TimeSpan(GetElapsedDateTimeTicks()); }
}
public TimeSpan Elapsed => new(ElapsedTimeSpanTicks);

public long ElapsedMilliseconds
{
get { return GetElapsedDateTimeTicks() / TimeSpan.TicksPerMillisecond; }
}
public long ElapsedMilliseconds => ElapsedTimeSpanTicks / TimeSpan.TicksPerMillisecond;

public long ElapsedTicks
{
get { return GetRawElapsedTicks(); }
}
get
{
long timeElapsed = _elapsed;

public static long GetTimestamp()
{
Debug.Assert(IsHighResolution);
return QueryPerformanceCounter();
// If the Stopwatch is running, add elapsed time since the Stopwatch is started last time.
if (_isRunning)
{
timeElapsed += GetTimestamp() - _startTimeStamp;
}

return timeElapsed;
}
}

public static long GetTimestamp() => QueryPerformanceCounter();

/// <summary>Gets the elapsed time since the <paramref name="startingTimestamp"/> value retrieved using <see cref="GetTimestamp"/>.</summary>
/// <param name="startingTimestamp">The timestamp marking the beginning of the time period.</param>
/// <returns>A <see cref="TimeSpan"/> for the elapsed time between the starting timestamp and the time of this call.</returns>
Expand All @@ -131,31 +106,9 @@ public static TimeSpan GetElapsedTime(long startingTimestamp) =>
/// <param name="endingTimestamp">The timestamp marking the end of the time period.</param>
/// <returns>A <see cref="TimeSpan"/> for the elapsed time between the starting and ending timestamps.</returns>
public static TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) =>
new TimeSpan((long)((endingTimestamp - startingTimestamp) * s_tickFrequency));

// Get the elapsed ticks.
private long GetRawElapsedTicks()
{
long timeElapsed = _elapsed;
new((long)((endingTimestamp - startingTimestamp) * s_tickFrequency));

if (_isRunning)
{
// If the Stopwatch is running, add elapsed time since
// the Stopwatch is started last time.
long currentTimeStamp = GetTimestamp();
long elapsedUntilNow = currentTimeStamp - _startTimeStamp;
timeElapsed += elapsedUntilNow;
}
return timeElapsed;
}

// Get the elapsed ticks.
private long GetElapsedDateTimeTicks()
{
Debug.Assert(IsHighResolution);
// convert high resolution perf counter to DateTime ticks
return unchecked((long)(GetRawElapsedTicks() * s_tickFrequency));
}
private long ElapsedTimeSpanTicks => (long)(ElapsedTicks * s_tickFrequency);

private string DebuggerDisplay => $"{Elapsed} (IsRunning = {_isRunning})";
}
Expand Down