第13章 诊断

第13章 诊断

13.1 条件编译

预编译的指令见 4.16 预处理指令,我们这里的条件编译用到的指令有:

  • #if​​、#else​​、#endif​​、#elif​​

    条件编译指令可以进行 && ​、 || ​、 ! 运算。

预定义指令可以通过三种方式定义:

  1. 在文件中通过 #define 定义 Symbol
  2. 编译时传递 /define 参数
csc Program.cs /define:TESTMODE,PLAYMODE
  1. Visual Studio 在项目属性中定义编译 Symbol

13.1.2 Conditional​ 特性

Conditional​ 特性用法如下:

[Conditional ("LOGGINGMODE")]
static void LogStatus (string msg)
{
    string logFilePath = ...
    System.IO.File.AppendAllText (logFilePath, msg + "\r\n");
}

Conditional​ 特性指示编译器在所有调用 LogStatus​ 的地方使用 #if LOGGINGMODE ​ 进行包装,如果 LOGGINGMODE​ 没有定义,则 LogStatus​ 的调用在编译时将完全忽略,包括参数表达式的计算。即使 LogStatus​ 和调用者 不在同一程序集 该机制仍然有效。

C7.0 核心技术指南 第7版.pdf - p578 - C7.0 核心技术指南 第 7 版-P578-20240214151621

#if​ 相较 Conditional​ 的不足:

C7.0 核心技术指南 第7版.pdf - p577 - C7.0 核心技术指南 第 7 版-P577-20240214151902

13.2 Debug​ 和 Trace​ 类

Debug​ 和 Trace​ 类的方法都被 Conditional​ 进行了标注,区别如下:

  • 所有 Debug​ ​类中的方法都标记为 [Conditional("DEBUG")] ​​。
  • 所有 Trace​ ​类中的方法都标记为 [Conditional("TRACE")] ​​。

默认情况下,VS 在项目的 debug 配置中定义了 ** DEBUG ** 和 ** TRACE ** 符号,在 release 中仅定义了 ** TRACE ** 符号。

13.2.1 Fail​ 和 Assert​ 方法

Debug​ 和 Trace​ 类型都提供了 Fail ​和 Assert ​方法,区别如下:

  • Fail

    调用时将弹出对话框,询问“忽略”、“终止”还是“重试”。

  • Assert

    Assert​ 中的表达式返回 false​(意味着程序缺陷),将会自动调用 Fail​ 方法。

从技术上说,根据条件抛出异常也是一种断言,区别如下:

  • 抛出异常:反映了 调用者代码 缺陷
  • Assert​:反映了 当前方法的 缺陷

13.2.2 TraceListener​ 类

Debug​ 和 Trace​ 类的 Listeners​ 属性是 TraceListener ​ 实例的静态集合。它们负责处理 Write​、Fail​ 和 Trace​ 方法触发的信息。

默认情况下,该集合包含一个单独的监听器( DefaultTraceListener ​)。可以手动添加自定义监听器、移除默认监听器,以改变监听行为。既可以从零开始编写 TraceListener​(从 TraceListener​ 继承),也可使用以下预定义的监听器类型:

  1. TextWriterTraceListener ​​:将消息写入 Stream​ ​或者 TextWriter​​,或将消息追加到文件中。

    TextWriterTraceListener​ 类还进一步的划分为了:

    1. ConsoleTraceListener​​、
    2. DelimitedListTraceListener​​、
    3. XmlWriterTraceListener​​
    4. EventSchemaTraceListener​​.
  2. EventLogTraceListener ​​:将事件日志写入到 Windows 事件日志中。

  3. EventProviderTraceListener ​​:将事件写入操作系统的 Windows 事件追踪(EventTracingforWindows,ETW)子系统中(支持 WindowsVista 及其之后的操作系统)。

  4. WebPageTraceListener​​:写入某个 ASP.NET 页面。

