龙空技术网

c# 10 教程:13 诊断

启辰8 49

前言:

此刻各位老铁们对“ubuntu fftw3”可能比较看重,同学们都想要知道一些“ubuntu fftw3”的相关内容。那么小编也在网上汇集了一些关于“ubuntu fftw3””的相关内容,希望你们能喜欢,朋友们快快来学习一下吧!

当出现问题时,重要的是提供信息来帮助诊断问题。集成开发环境 (IDE) 或调试器可以极大地帮助实现此效果,但它通常仅在开发期间可用。应用程序交付后,应用程序本身必须收集和记录诊断信息。为了满足此要求,.NET 提供了一组工具来记录诊断信息、监视应用程序行为、检测运行时错误以及与调试工具(如果可用)集成。

某些诊断工具和 API 是特定于 Windows 的,因为它们依赖于 Windows 操作系统的功能。为了防止特定于平台的 API 使 .NET BCL 混乱,Microsoft 已将它们放在可以选择引用的单独 NuGet 中。有十几个特定于Windows的软件包,您可以使用 “master”软件包一次引用所有软件包。

本章中的类型主要在 System.Diagnostics 中定义。

条件编译

可以使用有条件地编译 C# 中的任何代码部分。预处理器指令是编译器的特殊指令,以 # 符号开头(与其他 C# 构造不同,它必须出现在自己的行上)。从逻辑上讲,它们在主编译发生之前执行(尽管在实践中,编译器在词法解析阶段处理它们)。条件编译的预处理器指令是 #if、#else、#endif 和 #elif。

#if 指令指示编译器忽略一段代码,除非定义了指定的。可以使用 #define 指令在源代码中定义符号(在这种情况下,符号仅适用于该文件),也可以使用 <DefineConstants> 元素在 文件中定义符号(在这种情况下,符号应用于整个程序集):

#define TESTMODE            // #define directives must be at top of file                            // Symbol names are uppercase by convention.using System;class Program{  static void Main()  {#if TESTMODE    Console.WriteLine ("in test mode!");     // OUTPUT: in test mode!#endif  }}

如果我们删除第一行,程序将使用从可执行文件中完全删除的 Console.WriteLine 语句进行编译,就好像它被注释掉了一样。

#else 语句类似于 C# 的 else 语句,#elif 等效于 #else 后跟 #if 。的 ||、 && 和 !运算符执行 和 :

#if TESTMODE && !PLAYMODE      // if TESTMODE and not PLAYMODE  ...

但请记住,您不是在构建普通的 C# 表达式,并且您操作的符号与(静态或其他)完全没有联系。

可以通过编辑 文件(或在 Visual Studio 中转到“项目属性”窗口中的“生成”选项卡)来定义应用于程序集中每个文件的符号。下面定义了两个常量,测试模式和播放模式:

<PropertyGroup>  <DefineConstants>TESTMODE;PLAYMODE</DefineConstants></PropertyGroup>

如果在程序集级别定义了一个符号,然后想要为特定文件“取消定义”它,则可以使用 #undef 指令执行此操作。

条件编译与静态变量标志

您可以改为使用简单的静态字段实现前面的示例:

static internal bool TestMode = true;static void Main(){  if (TestMode) Console.WriteLine ("in test mode!");}

这具有允许运行时配置的优点。那么,为什么选择条件编译呢?原因是条件编译可以带你去变量标志不能的地方,例如:

有条件地包含属性更改声明的变量类型在 using 指令中的不同命名空间或类型别名之间切换;例如:using TestType = #if V2 MyCompany.Widgets.GadgetV2; #else MyCompany.Widgets.Gadget; #endif

您甚至可以在条件编译指令下执行重大重构,因此您可以立即在新旧版本之间切换,并编写可针对多个运行时版本进行编译的库,利用可用的最新功能。

条件编译的另一个优点是调试代码可以引用程序集中未包含在部署中的类型。

条件属性

Condition 属性指示编译器忽略对特定类或方法的任何调用(如果尚未定义指定的符号)。

若要了解这有何用处,假设您编写了一个用于记录状态信息的方法,如下所示:

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

现在假设您希望仅在定义了 LOGGINGMODE 符号时才执行此操作。第一个解决方案是对 LogStatus 的所有调用都围绕一个 #if 指令:

#if LOGGINGMODELogStatus ("Message Headers: " + GetMsgHeaders());#endif

这给出了一个理想的结果,但它很乏味。第二种解决方案是将 #if 指令放在 LogStatus 方法中。但是,如果按如下方式调用 ,这是有问题的:

LogStatus ("Message Headers: " + GetComplexMessageHeaders());

GetComplexMessageHeaders 将始终被调用,这可能会导致性能下降。

