调试秒表






4.42/5 (8投票s)
一个调试版本秒表,可用于诊断计时。
问题
有时,需要对某个操作进行时间采样,但仅作为调试版本中的诊断。而且,虽然 .NET 2.0 中有一个出色的 Stopwatch
类,但该类不适用于非诊断时间采样,因为它
- 不是静态类,并且
- 因此不支持条件调试版本。
解决方案
解决方案是将 Stopwatch
类包装在一个静态类(称为 DebugStopwatch
)中,该类将 Stopwatch
方法公开为由 ConditionalAttribute
属性修饰的方法。此外,由于人们可能希望同时测量多个间隔,因此 DebugStopwatch
类(由于是静态的,因此无法创建实例)必须提供管理 .NET Stopwatch
类多个实例的功能。作为一项附加功能,DebugStopwatch
类还允许累积多个时间样本。这可能有助于测量重复任务中的方差。
实现
示例包装器类
此类是一个简单的包装器,用于获取时间样本。稍后我们将看到此类的用法。示例名称可用于提供有关样本的特定于应用程序的信息。
/// <summary>
/// A wrapper for a sample in time.
/// </summary>
public class Sample
{
protected string sampleName;
protected long milliseconds;
/// <summary>
/// Gets/sets milliseconds
/// </summary>
public long Milliseconds
{
get { return milliseconds; }
}
/// <summary>
/// Returns sampleName
/// </summary>
public string SampleName
{
get { return sampleName; }
}
public Sample(string sampleName, long milliseconds)
{
this.sampleName = sampleName;
this.milliseconds = milliseconds;
}
}
SampleDelta 包装器类
此类是 s[1]-s[0] 的差值和总经过时间 s[1] 的包装器。稍后我们将看到此类的用法。示例名称可用于提供有关样本的特定于应用程序的信息。
/// <summary>
/// A wrapper for elapsed time between samples
/// and the total time from the first sample
/// to the current sample.
/// </summary>
public class SampleDelta
{
protected string sampleName;
protected long milliseconds;
protected long totalMilliseconds;
/// <summary>
/// Returns totalMilliseconds
/// </summary>
public long TotalMilliseconds
{
get { return totalMilliseconds; }
}
/// <summary>
/// Returns milliseconds
/// </summary>
public long DeltaMilliseconds
{
get { return milliseconds; }
}
/// <summary>
/// Returns sampleName
/// </summary>
public string SampleName
{
get { return sampleName; }
}
public SampleDelta(string sampleName, long milliseconds, long totalMilliseconds)
{
this.sampleName = sampleName;
this.milliseconds = milliseconds;
this.totalMilliseconds = totalMilliseconds;
}
}
DebugStopwatch 类
内部集合
/// <summary>
/// Encapsulates methods that are executed only in Debug mode for measuring
/// timespans.
/// </summary>
public static class DebugStopwatch
{
internal static Dictionary<Enum, Stopwatch> stopwatchMap =
new Dictionary<Enum, Stopwatch>();
internal static Dictionary<Enum, List<Sample>> stopwatchSampleMap =
new Dictionary<Enum, List<Sample>>();
使用字典将秒表“描述符”映射到实际的 Stopwatch
实例。还使用字典来管理特定秒表“描述符”的样本。请注意,两个字典中的键都是 Enum
,允许您使用枚举而不是整数 ID 或字符串来“描述”您的秒表。请注意,这两个枚举之间没有冲突
enum Modem
{
Send=1,
Receive=2,
}
enum DSL
{
Send=1,
Receive=2,
}
因为即使枚举值相同,Enum
类型也不同。换句话说,字典中将有两个不同的条目分别用于 Modem.Send
和 DSL.Send
,即使这两个枚举的值都为 1。
关于 internal
关键字,我并不完全满意 C# 不允许在静态类中使用 protected
成员。internal
关键字阻止从不同程序集访问成员,但我真正想做的是阻止甚至从同一程序集访问。通过将类标记为 static
,这很有用,因为它能防止有人意外地实例化它,我却失去了我认为重要的对我的静态类成员可见性的控制——它们必须是 public
或 internal
。
在映射中创建条目
如果字典中当前不存在秒表描述符,则 internal
方法会在两个字典中创建条目。请注意,在将键添加到字典时,字典的“value
”成员会被初始化。
/// <summary>
/// Creates, if new, the list entries for the stopwatch.
/// </summary>
/// <param name="t"></param>
internal static void CreateIfNotInMap(Enum t)
{
if (!stopwatchMap.ContainsKey(t))
{
lock(stopwatchMap)
{
stopwatchMap[t] = new Stopwatch();
stopwatchSampleMap[t] = new List<Sample>();
}
}
}
为什么要加锁?因为可能存在不同线程同时向字典添加项的情况,而且也可能存在一个读操作正在进行,而另一个线程正在添加秒表。
基本秒表控制
基本秒表控件包括启动和停止秒表以及开始新样本收集的方法
/// <summary>
/// Starts the specified stopwatch.
/// </summary>
[Conditional("DEBUG")]
public static void Start(Enum t)
{
CreateIfNotInMap(t);
stopwatchMap[t].Start();
}
/// <summary>
/// Resets the specified stopwatch, clears the samples,
/// and starts the stopwatch.
/// </summary>
/// <param name="t"></param>
[Conditional("DEBUG")]
public static void StartNew(Enum t)
{
CreateIfNotInMap(t);
stopwatchMap[t].Reset();
stopwatchSampleMap[t].Clear();
stopwatchMap[t].Start();
}
/// <summary>
/// Stops the specified stopwatch.
/// </summary>
[Conditional("DEBUG")]
public static void Stop(Enum t)
{
stopwatchMap[t].Stop();
}
/// <summary>
/// Resets the specified stopwatch and clears the samples.
/// </summary>
[Conditional("DEBUG")]
public static void Reset(Enum t)
{
stopwatchMap[t].Reset();
stopwatchSampleMap[t].Clear();
}
StartNew
和 Reset
之间的显着区别在于,Reset
仅清除样本收集并重置秒表,而 StartNew
是依次调用 Reset
和 Start
的一种更简单的方法。
为什么在 StartNew
和 Reset
方法中的 Clear
调用周围没有锁?因为我认为这些是“主控”方法。在调用这些方法时,所有正在进行采样或报告样本的线程都应被暂停或终止。任何其他情况都没有意义。
进行时间采样
要进行时间采样,请调用所需秒表的 Sample
方法。sampleName
参数可用于提供有关时间样本的更多信息——例如,迭代计数。
/// <summary>
/// Saves the current elapsed time for the specified
/// stopwatch in the sample buffer.
/// </summary>
[Conditional("DEBUG")]
public static void Sample(Enum t, string sampleName)
{
lock (stopwatchMap)
{
stopwatchSampleMap[t].Add(new Sample(sampleName,
stopwatchMap[t].ElapsedMilliseconds));
}
}
为什么要加锁?因为这些例程需要是线程安全的,而且可能一个线程正在向秒表集合添加项,而另一个线程正在获取样本集合(见下文)。
获取经过时间
无论秒表是否正在运行,您都可以随时通过调用 ElapsedMilliseconds
方法来获取经过的时间。
/// <summary>
/// Gets the elapsed time in milliseconds for the specified stopwatch.
/// </summary>
/// <param name="t"></param>
[Conditional("DEBUG")]
public static void ElapsedMilliseconds(Enum t, ref long ret)
{
ret = stopwatchMap[t].ElapsedMilliseconds;
}
您可能会问,为什么我使用 ref
参数获取经过的毫秒数,而不是直接返回它?因为条件编译要求方法返回类型为 void
——换句话说,它不能返回任何东西。这是一个有些道理的不便。如果我可以返回列表,我就可以编写
long ms=DebugStopwatch.ElapsedMilliseconds(Stopwatches.SW1);
如果我在 Release 模式下编译此代码,编译器会怎么做?顺便说一句,out
关键字也不起作用,因为 out
会初始化变量,而在 Release 模式下编译时,变量不会被初始化,这会再次导致编译器出现问题,因为它无法发出那个关于变量未初始化的有用错误。
获取样本
在任何时候,都可以返回当前样本的集合。
/// <summary>
/// Returns the sample buffer for the specified stopwatch.
/// The input list is replaced with a copy of the sample list at
/// the current moment in time.
/// </summary>
[Conditional("DEBUG")]
public static void GetSamples(Enum t, ref List<Sample> ret)
{
lock (stopwatchMap)
{
ret = new List<Sample>(stopwatchSampleMap[t]);
}
}
如上所述,条件编译方法不能返回任何东西,所以返回必须通过 ref
来处理。锁可以防止在复制样本列表时更新该列表,我们复制它以便调用者获得副本,而不是原始列表。这可以防止调用者篡改我们的列表,而调用者也不必担心线程问题——副本是调用者自己的,可以随意处理。
获取样本增量
在任何时候,您都可以获取样本列表作为时间增量。
/// <summary>
/// Returns the buffer of sample deltas, S(n) - S(n-1) for the specified stopwatch.
/// The input list is replaced with the sample delta list.
/// </summary>
[Conditional("DEBUG")]
public static void GetSampleDeltas(Enum t, ref List<SampleDelta> ret)
{
List<Sample> samples = null;
GetSamples(t, ref samples);
ret = new List<SampleDelta>();
if (samples.Count > 0)
{
Sample s0=samples[0];
ret.Add(new SampleDelta(s0.SampleName, s0.Milliseconds, s0.Milliseconds));
long ms=s0.Milliseconds;
for (int i=1; i<samples.Count; i++)
{
Sample sn=samples[i];
long delta = sn.Milliseconds - ms;
ret.Add(new SampleDelta(sn.SampleName, delta, sn.Milliseconds));
ms=sn.Milliseconds;
}
}
}
这是一个很好的辅助方法,可以获取每个样本之间的时间,而不是必须自己编写此方法。还会保持运行总计。
进行样本转储
一个名为 DumpSamples
的辅助方法会对指定秒表当前集合中的所有样本进行调试输出。如果您使用的是 Traceract,输出将与此类似(来自单元测试)
关于异常的说明
此类不显式测试秒表描述符是否存在于字典中。字典已经进行了此测试,如果描述符不存在于字典中,您将从字典中获得一个异常,而不是从秒表类中获得。
单元测试
有三个单元测试验证了 DebugStopwatch
类的基本功能,并验证了不同类型的枚举即使值相同也确实是不同的。这些测试依赖于我的 单元测试应用程序 的排序功能。
鉴于
public class DebugStopwatchTests
{
protected enum Items1
{
Avi,
Mpg,
}
protected enum Items2
{
Dvd,
}
protected int[] delays = new int[] { 100, 200, 300 };
protected string[] sampleNames1 = new string[] { "A1", "A2", "A3" };
protected string[] sampleNames2 = new string[] { "D1", "D2", "D3" };
protected string[] sampleNames3 = new string[] { "M1", "M2", "M3" };
第一个单元测试验证了样本及其增量
/// <summary>
/// Tests managing samples for a single enum type.
/// </summary>
[Test, Sequence(0)]
public void SampleTest1()
{
DebugStopwatch.Start(Items1.Avi);
for (int i = 0; i < 3; i++)
{
Thread.Sleep(delays[i]);
DebugStopwatch.Sample(Items1.Avi, sampleNames1[i]);
}
List<SampleDelta> samples = null;
DebugStopwatch.GetSampleDeltas(Items1.Avi, ref samples);
Assertion.Assert(samples.Count == 3, "Expected 3 samples.");
for (int i = 0; i < 3; i++)
{
Assertion.Assert(InRange(samples[i].DeltaMilliseconds, delays[i]),
"Sample not within expected range.");
Assertion.Assert(samples[i].SampleName == sampleNames1[i],
"Sample name does not match.");
}
}
第二个单元测试验证了为 Items2.Dvd
(值为 0)获取的样本与为 Items1.Avi
(值也为 0)获取的样本是不同的
/// <summary>
/// Tests that there is not conflict between
/// the previous sample enum and a different one,
/// even though the enumerations begin at the same values.
/// </summary>
[Test, Sequence(1)]
public void SampleTest2()
{
DebugStopwatch.Start(Items2.Dvd);
for (int i = 0; i < 3; i++)
{
Thread.Sleep(delays[i]);
DebugStopwatch.Sample(Items2.Dvd, sampleNames2[i]);
}
List<SampleDelta> samples = null;
DebugStopwatch.GetSampleDeltas(Items2.Dvd, ref samples);
Assertion.Assert(samples.Count == 3, "Expected 3 samples.");
for (int i = 0; i < 3; i++)
{
Assertion.Assert(InRange(samples[i].DeltaMilliseconds, delays[i]),
"Sample not within expected range.");
Assertion.Assert(samples[i].SampleName == sampleNames2[i],
"Sample name does not match.");
}
}
最后一个单元测试验证了 Items1.Mpg
(值为 1)创建的样本列表与 Items.Avi
(值为 0)创建的样本列表是不同的
/// <summary>
/// Tests that there is not conflict between
/// the first sample enum and a second enumeration value.
/// </summary>
[Test, Sequence(3)]
public void SampleTest3()
{
DebugStopwatch.Start(Items1.Mpg);
for (int i = 0; i < 3; i++)
{
Thread.Sleep(delays[i]);
DebugStopwatch.Sample(Items1.Mpg, sampleNames3[i]);
}
List<SampleDelta> samples = null;
DebugStopwatch.GetSampleDeltas(Items1.Mpg, ref samples);
Assertion.Assert(samples.Count == 3, "Expected 3 samples.");
for (int i = 0; i < 3; i++)
{
Assertion.Assert(InRange(samples[i].DeltaMilliseconds, delays[i]),
"Sample not within expected range.");
Assertion.Assert(samples[i].SampleName == sampleNames3[i],
"Sample name does not match.");
}
DebugStopwatch.DumpSamples(Items1.Mpg);
}
结论
希望您发现此类对于添加调试版本时间采样很有用,而且您不会觉得必须使用 ref
变量来返回经过时间和时间样本有些烦人。