65.9K
CodeProject 正在变化。 阅读更多。
Home

调试秒表

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.42/5 (8投票s)

2007年12月15日

CPOL

6分钟阅读

viewsIcon

49385

downloadIcon

247

一个调试版本秒表,可用于诊断计时。

Screenshot -

问题

有时,需要对某个操作进行时间采样,但仅作为调试版本中的诊断。而且,虽然 .NET 2.0 中有一个出色的 Stopwatch 类,但该类不适用于非诊断时间采样,因为它

  1. 不是静态类,并且
  2. 因此不支持条件调试版本。

解决方案

解决方案是将 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.SendDSL.Send,即使这两个枚举的值都为 1。

关于 internal 关键字,我并不完全满意 C# 不允许在静态类中使用 protected 成员。internal 关键字阻止从不同程序集访问成员,但我真正想做的是阻止甚至从同一程序集访问。通过将类标记为 static,这很有用,因为它能防止有人意外地实例化它,我却失去了我认为重要的对我的静态类成员可见性的控制——它们必须是 publicinternal

在映射中创建条目

如果字典中当前不存在秒表描述符,则 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();
}

StartNewReset 之间的显着区别在于,Reset 仅清除样本收集并重置秒表,而 StartNew 是依次调用 ResetStart 的一种更简单的方法。

为什么在 StartNewReset 方法中的 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 变量来返回经过时间和时间样本有些烦人。

© . All rights reserved.