我们可以通过将条件属性(在 System.Diagnostics 中定义)附加到 LogStatus 方法,将第一个解决方案的功能与第二个解决方案的便利性结合起来:

[Conditional ("LOGGINGMODE")]static void LogStatus (string msg){  ...}

这将指示编译器将对 LogStatus 的调用视为包装在 #if LOGGINGMODE 指令中。如果未定义该符号,则在编译中完全消除对 LogStatus 的任何调用,包括其参数计算表达式。(因此,将绕过任何副作用表达式。即使 LogStatus 和调用方位于不同的程序集中,这也有效。

注意

[条件] 的另一个好处是,条件性检查是在编译调用时执行的,而不是在编译时执行的。这是有益的,因为它允许您编写包含 LogStatus 等方法的库,并且只构建该库的一个版本。

Conditional 属性在运行时被忽略,它纯粹是对编译器的指令。

条件属性的替代项

如果需要在运行时动态启用或禁用功能,则 Condition 属性毫无用处:相反,必须使用基于变量的方法。这就留下了一个问题,即在调用条件日志记录方法时如何优雅地规避参数的计算。函数式方法可以解决此问题:

using System;using System.Linq;class Program{  public static bool EnableLogging;  static void LogStatus (Func<string> message)  {    string logFilePath = ...    if (EnableLogging)      System.IO.File.AppendAllText (logFilePath, message() + "\r\n");  }}

lambda 表达式允许您调用此方法而不会造成语法膨胀:

LogStatus ( () => "Message Headers: " + GetComplexMessageHeaders() );

如果 EnableLogging 为 false,则永远不会计算 GetComplexMessageHeaders。

调试和跟踪类

调试和跟踪是提供基本日志记录和断言功能的静态类。这两个类非常相似;主要区别在于它们的预期用途。Debug 类用于调试版本;Trace 类适用于调试和发布版本。为此:

All methods of the Debug class are defined with [Conditional("DEBUG")].All methods of the Trace class are defined with [Conditional("TRACE")].

