【IT168 专稿】作为程序员,谁都希望自己的软件性能优异,运行如飞。但是当我们在看到自己开发的软件像蜗牛一样慢吞吞地运行,半天没有反应的时候,我们常常会有这样一些疑问:
“我的系统都在忙些什么?CPU在干啥?”
“为什么我的软件性能表现这么低下?”
“哪里才是软件的性能瓶颈?什么代码导致了软件的性能低下?”
“软件运行到了什么状态?”
面对这些问题,程序员们都在想,要是有个软件仪表仪表,就像汽车的仪表盘一样,能够实时向我们报告系统和软件的运行状态就好了!现在,在Windows 7中,程序员们的这个梦想成为了现实。通过Windows 7所提供的Performance Counters,Event Tracing for Windows (ETW) ,Windows Management Instrumentation以及Windows Performance Toolkit,我们可以实时地获得系统和在其上运行的各种软件的性能状态信息,圆满地回答上面这些问题。利用这些丰富的状态信息,我们可以对应用程序进行诊断调试,性能分析,找到性能瓶颈,从而对其进行性能调优,给蜗牛软件插上飞的翅膀。
图1 我不是蜗牛,我是飞牛
关于性能分析的这十八般武器各有所长,这里我们先介绍性能计数器。
Performance Counters(性能计数器)
当我们在开发一些对性能期望较高的软件的时候,简单高效的性能计数器对发现软件中的性能瓶颈是很有价值的。虽然我们可以自己实现简单的性能计数器,但是,使用Windows操作系统本身所提供的Performance Counters(性能计数器),我们可以获得更多得天独厚的优势。
性能监视,是Windows NT引入的一种系统功能。从Windows NT以后,Windows操作系统总是集成了各种性能监视工具,它提供有关操作系统当前运行状况的信息,针对各种性能对象提供了数百个性能计数器。性能对象,就是被性能计数器监视的对象,我们通常比较关心的监视对象主要有Processor、Process、Memory、TCP/UDP/IP/ICMP、Physical Disk等。性能计数器通常提供操作系统、应用程序、服务、驱动程序等的性能相关信息,以此来分析系统瓶颈和对系统及应用程序性能进行诊断和调优。除了针对操作系统本身,通过性能计数器机制,我们也可以在应用程序或者是操作系统组件中向性能监视器(Performance Monitor)报告一些与性能有关的统计信息,以此来查看软件的性能信息,对其进行诊断和调优。
在Windows 7中,微软对性能计数器做了进一步的改进和优化。例如,采用了新的2.0版本的核心模块API、采用XML定义、更加强大的性能、更高的可扩展性和鲁棒性、增加了多个系统计时器等等。同时,为了方便系统管理员进行管理,还增加了PowerShell对计时器日志文件的处理功能等等,使得性能计数器的功能大大增强。
使用系统提供的性能计数器
很多程序员都精于使用 Windows 系统的性能监视器来查看 CPU 和内存的使用情况或硬盘信息。性能监视器就像一个听诊器,能够实时地反映当前系统的状态信息,以帮助我们判断系统是否运作正常。
图2 性能监视器
通过性能监视器,我们可以直观地获得系统所提供的各种性能监视器的数据,了解系统的运行状态。但是,如果我们想在应用程序中以编程的方式获得这些性能计数器的信息,该怎么处理呢?实际上,在Visual Studio 2010中,微软为我们提供了很多性能计时器供我们在应用程序中使用。在Visual Studio 2010的Server Explorer(服务器浏览器)中,我们可以看到很多系统提供的性能计数器。我们可以在应用程序中简单地以控件的形式使用这些性能计数器,获取系统的各种消息。
图3 Visual Studio提供的性能计数器
在这里,我们演示一下如何在应用程序中使用这些系统提供的性能计数器。首先,我们新建一个基于Visual C#的Windows窗体应用程序。然后,展开Server Explorer(服务器浏览器) 中的“Memory”(内存)节点,然后展开“Available Mbytes”(可用M字节)计数器,将该计数器拖到我们的应用程序中的新 Windows 窗体上。接着,我们在窗体上放置一个计时器并在其属性中启用这个计时器,这个计时器主要用于定时向性能计数器查询当前可用的内存,然后将这个值更新到界面上。最后,我们用一个Label标签来显示性能计数器中查询所得的当前可用内存数。整个窗体设计如下:
图4 在应用程序中使用性能计数器
完成Windows窗体的设计后,我们就可以在计时器的响应函数中,向我们使用的性能计数器查询当前可用内存数,并将这个数值更新到窗体的Label文本中:
{
// 查询性能计数器并更新Label文本
label1.Text = performanceCounter1.NextValue().ToString();
}
这样,当我们编译运行这个应用程序时,就可以通过性能计数器实时地得到当前系统可用内存数了:
图5 性能计数器实例
当然,这里只是一个非常简单的示例,在实际的应用程序中,我们不可能这么简单的使用性能计数器。但是这个实例却向我们演示了如何向我们自己的应用程序中添加系统定义的性能计数器,以监控和查询系统性能对象的信息。在实际应用中,我们可以根据某个系统性能对象的状态而采取某种行动,比如当系统的可用内存比较少的时候,我们可以减少程序的并发线程数;当笔记本的电池电量较低的时候,我们甚至可以采取自我保护,提示用户保存当前的工作成果等等。合理地使用这些系统信息,可以使得应用程序的性能更加优化,交互更加人性化。
自定义性能计数器
在很多情况下,除了直接使用系统提供的各种性能计数器获取系统信息之外,我们还希望可以添加自定义的性能计数器,以监视我们的应用程序中对性能影响比较大的某些因素。在Visual Studio 2010中,除了使用Server Explorer(服务器浏览器)提供的各种性能计数器之外,实际上,我们还可以通过使用System.Diagnostics名字空间中的PerformanceCounterCategory,CounterCreationDataCollection和PerformanceCounter这些类,完全以代码的形式为我们自己的应用程序添加自定义的计数器类别和性能计数器。当 PerformanceCounter 和 Windows 性能监视器结合使用时,就像一个医生(你自己)拿着听诊器(Windows性能监视器)来诊断你的应用程序(PerformanceCounter)一样,随时将你的应用程序的内部情况在性能监视器中反应出来,应用程序中哪里问题,哪里是性能瓶颈,一目了然。
在这里,我们以一个复制文件的应用程序为例,使用性能计数器来监视当前所复制的文件数目以及完成总进度的百分比。首先,我们新创建一个基于Visual C#的Windows Form应用程序,并设计窗体如下:
图6 复制文件应用程序
这里,我们并不关注文件选择、文件复制这些操作是如何完成的,我们把关注的重点放在如何新建一个性能计数器并对文件复制过程进行监控。为了便于使用,我们新创建一个类FileCopyPerformanceCounters来封装相关的自定义性能计数器。在Visual Studio 2010中,我们将这个类实现如下:
using System.Collections.Generic;
using System.Linq;
using System.Text;
// 为了使用计数器所需要引入的名字空间
using System.Diagnostics;
namespace FileCopier
{
// 文件复制性能计数器
public static class FileCopyPerformanceCounters
{
// 计数器成员
// 用于记录已经复制的文件数和当前进度百分比
private static PerformanceCounter totalFilesCounter;
private static PerformanceCounter percentDoneCounter;
public static void Initialize()
{
// 创建新的计数器数据并添加到计数器数据集合中
CounterCreationDataCollection counters =
new CounterCreationDataCollection();
CounterCreationData counter = new CounterCreationData(
"Total Files Copied",
"Total number of files copied by the application.",
PerformanceCounterType.NumberOfItems32);
counters.Add(counter);
counter = new CounterCreationData(
"% Files Copied",
"Percent of files copied in the current operation.",
PerformanceCounterType.NumberOfItems32);
counters.Add(counter);
// 创建新的自定义性能计数器类别
// 这个类别收集之前定义的计数器数据集合
if (PerformanceCounterCategory.Exists("FileCopier"))
PerformanceCounterCategory.Delete("FileCopier");
PerformanceCounterCategory.Create(
"FileCopier",
"Instrumentation of the FileCopier application.",
PerformanceCounterCategoryType.SingleInstance,
counters);
// 创建新的性能计数器
// 相应的数据保存在之前定义的计数器数据集合中
totalFilesCounter = new PerformanceCounter(
"FileCopier", "Total Files Copied", false);
percentDoneCounter = new PerformanceCounter(
"FileCopier", "% Files Copied", false);
}
// 更新已经复制的文件数
public static void UpdateTotalFiles(int totalFiles)
{
// 更新计数器的值
totalFilesCounter.RawValue = totalFiles;
}
// 更新复制进度百分比
public static void UpdatePercentDone(int percentDone)
{
// 更新计数器的值
percentDoneCounter.RawValue = percentDone;
}
}
}
在这段代码中,我们在类中包装了两个性能计数器totalFilesCounter和percentDoneCounter,分别用于记录已经复制的文件数和复制进度的百分比。在类的初始化函数中,我们完成了这两个自定义性能计数器的创建工作。
首先我们创建了两个CounterCreationData对象,用于收集记录计数器的值,然后将这两个CounterCreationData对象添加到计数器数据集合CounterCreationDataCollection中。利用这个数据集合,我们就可以创建新的计数器类别了,这表示这个自定义的计数器类别会收集来自这个数据集合的计数器数据。最后,我们再创建自定义的性能计数器PerformanceCounter,并通过构造函数的参数(string categoryName, string counterName),指定这个计数器将数据保存到什么位置。
完成计数器的创建后,我们新添加了两个函数用于更新计数器的值,便于我们的应用程序调用。至此,我们自定义的文件复制计数器就实现完成了。下面我们来看看如何在我们的应用程序中使用这个自定义的性能计数器。
首先,我们需要在构造函数中完成文件复制性能计数器类的初始化工作,这一步通过调用FileCopyPerformanceCounters类的Initialize函数完成:
{
this.InitializeComponent();
// 初始化计数器类
FileCopyPerformanceCounters.Initialize();
}
完成性能计数器的初始化后,我们就可以在文件复制过程中使用计数器对文件复制过程进行监视和统计了。在“复制”按钮的响应函数中,我们用性能计数器记录文件复制数和完成的百分比:
{
// 获得源目录和目标目录
string source = this.txtSourceDirectory.Text;
string dest = this.txtDestinationDirectory.Text;
// 检查目录合法性
if (!Directory.Exists(source) || !Directory.Exists(dest))
{
MessageBox.Show("源目录或者目标目录不存在.", "Error");
return;
}
// 设置进度条
this.btnCopy.Enabled = false;
this.progressBar.Value = 0;
this.progressBar.Style = ProgressBarStyle.Continuous;
// 创建后台复制线程
this.worker = new BackgroundWorker();
// 执行复制
this.worker.DoWork += (o, e) =>
{
string[] files = Directory.GetFiles(source);
for (int i = 0; i < files.Length; ++i)
{
Thread.Sleep(1000);
File.Copy(files[i], Path.Combine(dest, Path.GetFileName(files[i])));
this.worker.ReportProgress((int)((100.0f * i) / files.Length));
// 用计数器记录已经复制的文件数
FileCopyPerformanceCounters.UpdateTotalFiles(i);
}
};
this.worker.WorkerReportsProgress = true;
this.worker.ProgressChanged += (o, e) =>
{
this.BeginInvoke((MethodInvoker)delegate
{
progressBar.Value = e.ProgressPercentage;
// 用计数器记录文件复制完成的百分比
FileCopyPerformanceCounters.UpdatePercentDone(e.ProgressPercentage);
});
};
this.worker.RunWorkerCompleted += (o, e) =>
{
this.BeginInvoke((MethodInvoker)delegate
{
btnCopy.Enabled = true;
progressBar.Style = ProgressBarStyle.Marquee;
});
};
this.worker.RunWorkerAsync();
}
在这段代码中,我们通过在文件复制Lambda表达式中调用FileCopyPerformanceCounters类的UpdateTotalFiles函数,实时地对性能计数器进行了更新,对复制的文件数进行了监视。同样地,在进度条更新的Lambda表达式中,我们对当前复制进度的百分比计数器进行了更新。
现在,编译整个项目,并以管理员身份运行这个应用程序,我们就可以利用自定义的性能计数器对整个复制过程进行监视了。当我们选择合适的源目录和目标目录并进行文件复制的时候,自定义的性能计数器会记录当前复制完成的文件数和复制过程的百分比。然后,利用性能监视器,添加我们自定义的性能计数器分类,我们就可以对整个复制过程进行实时的监视了。
图7 添加自定义的性能计数器
在文件复制过程,我们可以通过性能监视器动态实时地看到整个复制过程了,如果在复制过程中有什么异常,可以非常直观地在性能监视器中反映出来。这对于我们对应用程序进行性能调优有很大的帮助:
图8 对文件复制过程进行监视
在上面这幅性能监视器的截图中,我们可以清楚地看到在文件复制过程中有一段进度上升的速度不一致,也就是说复制某个文件用了很长的时间,影响了应用程序的性能。这些信息就指引我们对文件复制过程进行进一步的调试,找到这个文件复制性能下降的原因。
通过性能计数器,我们可以找到应用程序性能瓶颈的所在,但是要对问题进行精确定位,我们还需要更多的信息,这个时候,我们就需要Event Tracing for Windows (ETW)来帮忙了。至于ETW是如何与性能计数器通力合作智斗性能瓶颈这个纸老虎的,请听下回分解。