C7.0 核心技术指南 第7版.pdf - p581 - C7.0 核心技术指南 第 7 版-P581-20240215094613

提纲
TraceListener预定义DefaultTraceListenerTextWriterTraceListenerEventLogTraceListenerEventProviderTraceListenerWebPageTraceListener常用成员FilterIndentLevelIndentSizeTraceOutputOptionsAutoFlushFlush()Close()
DefaultTraceListener

这个默认的监听器有两个关键特性:

  1. 当连接到调试器时(例如 Visual Studio 的调试器),将消息输出到 调试输出窗口 ;否则忽略消息内容。
  2. 当调用 Fail​ 方法时(或 Assert​ 失败时),不论是否附加了调试器,都 弹出对话框询问用户是继续、忽略还是重试(附加/调试)
EventLogTraceListener

该监听器将信息输出至 Windows 事件日志 ,不同的方法将输出不同类型的日志:

  1. 信息:

    Write​、Fail​、Assert

  2. 警告:

    TraceWarning

  3. 错误:

    TraceError

使用方式
Trace.Listeners.Clear();
Trace.Listeners.Add(new TextWriterTraceListener("D:\\trace.txt"));
TextWriter tw = Console.Out;
Trace.Listeners.Add(new TextWriterTraceListener(tw));
// 如下代码需用管理员权限运行
if (!EventLog.SourceExists("WinFormsApp1"))
    EventLog.CreateEventSource("WinFormsApp1", "Application");
Trace.Listeners.Add(new EventLogTraceListener("WinFormsApp1"));
TraceListener​ 的属性

TraceListener ​有如下属性进行自定义行为:

  1. Filter​​ ​属性

    TraceFilter​​ 类型,消息过滤器。可以使用其预定义子类(例如 EventTypeFilter​​、SourceFilter​​),也可以派生 TraceFilter​​ 并重写 ShouldTrace​​ 方法。

  2. IndentLevel​​、IndentSize​ ​属性

    用于 控制缩进

  3. TraceOutputOptions​ ​属性

    可以用来 写入额外的数据 ,调用 Trace ​​​ 方法时会应用 TraceOutputOptions​​​ 中的配置:

    TextWriterTraceListener tl = new TextWriterTraceListener (Console.Out);
    tl.TraceOutputOptions = TraceOptions.DateTime | TraceOptions.Callstack;
    ...
    Trace.TraceWarning("Orange alert");
    // 输出:
    // DiagTest.vshost.exe Warning: O : Orange alert
    //        DateTime=2007-03-08T05:57:13.6250000Z
    //        Callstack=    at System.Environment.GetStackTrace(Exception e,BooleanneedFileInfo)
    //        at System.Environment.get_StackTrace()    at..
    

13.2.3 刷新并关闭监听器

TextWriterTraceListener​ 会先缓存再写入,需要适时关闭或刷新监听器。方法有三:

  1. 使用 Debug(Trace).Flush ​ ​方法刷新监听器

  2. 关闭程序时调用 Debug(Trace).Close ​ ​方法

    该方法会隐式调用 Flush ​方法,并关闭文件句柄。

  3. 设置 Debut(Trace).AutoFlush ​ ​属性为 true

    每条消息后强制执行 Flush ​方法。

C7.0 核心技术指南 第7版.pdf - p582 - C7.0 核心技术指南 第 7 版-P582-20240215171006

13.3 调试器的集成

13.3.1 附加和断点

System.Diagnostics ​命名空间中的静态 Debugger ​类提供了与调试器交互的基本函数:

  1. Break

    断点。启动调试器并将进程附加在调试器上,并在当前执行点挂起。

  2. Launch

    除挂起外,和 Break ​无区别。

  3. Log

    向调试器窗口输出信息。

  4. IsAttached

    是否已有调试器附加到了当前应用程序上。

13.3.2 Debugger​ 特性