这意味着编译器将消除对调试或跟踪所做的所有调用,除非您定义了 DEBUG 或 TRACE 符号。(Visual Studio 提供了用于在“项目属性”的“生成”选项卡中定义这些符号的复选框,默认情况下,新项目启用 TRACE 符号。

Debug 和 Trace 类都提供 Write、WriteLine 和 WriteIf 方法。默认情况下,这些消息将消息发送到调试器的输出窗口:

Debug.Write     ("Data");Debug.WriteLine (23 * 34);int x = 5, y = 3;Debug.WriteIf   (x > y, "x is greater than y");

Trace 类还提供了 TraceInformation 、TraceWarning 和 TraceError 等方法。这些方法和 Write 方法之间的行为差异取决于活动的 TraceListener(我们在中对此进行了介绍)。

失败和断言

调试类和跟踪类都提供 Fail 和断言方法。Fail 将消息发送到调试或跟踪类的侦听器集合中的每个 TraceListener(请参阅下一节),默认情况下,该集合将消息写入调试输出:

Debug.Fail ("File data.txt does not exist!");

如果 bool 参数为 false,则 Assert 只是调用 Fail,这称为,并在违反时指示代码中的错误。指定失败消息是可选的:

Debug.Assert (File.Exists ("data.txt"), "File data.txt does not exist!");var result = ...Debug.Assert (result != null);

写入、失败和断言方法也会重载,以接受除消息之外的字符串类别,这在处理输出时很有用。

断言的替代方法是在相反条件为真时引发异常。这是验证方法参数时的常见做法:

public void ShowMessage (string message){  if (message == null) throw new ArgumentNullException ("message");  ...}

此类“断言”是无条件编译的,灵活性较低,因为您无法通过 TraceListener 控制断言失败的结果。从技术上讲,它们不是断言。断言是如果违反,则指示当前方法代码中的错误。基于参数验证引发异常表示代码中存在 bug。

跟踪侦听器

Trace 类具有一个静态 Listeners 属性,该属性返回 TraceListener 实例的集合。它们负责处理由写入、失败和跟踪方法发出的内容。

默认情况下,每个侦听器集合都包含一个侦听器 ( 默认跟踪侦听器 )。默认侦听器具有两个主要功能:

连接到调试器(如 Visual Studio)时,消息将写入调试输出窗口;否则,将忽略消息内容。调用 Fail 方法(或断言失败)时,应用程序将。

可以通过(可选)删除默认侦听器,然后添加一个或多个自己的侦听器来更改此行为。您可以从头开始编写跟踪侦听器(通过子类化 TraceListener ),也可以使用预定义的类型之一:

TextWriterTraceListener 写入 Stream 或 TextWriter 或追加到文件。EventLogTraceListener 写入 Windows 事件日志(仅限 Windows)。EventProviderTraceListener 写入 Windows 事件跟踪 (ETW) 子系统(跨平台支持)。

TextWriterTraceListener 进一步子类化为 ConsoleTraceListener、 、XmlWriterTraceListener 和 EventSchemaTraceListener。

下面的示例清除 Trace 的默认侦听器,然后添加三个侦听器,一个追加到文件,一个写入控制台,另一个写入 Windows 事件日志:

// Clear the default listener:Trace.Listeners.Clear();// Add a writer that appends to the trace.txt file:Trace.Listeners.Add (new TextWriterTraceListener ("trace.txt"));// Obtain the Console's output stream, then add that as a listener:System.IO.TextWriter tw = Console.Out;Trace.Listeners.Add (new TextWriterTraceListener (tw));// Set up a Windows Event log source and then create/add listener.// CreateEventSource requires administrative elevation, so this would// typically be done in application setup.if (!EventLog.SourceExists ("DemoApp"))  EventLog.CreateEventSource ("DemoApp", "Application");Trace.Listeners.Add (new EventLogTraceListener ("DemoApp"));

对于 Windows 事件日志,使用 写入 、 失败 或 断言 方法编写的消息始终在 Windows 事件查看器中显示为“信息”消息。但是,通过 TraceWarning 和 TraceError 方法编写的消息将显示为警告或错误。

TraceListener 还具有 TraceFilter 类型的筛选器,您可以设置该筛选器以控制是否将消息写入该侦听器。为此,您可以实例化预定义的子类之一( 事件类型筛选器 或 源筛选器 ),或者子类 TraceFilter 并重写 ShouldTrace 方法。例如,您可以使用它按类别进行过滤。

TraceListener 还定义了用于控制缩进的 IndentLevel 和 IndentSize 属性,以及用于写入额外数据的 TraceOutputOptions 属性:

TextWriterTraceListener tl = new TextWriterTraceListener (Console.Out);tl.TraceOutputOptions = TraceOptions.DateTime | TraceOptions.Callstack;

使用 Trace 方法时应用跟踪输出选项:

Trace.TraceWarning ("Orange alert");DiagTest.vshost.exe Warning: 0 : Orange alert     DateTime=2007-03-08T05:57:13.6250000Z     Callstack=   at System.Environment.GetStackTrace(Exception e, BooleanneedFileInfo)     at System.Environment.get_StackTrace()     at ...
刷新和关闭侦听器

某些侦听器(如 TextWriterTraceListener)最终会写入受缓存影响的流。这有两个含义:

消息可能不会立即显示在输出流或文件中。您必须在应用程序结束之前关闭(或至少刷新)侦听器;否则,您将丢失缓存中的内容(默认情况下,如果您正在写入文件,则最多为 4 KB)。

Trace 和 Debug 类提供静态的 Close 和 Flush 方法,这些方法在所有侦听器上调用 Close 或 Flush(而侦听器又在任何基础器和流上调用 Close 或 Flush)。Close 隐式调用 Flush ,关闭文件句柄,并防止写入进一步的数据。

作为一般规则,在应用程序结束之前调用 Close,并随时调用 Flush 以确保写入当前消息数据。这适用于使用基于流或文件的侦听器。

跟踪和调试还提供自动刷新属性,如果为 true,则在每条消息后强制刷新。

注意

如果使用任何基于文件或流的侦听器,则最好在“调试和跟踪”上将自动刷新设置为 true。否则,如果发生未经处理的异常或严重错误,最后 4 KB 的诊断信息可能会丢失。

调试器集成

有时,应用程序与调试器交互(如果可用)很有用。在开发过程中,调试器通常是您的IDE(例如,Visual Studio);在部署中,调试器更可能是较低级别的调试工具之一,例如 WinDbg、Cordbg 或 MDbg。

连接和断开

System.Diagnostics中的静态调试器类提供了与调试器交互的基本函数,即中断、启动、日志和IsAttached 。

调试器必须首先附加到应用程序才能对其进行调试。如果从 IDE 中启动应用程序,则会自动执行此操作,除非您另有请求(通过选择“启动但不调试”)。但是,有时在 IDE 中以调试模式启动应用程序是不方便或不可能的。一个例子是Windows Service应用程序或(具有讽刺意味的)Visual Studio设计器。一种解决方案是正常启动应用程序,然后在 IDE 中选择“调试进程”。但是,这不允许在程序执行的早期设置断点。

解决方法是从应用程序内部调用 Debugger.Break。此方法启动调试器,附加到该调试器,并在该点挂起执行。(启动执行相同的操作,但不暂停执行。附加后,可以使用 Log 方法将消息直接记录到调试器的输出窗口。可以通过检查 IsAttached 属性来验证是否已附加到调试器。

调试器属性

DebuggerStepThrough 和 DebuggerHidden 属性向调试器提供有关如何处理特定方法、构造函数或类的单步执行的建议。

调试器单步执行请求调试器在没有任何用户交互的情况下单步执行函数。此属性在自动生成的方法和将实际工作转发到其他位置的方法的代理方法中很有用。在后一种情况下,如果在“real”方法中设置了断点,调试器仍将在调用堆栈中显示代理方法,除非还添加了 DebuggerHidden 属性。可以在代理上组合这两个属性,以帮助用户专注于调试应用程序逻辑而不是管道:

[DebuggerStepThrough, DebuggerHidden]void DoWorkProxy(){  // setup...  DoWork();  // teardown...}void DoWork() {...}   // Real method...
进程和进程线程

我们在的最后一节中描述了如何使用 Process.Start 启动一个新进程。Process 类还允许您查询在同一台或另一台计算机上运行的其他进程并与之交互。Process 类是 .NET 标准 2.0 的一部分,尽管其功能仅限于 UWP 平台。

检查正在运行的进程

这些方法按名称或进程 ID 检索特定进程,或检索当前计算机或指定计算机上运行的所有进程。这包括托管和非托管进程。每个 Process 实例都有丰富的属性映射统计信息,例如名称、ID、优先级、内存和处理器利用率、窗口句柄等。下面的示例枚举当前计算机上所有正在运行的进程:Process.GetProcessXXX

foreach (Process p in Process.GetProcesses())using (p){  Console.WriteLine (p.ProcessName);  Console.WriteLine ("   PID:      " + p.Id);  Console.WriteLine ("   Memory:   " + p.WorkingSet64);  Console.WriteLine ("   Threads:  " + p.Threads.Count);}

Process.GetCurrentProcess 返回当前进程。

可以通过调用进程的 Kill 方法来终止进程。

检查进程中的线程

还可以使用 Process.Threads 属性枚举其他进程的线程。但是,您获得的对象不是 System.Threading.Thread 对象;它们是 ProcessThread 对象,用于管理任务而不是同步任务。ProcessThread 对象提供有关基础线程的诊断信息,并允许您控制它的某些方面,例如其优先级和处理器关联性:

public void EnumerateThreads (Process p){  foreach (ProcessThread pt in p.Threads)  {    Console.WriteLine (pt.Id);    Console.WriteLine ("   State:    " + pt.ThreadState);    Console.WriteLine ("   Priority: " + pt.PriorityLevel);    Console.WriteLine ("   Started:  " + pt.StartTime);    Console.WriteLine ("   CPU time: " + pt.TotalProcessorTime);  }}
StackTrace 和 StackFrame

类提供执行调用堆栈的只读视图。您可以获取当前线程或异常对象的堆栈跟踪。此类信息主要用于诊断目的,尽管您也可以在编程(黑客)中使用它。堆栈跟踪表示一个完整的调用堆栈;StackFrame 表示该堆栈中的单个方法调用。

注意

如果只需要知道调用方法的名称和行号,则调用方信息属性可以提供更简单、更快捷的替代方法。我们将在第 的中介绍此主题。

如果实例化不带参数的 StackTrace 对象(或使用布尔参数),则会获得当前线程调用堆栈的快照。如果为 true,则 bool 参数指示 StackTrace 读取程序集(项目调试)文件(如果存在),从而使您能够访问文件名、行号和列偏移量数据。使用 /debug 开关进行编译时,将生成项目调试文件。(Visual Studio 使用此开关进行编译,除非您通过提出其他请求。

获得 StackTrace 后,您可以通过调用 GetFrame 来检查特定帧,或者使用 GetFrame 获取整个帧:

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 (StackFrame f in s.GetFrames())    Console.WriteLine (      "  File: "   + f.GetFileName() +      "  Line: "   + f.GetFileLineNumber() +      "  Col: "    + f.GetFileColumnNumber() +      "  Offset: " + f.GetILOffset() +      "  Method: " + f.GetMethod().Name);}

下面是输出:

Total frames:   4Current method: CCalling method: BEntry method: MainCall stack:  File: C:\Test\Program.cs  Line: 15  Col: 4  Offset: 7  Method: C  File: C:\Test\Program.cs  Line: 12  Col: 22  Offset: 6  Method: B  File: C:\Test\Program.cs  Line: 11  Col: 22  Offset: 6  Method: A  File: C:\Test\Program.cs  Line: 10  Col: 25  Offset: 6  Method: Main
注意

中间语言 (IL) 偏移量表示将执行的指令的偏移量,而不是当前正在执行的指令的偏移量。但是,奇怪的是,行号和列号(如果存在文件)通常指示实际的执行点。

发生这种情况是因为 CLR 在从 IL 偏移量计算行和列时会尽力实际执行点。编译器以这样一种方式发出 IL,包括将 nop(无操作)指令插入 IL 流中。

但是,在启用优化的情况下进行编译会禁用 nop 指令的插入,因此堆栈跟踪可能会显示要执行的下一条语句的行号和列号。由于优化可能会拉出其他技巧(包括折叠整个方法),因此进一步阻碍了获取有用的堆栈跟踪。

获取整个 StackTrace 基本信息的快捷方式是在其上调用 ToString。结果如下所示:

   at DebugTest.Program.C() in C:\Test\Program.cs:line 16   at DebugTest.Program.B() in C:\Test\Program.cs:line 12   at DebugTest.Program.A() in C:\Test\Program.cs:line 11   at DebugTest.Program.Main() in C:\Test\Program.cs:line 10

您还可以通过将异常传递到 StackTrace 的中来获取异常对象的堆栈跟踪(显示导致引发异常的原因)。

注意

异常已具有 StackTrace 属性;但是,此属性返回一个简单的字符串,而不是 StackTrace 对象。StackTrace 对象在记录部署后发生的异常(没有文件)时要有用得多,因为您可以记录 量来代替行号和列号。使用 IL 偏移量和 ,您可以查明方法中发生错误的位置。

视窗事件日志

Win32 平台以 Windows 事件日志的形式提供集中式日志记录机制。

我们之前使用的调试和跟踪类将写入 Windows 事件日志,如果您注册了 EventLogTraceListener 。但是,使用 EventLog 类,可以直接写入 Windows 事件日志,而无需使用 Trace 或 Debug 。还可以使用此类来读取和监视事件数据。

注意

写入 Windows 事件日志在 Windows 服务应用程序中是有意义的,因为如果出现问题,则无法弹出用户界面,将用户定向到已写入诊断信息的某些特殊文件。此外,由于服务写入 Windows 事件日志是常见的做法,因此,如果服务失败,管理员可能会首先查看此位置。

有三个标准的 Windows 事件日志,由以下名称标识:

应用系统安全

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

写入事件日志

写入 Windows 事件日志:

选择三个事件日志(通常是)之一。确定并在必要时创建它(创建需要管理权限)。使用日志名称、源名称和消息数据调用 EventLog.WriteEntry。

是应用程序的易于识别的名称。必须先注册源名称,然后才能使用它 - CreateEventSource 方法执行此功能。然后,您可以调用 WriteEntry :

const string SourceName = "MyCompany.WidgetServer";// CreateEventSource requires administrative permissions, so this would// typically be done in application setup.if (!EventLog.SourceExists (SourceName))  EventLog.CreateEventSource (SourceName, "Application");EventLog.WriteEntry (SourceName,  "Service started; using configuration file=...",  EventLogEntryType.Information);

事件日志条目类型可以是 信息 、 警告 、 错误 、 成功审核 或 失败审核 。每个事件在 Windows 事件查看器中都显示不同的图标。您还可以选择指定类别和事件 ID(每个都是您自己选择的数字)并提供可选的二进制数据。

CreateEventSource 还允许您指定计算机名称:如果您有足够的权限,则写入另一台计算机的事件日志。

读取事件日志

若要读取事件日志,请使用要访问的日志的名称和日志所在的另一台计算机的名称(可选)实例化 EventLog 类。然后,可以通过 Entries 集合属性读取每个日志条目:

EventLog log = new EventLog ("Application");Console.WriteLine ("Total entries: " + 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);

这通常至少打印。

监视事件日志

每当条目写入 Windows 事件日志时,都可以通过 EntryWrite 事件收到警报。这适用于本地计算机上的事件日志,无论哪个应用程序记录了事件,它都会触发。

要启用日志监控:

实例化事件日志并将其 EnableRaisingEvents 属性设置为 true 。处理 EntryWrite 事件。

例如:

using (var log = new EventLog ("Application")){  log.EnableRaisingEvents = true;  log.EntryWritten += DisplayEntry;  Console.ReadLine();}void DisplayEntry (object sender, EntryWrittenEventArgs e){  EventLogEntry entry = e.Entry;  Console.WriteLine (entry.Message);}
性能计数器注意

性能计数器是仅限 Windows 的功能,需要 NuGet 包 System.Diagnostics.PerformanceCounter 。如果您面向 Linux 或 macOS,请参阅了解替代方案。

到目前为止,我们讨论的日志记录机制对于捕获信息以供将来分析非常有用。但是,要深入了解应用程序(或整个系统)的当前状态,需要一种更实时的方法。满足此需求的 Win32 解决方案是性能监视基础结构,它由系统和应用程序公开的一组性能计数器以及用于实时监视这些计数器的Microsoft管理控制台 (MMC) 管理单元组成。

性能计数器分为“系统”、“处理器”、“.NET CLR 内存”等类别。这些类别有时也被 GUI 工具称为“性能对象”。每个类别对一组相关的性能计数器进行分组,这些性能计数器监视系统或应用程序的一个方面。“.NET CLR 内存”类别中的性能计数器示例包括“GC 中的时间百分比”、“所有堆中的 # 字节数”和“分配的字节/秒”。

每个类别可以选择具有一个或多个可以独立监视的实例。例如,这在“处理器”类别中的“处理器时间百分比”性能计数器中很有用,它允许监视 CPU 利用率。在多处理器计算机上,此计数器支持每个 CPU 的实例,允许您独立监视每个 CPU 的利用率。

以下各节演示如何执行通常需要的任务,例如确定公开的计数器、监视计数器以及创建自己的计数器以公开应用程序状态信息。

注意

读取性能计数器或类别可能需要本地或目标计算机上的管理员权限,具体取决于访问的内容。

枚举可用计数器

下面的示例枚举计算机上所有可用的性能计数器。对于具有实例的用户,它会枚举每个实例的计数器:

PerformanceCounterCategory[] cats =  PerformanceCounterCategory.GetCategories();foreach (PerformanceCounterCategory cat in cats){  Console.WriteLine ("Category: " + cat.CategoryName);  string[] instances = cat.GetInstanceNames();  if (instances.Length == 0)  {    foreach (PerformanceCounter ctr in cat.GetCounters())      Console.WriteLine ("  Counter: " + ctr.CounterName);  }  else   // Dump counters with instances  {    foreach (string instance in instances)    {      Console.WriteLine ("  Instance: " + instance);      if (cat.InstanceExists (instance))        foreach (PerformanceCounter ctr in cat.GetCounters (instance))          Console.WriteLine ("    Counter: " + ctr.CounterName);    }  }}
注意

结果是超过 10,000 行长!执行也需要一段时间,因为 PerformanceCounterCategory.InstanceExists 的实现效率低下。在实际系统中,您只希望按需检索更详细的信息。

下一个示例使用 LINQ 仅检索 .NET 性能计数器,并将结果写入 XML 文件:

var x =  new XElement ("counters",    from PerformanceCounterCategory cat in         PerformanceCounterCategory.GetCategories()    where cat.CategoryName.StartsWith (".NET")    let instances = cat.GetInstanceNames()    select new XElement ("category",      new XAttribute ("name", cat.CategoryName),      instances.Length == 0      ?        from c in cat.GetCounters()        select new XElement ("counter",          new XAttribute ("name", c.CounterName))      :        from i in instances        select new XElement ("instance", new XAttribute ("name", i),          !cat.InstanceExists (i)          ?            null          :            from c in cat.GetCounters (i)            select new XElement ("counter",              new XAttribute ("name", c.CounterName))        )    )  );x.Save ("counters.xml");
读取性能计数器数据

若要检索性能计数器的值,请实例化性能计数器对象,然后调用 NextValue 或 NextSample 方法。下一个值返回一个简单的浮点值;NextSample 返回一个 CounterSample 对象,该对象公开一组更高级的属性,例如 CounterFrequency 、TimeStamp 、BaseValue 和 RawValue 。

PerformanceCounter 的构造函数采用类别名称、计数器名称和可选实例。因此,要显示所有 CPU 的当前处理器利用率,请执行以下操作:

using PerformanceCounter pc = new PerformanceCounter ("Processor",                                                      "% Processor Time",                                                      "_Total");Console.WriteLine (pc.NextValue());

或者显示当前进程的“真实”(即私有)内存消耗:

string procName = Process.GetCurrentProcess().ProcessName;using PerformanceCounter pc = new PerformanceCounter ("Process",                                                      "Private Bytes",                                                      procName);Console.WriteLine (pc.NextValue());

性能计数器不会公开 ValueChanged 事件,因此如果要监视更改,则必须轮询。在下一个示例中,我们每 200 毫秒轮询一次,直到 EventWaitHandle 发出退出信号:

// need to import System.Threading as well as System.Diagnosticsstatic void Monitor (string category, string counter, string instance,                     EventWaitHandle stopper){  if (!PerformanceCounterCategory.Exists (category))    throw new InvalidOperationException ("Category does not exist");  if (!PerformanceCounterCategory.CounterExists (counter, category))    throw new InvalidOperationException ("Counter does not exist");  if (instance == null) instance = "";   // "" == no instance (not null!)  if (instance != "" &&      !PerformanceCounterCategory.InstanceExists (instance, category))    throw new InvalidOperationException ("Instance does not exist");  float lastValue = 0f;  using (PerformanceCounter pc = new PerformanceCounter (category,                                                      counter, instance))    while (!stopper.WaitOne (200, false))    {      float value = pc.NextValue();      if (value != lastValue)         // Only write out the value      {                               // if it has changed.        Console.WriteLine (value);        lastValue = value;      }    }}

以下是我们如何使用此方法同时监视处理器和硬盘驱动器活动:

EventWaitHandle stopper = new ManualResetEvent (false);new Thread (() =>  Monitor ("Processor", "% Processor Time", "_Total", stopper)).Start();new Thread (() =>  Monitor ("LogicalDisk", "% Idle Time", "C:", stopper)).Start();Console.WriteLine ("Monitoring - press any key to quit");Console.ReadKey();stopper.Set();
创建计数器和写入性能数据

在写入性能计数器数据之前,需要创建性能类别和计数器。您必须在一个步骤中创建性能类别以及属于它的所有计数器,如下所示:

string category = "Nutshell Monitoring";// We'll create two counters in this category:string eatenPerMin = "Macadamias eaten so far";string tooHard = "Macadamias deemed too hard";if (!PerformanceCounterCategory.Exists (category)){  CounterCreationDataCollection 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);}

然后,当您选择“添加计数器”时,新计数器将显示在 Windows 性能监视工具中。如果以后要在同一类别中定义更多计数器,则必须首先通过调用 PerformanceCounterCategory.Delete 来删除旧类别。

注意

创建和删除性能计数器需要管理权限。因此,它通常作为应用程序设置的一部分完成。

创建计数器后,可以通过实例化性能计数器、将“只读”设置为 false 以及设置 RawValue 来更新其值。还可以使用 Increment 和 IncrementBy 方法来更新现有值:

string category = "Nutshell Monitoring";string 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());    // 1011}
秒表类

秒表类为测量执行时间提供了一种方便的机制。秒表使用操作系统和硬件提供的最高分辨率机制,通常小于一微秒。(相比之下,DateTime.Now 和 Environment.TickCount 的分辨率约为 15 毫秒。

要使用秒表,请调用 StartNew — 这将实例化秒表并启动它滴答作响。(或者,您可以手动实例化它,然后调用“开始”。已用属性以 TimeSpan 的形式返回已用间隔:

Stopwatch s = Stopwatch.StartNew();System.IO.File.WriteAllText ("test.txt", new string ('*', 30000000));Console.WriteLine (s.Elapsed);       // 00:00:01.4322661

秒表还公开了一个 ElapsedTicks 属性,该属性以 long 的形式返回已用的“ticks”数。要将刻度转换为秒,请除以秒表。还有一个 ElapsedMilliseconds 属性,它通常是最方便的。

调用停止会冻结已用和已用的滴答声。“正在运行”的秒表不会产生后台活动,因此调用 Stop 是可选的。

跨平台诊断工具

在本部分中,我们将简要介绍可用于 .NET 的跨平台诊断工具:

dotnet-counters

提供正在运行的应用程序的状态概述

dotnet-trace

有关更详细的性能和事件监控

dotnet-dump

按需或在崩溃后获取内存转储

这些工具不需要管理提升,适用于开发和生产环境。

dotnet-counters

工具监视 .NET 进程的内存和 CPU 使用率,并将数据写入控制台(或文件)。

若要安装该工具,请从命令提示符或路径中带有 的终端运行以下命令:

dotnet tool install --global dotnet-counters

然后,您可以开始监视进程,如下所示:

dotnet-counters monitor System.Runtime --process-id <<ProcessID>>

System.Runtime 意味着我们要监视 类别下的所有计数器。可以指定类别或计数器名称(dotnet 计数器列表命令列出所有可用的类别和计数器)。

输出会不断刷新,如下所示:

Press p to pause, r to resume, q to quit.    Status: Running[System.Runtime]    # of Assemblies Loaded                            63    % Time in GC (since last GC)                       0    Allocation Rate (Bytes / sec)                244,864    CPU Usage (%)                                      6    Exceptions / sec                                   0    GC Heap Size (MB)                                  8    Gen 0 GC / sec                                     0    Gen 0 Size (B)                               265,176    Gen 1 GC / sec                                     0    Gen 1 Size (B)                               451,552    Gen 2 GC / sec                                     0    Gen 2 Size (B)                                    24    LOH Size (B)                               3,200,296    Monitor Lock Contention Count / sec                0    Number of Active Timers                            0    ThreadPool Completed Work Items / sec             15    ThreadPool Queue Length                            0    ThreadPool Threads Count                           9    Working Set (MB)                                  52

以下是所有可用的命令:

命令

目的

列表

显示计数器名称列表以及每个计数器名称的说明

附言

显示符合监视条件的 dotnet 进程的列表

监控

显示所选计数器的值(定期刷新)

收集

将计数器信息保存到文件中

支持以下参数:

选项/参数

目的

--版本

显示的版本。

-h, --帮助

显示有关程序的帮助。

-p, --进程标识

要监视的 dotnet 进程的 ID。适用于监视器和收集命令。

--刷新间隔

设置所需的刷新间隔(以秒为单位)。适用于监视器和收集命令。

-o, --输出

设置输出文件名。适用于收集命令。

--格式

设置输出格式。有效的是 或 。适用于收集命令。

dotnet-trace

跟踪是程序中事件的时间戳记录,例如正在调用的方法或正在查询的数据库。跟踪还可以包括性能指标和自定义事件,并且可以包含局部上下文,例如局部变量的值。传统上,.NET Framework 和框架(如 ASP.NET)使用 ETW。在 .NET 5 中,应用程序跟踪在 Windows 上运行时写入 ETW,在 Linux 上运行时写入 LTTng。

若要安装该工具,请运行以下命令:

dotnet tool install --global dotnet-trace

若要开始记录程序的事件,请运行以下命令:

dotnet-trace collect --process-id <<ProcessId>>

这将使用默认配置文件运行 ,该配置文件收集 CPU 和 .NET 运行时事件并写入名为 的文件。您可以使用 --profile 开关指定其他配置文件:gc-verbose 跟踪垃圾回收和采样对象分配, 以较低的开销跟踪垃圾回收。-o 开关允许您指定不同的输出文件名。

默认输出是一个 文件,可以使用 PerfView 工具直接在 Windows 计算机上进行分析。或者,您可以指示创建与Speedscope兼容的文件,Speedscope是 的免费在线分析服务。要创建 Speedscope (.) 文件,请使用选项 -。

注意

您可以从 下载最新版本的 PerfView。Windows 10 附带的版本可能不支持 文件。

支持以下命令:

命令

目的

收集

开始将计数器信息记录到文件中。

附言

显示符合监视条件的 dotnet 进程的列表。

列表配置文件

列出预生成的跟踪配置文件,并附有每个配置文件中的提供程序和筛选器的说明。

转换<文件>

从 () 格式转换为备用格式。目前,是唯一的目标选项。

自定义跟踪事件

你的应用可以通过定义自定义事件源来发出自定义事件:

[EventSource (Name = "MyTestSource")]public sealed class MyEventSource : EventSource{  public static MyEventSource Instance = new MyEventSource ();  MyEventSource() : base (EventSourceSettings.EtwSelfDescribingEventFormat)  {  }  public void Log (string message, int someNumber)  {    WriteEvent (1, message, someNumber);  }}

WriteEvent 方法被重载以接受简单类型(主要是字符串和整数)的各种组合。然后可以按如下方式调用它:

MyEventSource.Instance.Log ("Something", 123);

调用 时,必须指定要记录的任何自定义事件源的名称:

dotnet-trace collect --process-id <<ProcessId>> --providers MyTestSource
dotnet-dump

转储(有时称为)是进程虚拟内存状态的快照。您可以按需转储正在运行的进程,也可以将操作系统配置为在应用程序崩溃时生成转储。

在 Ubuntu Linux 上,以下命令在应用程序崩溃时启用核心转储(必要的步骤可能因不同版本的 Linux 而异):

ulimit -c unlimited

在 Windows 上,使用 在本地计算机配置单元中创建或编辑以下项:

SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps

在此下,添加一个与可执行文件同名的密钥(例如 ),并在该密钥下添加以下密钥:

转储文件夹 (REG_EXPAND_SZ),其值指示要将转储文件写入的路径转储类型 (REG_DWORD),值为 2 以请求完全转储(可选)DumpCount (REG_DWORD),指示删除最旧的转储文件之前的最大转储文件数

若要安装该工具,请运行以下命令:

dotnet tool install --global dotnet-dump

安装后,可以按需启动转储(不结束该过程),如下所示:

dotnet-dump collect --process-id <<YourProcessId>>

以下命令启动用于分析转储文件的交互式 shell:

dotnet-dump analyze <<dumpfile>>

如果异常导致应用程序关闭,则可以使用 命令(简称 )显示该异常的详细信息。dotnet-dump shell 支持许多其他命令,您可以使用 命令列出这些命令。

标签: #ubuntu fftw3