DebuggerStepThrough ​和 DebuggerHidden ​特性可以应用于方法构造器、和

  • DebuggerStepThrough

    允许调试器在 单步执行 时跳过某些代码,但如果在该代码中发生异常,调试器 仍会停留

  • DebuggerHidden

    调试器完全忽略这段代码,即使发生异常 也不停留

13.4 进程与线程处理

13.4.1 检查运行中的进程

Process.GetProcessXXX ​ ​方法可以获得相应进程(包含托管、非托管进程)。每一个 Process​ ​实例都拥有诸多属性:名称、ID、优先级、内存、处理器利用率、窗口句柄等。

Process.GetCurrentProcess ​ ​方法返回当前的进程。如果创建了额外的应用程序域,它们将共享同一个进程。

如果需要终止一个进程,可以调用 Kill ​方法。

13.4.2 在进程中检查线程

Process.Threads ​ ​属性可用于枚举其他进程内的所有线程,为 ProcessThread​ ​类型。该对象用于管理而非执行同步任务。ProcessThread​ ​对象提供了相应线程的诊断信息,并允许对某些属性进行控制,例如优先级和处理器亲和性。

13.5 StackTrace​ 和 StackFrame​ 类

StackTrace​ ​和 StackFrame​ ​类提供了执行调用栈的只读视图,主要用于诊断目的。StackTrace​ ​代表了 一个完整的调用栈 ,而 StackFrame​ ​代表了 调用栈中的一个单独的方法调用

获取当前线程调用栈(StackTrace​)的快照,方式有二:

  1. 使用 StackTrace 的无参构造器。

  2. 使用 StackTrace bool 参数构造器。

    若参数传入 true​,且存在 pdb 文件,StackTrace​ 将读取文件名、行号等数据。

获取调用帧(StackFrame​),方法有二:

  • StackTrace.GetFrame ​​

    获取特定调用帧

  • StackTrace.GetFrames ​​

    获取所有调用帧

获取整个 StackTrace ​基本信息的最简单的方法是调用 ToString ​方法。当然也可以手动操作,方式如下:

static void Main() { A(); }
static void A() { B(); }
static void B() { C(); }
static void C()
{
    StackTrace s = new StackTrace(true);
    Console.WriteLine("Total frames:   " + s.FrameCount);
    Console.WriteLine("Current method: " + s.GetFrame(0).GetMethod().Name);
    Console.WriteLine("Calling method: " + s.GetFrame(1).GetMethod().Name);
    Console.WriteLine("Entry method:   " + s.GetFrame(s.FrameCount - 1).GetMethod().Name);

    Console.WriteLine("Call Stack:");
    foreach (var f in s.GetFrames())
    {
        Console.WriteLine(
        "  File:   " + f.GetFileName() +
        "  Line:   " + f.GetFileLineNumber() +
        "  Col:    " + f.GetFileColumnNumber() +
        "  Offset: " + f.GetILOffset() +
        "  Method: " + f.GetMethod().Name);
    }
}

其输出如下:

C7.0 核心技术指南 第7版.pdf - p585 - C7.0 核心技术指南 第 7 版-P585-20240215181746

C7.0 核心技术指南 第7版.pdf - p586 - C7.0 核心技术指南 第 7 版-P586-20240215224138

另见22.10 Suspend 和 Resume 方法,获取线程的调用栈信息

StackTrace stackTrace = null;
targetThread.Suspend();
try { stackTrace = new StackTrace (targetThread, true); }
finally { targetThread.Resume(); }

13.6 Windows 事件日志

标准 Windows 事件日志有三种,按名称分为:

  1. 应用程序
  2. 系统
  3. 安全

应用程序日志是大多数应用程序通常写入日志的地方。

13.6.1 写入事件日志

写入前需创建相应的日志源,创建后,调用 EventLog.WriteEntry​ 写入时需提供日志名称、源名称和消息数据

image

使用方式如下:

const string SourceName = "WinFormsApp1";
if (!EventLog.SourceExists(SourceName))
    EventLog.CreateEventSource(SourceName, "Application");
EventLog.WriteEntry(SourceName, "测试Windows日志", EventLogEntryType.Information);

13.6.2 读取事件日志

步骤如下:

  1. 传入日志名称(三种日志名称之一),实例化 EventLog​ 对象
  2. 指定日志所在计算机的名称(可选)
  3. 通过 Entries ​集合读取日志

使用方式如下:

EventLog log = new EventLog("Application");

Console.WriteLine(log.Entries.Count);

EventLogEntry last = log.Entries[log.Entries.Count - 1];
Console.WriteLine("Index:   " + last.Index);
Console.WriteLine("Source:  " + last.Source);
Console.WriteLine("Type:    " + last.EntryType);
Console.WriteLine("Time:    " + last.TimeWritten);
Console.WriteLine("Message: " + last.Message);

可以使用静态方法 EventLog.GetEventLogs​​(需要管理员权限)来枚举当前(或者其他)计算机的所有日志名称,以下代码通常情况下至少会打印“应用程序”、“安全”以及“系统”:

foreach (EventLog log in EventLog.GetEventLogs()) {
    Console.WriteLine(log.LogDisplayName);
}

13.6.3 监视事件日志

EntryWritten ​事件相当于一个钩子,可以在 Windows 事件日志被写入时获得通知。使用步骤如下:

  1. 实例化 EventLog ​并将它的 EnableRaisingEvents ​属性设置为 true
  2. 处理 EntryWritten ​事件。

代码如下:

static void Main()
{
    using (var log = new EventLog("Application"))
    {
        log.EnableRaisingEvents = true;
        log.EntryWritten += DisplayEntry;
        Console.ReadLine();
    }
}

static void DisplayEntry(object sender, EntryWrittenEventArgs e)
{
    EventLogEntry entry = e.Entry;
    Console.WriteLine(entry.Message);
}

Info

仅 .NET Framework 原生支持。

13.7 性能计数器

Windows 日志用于获取信息进行事后分析,性能计数器则提供运行时监控状态的能力。用本节提到的 API,我们可用在软件上显示 CPU 利用率、内存使用情况等。

image

13.7.1 遍历可用的计数器

以下示例将遍历计算机上的所有可用性能计数器。对于那些支持实例的计数器,则遍历每一个实例的计数器:

var cats = PerformanceCounterCategory.GetCategories();
foreach (var cat in cats) {
    Console.WriteLine("Category: " + cat.CategoryName);

    var instances = cat.GetInstanceNames();
    // 不支持实例的计数器,如CPU核心
    if(instances.Length == 0) {
        foreach(var ctr in cat.GetCounters())
            Console.WriteLine("  Counter: " + ctr.CounterName);
    }
    else {
    // 支持实例的计数器,如应用程序进程
        foreach (var instance in instances) {
            Console.WriteLine("  Instance: " + instance);
            if (cat.InstanceExists(instance))
                foreach (var ctr in cat.GetCounters(instance))
                    Console.WriteLine("    Counter: " + ctr.CounterName);
        }
    }
}

性能计数器类别(Performance Counter Categories)

性能计数器类别是一组逻辑相关的性能计数器的集合。例如,“Processor​”是一个性能计数器类别,它包含了与 CPU 性能相关的计数器,如 CPU 的利用率。

实例(Instances)

某些性能计数器类别下的计数器可以有多个实例。实例通常对应于系统中的资源或对象,例如,在 “Processor​” 类别下,每个 CPU 核心可能是一个实例;在 “Process​” 类别下,每个运行中的进程都是一个实例。

性能计数器(Counters)

在给定的类别下,性能计数器是实际测量的指标。例如,在 “Processor​” 类别下,可能有 “% Processor Time”(处理器时间百分比)这样的计数器。

关于每个进程的性能计数器

不是每个进程都包含自己的性能计数器类别,而是某些性能计数器类别(如 “Process​”)包含了多个实例,每个实例对应于一个进程。这些实例下的计数器反映了该进程的性能指标,如 CPU 使用率、内存使用量等。因此,通过这种方式,你可以监控每个进程的性能。

13.7.2 检索(查看)性能计数器

查看性能计数器的方式如下:

  1. 实例化 PerformanceCounter ​对象

  2. 调用 NextValue ​或者 NextSample ​方法。

    • NextValue ​返回简单的 float ​值;
    • NextSample ​返回 CounterSample ​对象,该对象包含高级属性,例如 CounterFrequency​、TimeStamp​、BaseValue ​以及 RawValue​。

下面列举一些简单的用法:

获取 CPU 整体使用率
using (PerformanceCounter pc = new PerformanceCounter("Processor", "% Processor Time", "_Total"))
    Console.WriteLine(pc.NextValue());
获取当前进程内存消耗
var procName = Process.GetCurrentProcess().ProcessName;
using (PerformanceCounter pc = new PerformanceCounter("Process", "Private Bytes", procName))
    Console.WriteLine(pc.NextValue());

PerformanceCounter ​并没有公开 ValueChanged ​事件,因此如果需要监视各种变化则必须使用轮询的方法。

13.7.3 创建计数器并写入性能数据

自定义计数器常见场景有:

  1. 应用性能监控

    开发者可以为自己的应用程序创建自定义的性能计数器来监控关键操作的性能,例如,数据库查询的平均响应时间、每秒处理的事务数、队列长度等。这些数据可以帮助开发者了解应用程序在实际运行中的性能状况,并及时发现性能瓶颈。

  2. 系统健康检查

  3. 动态性能调优

  4. 软件测试和质量保证

  5. 安全监控

如下实例代码演示了如何创建分组该分组下的所有计数器

var category = "Nutshell Monitoring";

var eatenPerMin = "Macadamias eaten so far";
var tooHard = "Macadamias deemed too hard";
if (!PerformanceCounterCategory.Exists(category))
{
    var cd = new CounterCreationDataCollection();

    cd.Add(new CounterCreationData(eatenPerMin,
        "Number of macadamias consumed, including shelling time",
        PerformanceCounterType.NumberOfItems32));

    cd.Add(new CounterCreationData(tooHard,
        "Number of macadamias that will not crack, despite much effort",
        PerformanceCounterType.NumberOfItems32));

    PerformanceCounterCategory.Create(category, "Test Category", PerformanceCounterCategoryType.SingleInstance, cd);
}

image

如果之后希望在该分组下添加更多的性能计数器,必须先调用 PerformanceCounterCategory.Delete​ 方法删除旧的分组。

C7.0 核心技术指南 第7版.pdf - p593 - C7.0 核心技术指南 第 7 版-P593-20240216163440

一旦性能计数器创建完成,就可以实例化 PerformanceCounter​,将 ReadOnly ​属性设置为 false​,并对 RawValue ​属性赋值来更新计数器的值。也可以使用 Increament ​和 IncreamentBy ​方法来更新现有的值:

var category = "Nutshell Monitoring";
var eatenPerMin = "Macadamias eaten so far";

using(PerformanceCounter pc = new PerformanceCounter(category, eatenPerMin, ""))
{
    pc.ReadOnly = false;
    pc.RawValue = 1000;
    pc.Increment();
    pc.IncrementBy(10);
    Console.WriteLine(pc.NextValue());
}

image

13.8 Stopwatch​ 类

此处简单介绍几个Stopwatch​属性:

  • ElapsedTicks

    long​ 类型,返回计数值。

  • Stopwatch.Frequency

    long 类型,表示计数频率。ElapsedTicks​ 除以该值可用得到对应的秒数。

可以直接通过 Stopwatch.ElapsedMilliseconds​ 属性获得用时,更为简便。

posted @   hihaojie  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示