使用 NUnit 插件进行性能基准测试






4.97/5 (14投票s)
本教程介绍了一个新的 NUnit 插件,能够记录单元测试的执行时间,并生成包含图表和历史记录的 XML、CSV、HTML 性能报告。
下载
简介
本教程介绍了一个新的 NUnit 插件,能够记录单元测试的执行时间,并生成包含图表和历史记录的 XML、CSV、HTML 性能报告。
开发仍在进行中,该项目将在未来几个月内不断发展,并增加新功能,以及与其他框架(如 CruiseControl.NET、mbUnit 和 Gallio)的集成。
背景
下面描述的用例是我开发该解决方案的驱动因素,但为该特定应用程序开发的工具可以完全回收并应用于任何需要基准测试功能的其他项目。
前段时间,我开始开发一个 CMS 项目,该项目需要且仍然需要持续精确的性能基准测试。
其内容数据库完全基于 XML 文件系统,由于插件子系统的强制灵活性,其整个架构变得非常复杂。
由于这两个因素,性能主要取决于:
- 整体算法复杂度
- XML IO 时间
算法复杂度可以估算(因为根据定义它是确定性的),但在复杂工作流中估算它很麻烦。
XML IO 时间更难预测。
这两个因素结合在一起,使得性能优化过程痛苦不堪, at some point 我意识到理论是不够的:我需要真实的基准测试。
先决条件形式化
在功能方面,为了调整性能和评估可能的优化策略,我需要一个能够:- 在我对代码库进行有意义的修改时,测量系统中每个高级和中级功能的执行时间。
- 保持每次测量记录的时间顺序,并以一种让我能够确定某个修改是否会提高系统性能的方式直观地比较测量结果。
- 免费且无许可证
- 在基准测试的视觉布局方面完全可定制
- 在基准测试参数方面完全可定制
- 对本机代码或单元测试代码无侵入性
- 足够“智能”,能够自动建议改进代码的方法。
解决方案
由于所有项目代码都由 NUnit 测试覆盖,我决定寻找一个能够集成到 NUnit 中,并以易于管理的 单元测试 形式执行性能基准测试的工具。
不幸的是,我测试过的工具没有一个满足我所有的需求,所以我决定基于 NUnit 内嵌的可扩展性功能,自己开发一个工具。
在查阅了一些文档和示例后,我决定开发我的新插件,以便它能够:
- 由测试方法中尽可能小的代码块驱动进行基准测试。我决定遵循通用的 NUnit 扩展策略,开发一个全新的属性(
NJC_TestPerformanceRecorderAttribute
),该属性能够指导插件,而不影响测试内的代码,从而不触及其执行流程。 - 通过描述符(
NJC_TestPerformanceDescriptor
)跟踪测试执行信息,该描述符通过跟踪属性NJC_TestPerformanceRecorderAttribute
中声明的参数进行初始化。 - 通过 EventListener 接口连接到 NUnit 基础设施。
- 使架构足够可扩展,以便我能够生成可重用于外部环境的有用数据报告。当前实现支持 3 种输出报告格式:
- CSV:写入 CSV 文件,其中每一行是一个单独的基准测试,其描述值写入其列中。
- XML:写入 XML 文件,其中根节点的每个子节点是一个单独的基准测试。
- HTML:同时写入一个 XML 文件和一个 HTML 文件,该 HTML 文件通过转换嵌入的 XML 文件生成。HTML 报告包含图表和视觉辅助工具,有助于阅读基准测试结果并就采取的优化措施做出进一步决策。
全局架构
下面是一个简化的图表,显示了插件的全局架构。如上所述,主要组件是主插件类(
NJC_TestPerformanceAddIn
)、执行描述符类(NJC_TestPerformanceDescriptor
)和测试属性类(NJC_TestPerformanceRecorderAttribute
)。
NJC_TestPerformanceRecorderAttribute 属性
用于定义基准测试工作流行为的属性结构非常简单,因此我们直接跳到其用法演示和每个参数的含义:
[Test]
// Mandatory declaration of the attribute to makes the Addin able to recognize it as a benchmark test
[NJC_TestPerformanceRecorder(
// Optional. The logic/descriptive name associated to the test
// Default value = ""
TestName = "Test 1",
// Optional. a desciption of the test
// Default value = ""
TestDescription = "Test description",
// Optional. The output of the benchmark must overwrite (Overwrite) the previous trackings, or must be appended to them (Append) ?
// Default value = NJC_TestPerformanceTargetWriteMode.Append
OutputTargetWriteMode = NJC_TestPerformanceTargetWriteMode.Append,
// Optional. Where will be written the output ? By now, the only possibile output value is "FileSystem"
// Default value = NJC_TestPerformanceTargetKind.FileSystem
OutputTargetKind = NJC_TestPerformanceTargetKind.FileSystem,
// Optional Flag. Which is the output format of the benchmark ? By now there are 3 values that can be combined together:
// Csv -> Write a CSV output, where every row is a single benchmark with the description values written in its columns
// Xml -> Write an XML output, where every child node of the root is a single benchmark
// HTML -> Writes both an XML output and an HTML output generated by the transformation of the XML output with the embedded
// transformation style sheet NJC_TestPerformanceDescriptorWriterHTML.xsl
// Default value = NJC_TestPerformanceTargetFormat.Xml
OutputTargetFormat = NJC_TestPerformanceTargetFormat.Csv | NJC_TestPerformanceTargetFormat.Xml,
// Optional. The storage location of the benchmark output.
// Since by now the only supported OutputTargetKind is "FileSystem", the OutputTarget value represents the target folder
// in which the output files are written.
// Default value = the test assembly folder
OutputTarget = TARGET_FOLDER,
// Optional. Every benchmarked test is identified by a unique key.
// With the OutputTargetKind set to FileSystem, this identification key corresponds to the file name where the output
// is stored with the OutputTargetWriteMode.
// Default value = NJC_TestPerformanceTargetIdentificationFormat.ClassFullNameAndMethodName
OutputTargetIdentificationFormat = NJC_TestPerformanceTargetIdentificationFormat.ClassFullNameAndMethodName
)]
public void My_Method_Test_That_Needs_For_Benchmarking()
{
/* place here the code to test AND benchmark*/
}
这里是
NJC_TestPerformanceRecorderAttribute
属性的内部结构表示,以及与配置属性关联的枚举。
NJC_TestPerformanceDescriptor 描述符
在 NJC_TestPerformanceDescriptor
类中实现的性能描述符仅仅是一个信息和参数持有者,其唯一功能是表示应用于测试方法的 NJC_TestPerformanceRecorderAttribute
的计时和执行状态。
由于这些原因,其所有属性和私有成员都以只读方式暴露给公共环境。
唯一的例外是 EndTime
属性,用于实现实例的时间跟踪功能,该功能由只读属性 ExecutionTime
暴露。
EventListener 接口
EventListener
接口非常有用,因为它使自定义代码能够以异步方式透明地连接到 NUnit 环境事件:接口实现者中的每个方法都由框架调用,而无需等待其执行结束。
它的定义(在 NUnit 2.4.4 中)如下:
public interface EventListener
{
void RunStarted( string name, int testCount );
void RunFinished( TestResult result );
void RunFinished( Exception exception );
void TestStarted(TestName testName);
void TestFinished(TestCaseResult result);
void SuiteStarted(TestName testName);
void SuiteFinished(TestSuiteResult result);
void UnhandledException( Exception exception );
void TestOutput(TestOutput testOutput);
}
为了代码编写的简洁和清晰,我将 NJC_TestPerformanceAddIn
的核心直接实现 IAddin
接口(NUnit 需要该接口来识别类为 Addin)和 EventListener
接口(驱动插件到 NUnit 基础设施的连接实现)。
/// <summary>
/// This is the main Addin class
/// </summary>
[NUnitAddin(Name = "NinjaCross Test Performance Extension")]
public sealed class NJC_TestPerformanceAddIn : IAddin, EventListener
{
...
}
NJC_TestPerformanceAddIn 插件及其工作流
插件的工作流由这些方法实现的顺序调用控制:
步骤 1) NJC_TestPerformanceAddIn.RunStarted
和/或 NJC_TestPerformanceAddIn.SuiteStarted
加载测试执行程序集
public void RunStarted(string name, int testCount)
{
if (Debugger.IsAttached)
Log("RunStarted", name, testCount);
EnsureCacheAssembly(name);
}
//----------------------------------------------------------------------------
public void SuiteStarted(TestName testName)
{
if (Debugger.IsAttached)
Log("SuiteStarted", testName.FullName, testName.Name, testName.RunnerID, testName.TestID, testName.UniqueName);
EnsureCacheAssembly(testName.FullName);
}
//----------------------------------------------------------------------------
private Assembly EnsureCacheAssembly(String fullName)
{
try
{
Assembly a = GetCachedAssembly(fullName);
if (a != null)
return a;
if (fullName.EndsWith(".dll", StringComparison.CurrentCultureIgnoreCase) || fullName.EndsWith(".exe", StringComparison.CurrentCultureIgnoreCase))
{
Assembly asm = Assembly.LoadFile(fullName);
_assembliesCache.Add(asm);
return asm;
}
else
{
return null;
}
}
catch (Exception exc)
{
Log("AddSession", exc);
return null;
}
}
步骤 2) NJC_TestPerformanceAddIn.TestStarted
检查测试方法装饰中是否存在 NJC_TestPerformanceRecorderAttribute
属性。
如果存在,则该方法是合适的基准测试候选者,因此获取属性中定义的描述和输出信息/参数,并将它们存储到类型为 NJC_TestPerformanceDescriptor
的执行描述符中。
使用 DateTime.Now
值将执行开始时间设置到 NJC_TestPerformanceDescriptor.StartTime
。
public void TestStarted(TestName testName)
{
if (Debugger.IsAttached)
Log("TestStarted", testName.FullName, testName.Name, testName.RunnerID, testName.TestID, testName.UniqueName);
MethodInfo method = NJC_TestPerformanceUtils.GetMethodInfo(testName.FullName, _assembliesCache);
if (method != null)
{
if (NJC_TestPerformanceUtils.GetPerformanceAttribute(method) != null)
{
// generate a descriptor for the current test method
NJC_TestPerformanceDescriptor descriptor = new NJC_TestPerformanceDescriptor(method);
// cache the generated descriptor
_runningTests.Add(descriptor);
}
}
}
步骤 3) NJC_TestPerformanceAddIn.TestFinished
通过将 NJC_TestPerformanceDescriptor.EndTime
设置为 DateTime.Now
来停止当前正在运行的测试的计时器,并按照 NJC_TestPerformanceRecorderAttribute
指定的方式写入输出,该信息已存储在执行描述符 NJC_TestPerformanceDescriptor
中。
描述符的序列化委托给一组特定的类,也称为“写入器”(参见下一段)。
public void TestFinished(TestResult result)
{
if (Debugger.IsAttached)
Log("TestFinished", result.FullName, result.HasResults, result.AssertCount, result.Description, result.Executed, result.FailureSite);
if (result.Executed)
{
// resolve the test method using its full name
MethodInfo method = NJC_TestPerformanceUtils.GetMethodInfo(result.FullName, _assembliesCache);
if (method != null)
{
// get the descriptor from the cached running tests
NJC_TestPerformanceDescriptor descriptor = _runningTests.GetItem(method);
// if a descriptor is found for the reflected method, ands the time measurement procedure
if (descriptor != null)
{
// end the time measurement, setting the EndTime property to DateTime.Now
descriptor.EndTime = DateTime.Now;
// loops inside the list of avaiable writers
foreach (KeyValuePair<NJC_TestPerformanceTargetFormat, NJC_TestPerformanceDescriptorWriter> writerKVP in _writers)
{
// check if the given format is supported by the current writer
if ((descriptor.TestMethodAttribute.OutputTargetFormat & writerKVP.Key) == writerKVP.Key)
{
// serialize the descriptor into the suitable target using the selected writer
writerKVP.Value.Write(descriptor);
}
}
// the method terminated it's execution, so it's not more needed into the cache
_runningTests.Remove(method);
}
}
}
}
输出写入器
如前所述,工作流的最后一步是根据 NJC_TestPerformanceRecorder
属性参数指定的格式存储与测试相关的输出信息。
为此,每个测试执行描述符(NJC_TestPerformanceDescriptor
)都被序列化到一个目标文件,使用一个实现适当格式的序列化业务逻辑的特定“写入器”。
插件通过 NJC_TestPerformanceRecorder.OutputTargetFormat
(类型为 NJC_TestPerformanceTargetFormat
)的值来选择要使用的写入器。
枚举 NJC_TestPerformanceTargetFormat
的每个值都在 NJC_TestPerformanceAddIn
类中的“写入器列表”中进行一对一映射。写入器列表声明为类成员……
/// <summary>
/// Writers avaiable into this addin implementation
/// </summary>
private NJC_TestPerformanceDescriptorWriterList _writers = new NJC_TestPerformanceDescriptorWriterList();
……并在构造函数中为每个可用写入器初始化一个实例。public NJC_TestPerformanceAddIn()
{
_writers.Add(NJC_TestPerformanceTargetFormat.Xml, new NJC_TestPerformanceDescriptorWriterXml());
_writers.Add(NJC_TestPerformanceTargetFormat.Csv, new NJC_TestPerformanceDescriptorWriterCsv());
_writers.Add(NJC_TestPerformanceTargetFormat.Html, new NJC_TestPerformanceDescriptorWriterHTML());
}
每个写入器都继承自基类 NJC_TestPerformanceDescriptorWriter
。
public abstract class NJC_TestPerformanceDescriptorWriter
{
/// <summary>
/// Write to the destination target
/// </summary>
public abstract void Write(NJC_TestPerformanceDescriptor descriptor);
public abstract String FileExtension
{
get;
}
}
这里是支持/提到的写入器的继承树表示。
如前所述,可用的输出格式有 3 种(每种都继承自 NJC_TestPerformanceDescriptorWriter
),并且只能生成到文件系统(OutputTargetKind = NJC_TestPerformanceTargetKind.FileSystem
)。
输出文件名使用以下模式组合而成:
<output_folder>\<file_name>.<file_extension>其中
<输出文件夹> =
NJC_TestPerformanceRecorder.OutputTarget
值,如果未指定值,则为测试程序集文件夹。<文件名> = 基于
NJC_TestPerformanceRecorder.OutputTargetIdentificationFormat
中指定的格式的名称。可用值为 MethodName、ClassNameAndMethodName、ClassFullNameAndMethodName。<文件扩展名> = 基于
NJC_TestPerformanceRecorder.OutputTargetFormat
标志值的扩展名。- "csv",用于
NJC_TestPerformanceTargetFormat.Csv
。 - "xml",用于
NJC_TestPerformanceTargetFormat.Xml
。 - "html",用于
NJC_TestPerformanceTargetFormat.Html
。对于此工作模式,还会生成一个 "xml" 文件,该文件用作NJC_TestPerformanceTargetFormat.Html
选项的NJC_TestPerformanceTargetWriteMode.Append
“模式”的增量存储库。
将写入器类包含到全局图中,得到的“全景图”如下:

使用示例
下面是一些 NJC_TestPerformanceRecorderAttribute
的使用示例。
考虑到
NJC_TestPerformanceRecorderAttribute
的所有参数都是可选的且具有默认值,只有那些需要不同于默认值的值才必须指定。示例 1
跟踪一个测试...
- 到文件系统(默认选项,无需指定属性参数
OutputTargetKind
) - 到 XML 文件作为跟踪存储库
- 以测试方法名称命名“MyTestMethod.xml”
- 到目标文件夹“C:\MyProjects\MyTests\PerformanceRepository”
- 覆盖先前记录的时间
- 不指定名称
- 不指定描述
[Test]
[NJC_TestPerformanceRecorder
(
OutputTargetFormat = NJC_TestPerformanceTargetFormat.Xml, // xml file output
OutputTargetIdentificationFormat = NJC_TestPerformanceTargetIdentificationFormat.MethodName, // method name
OutputTarget="C:\MyProjects\MyTests\PerformanceRepository", // in real-world code, a constant would be better
OutputTargetWriteMode = NJC_TestPerformanceTargetWriteMode.OverWrite // overwrite previous trackings
)]
public void MyTestMethod()
{
/* place here the code to test AND benchmark*/
}
示例 2
跟踪一个测试...
- 到文件系统(默认选项,无需指定属性参数
OutputTargetKind
) - 到 XHTML 文件作为视觉报告
- 以及一个用作跟踪存储库的 XML 文件
- 以类的全名和测试方法名称命名“MyTestNameSpace.MyTestClass.MyTestMethod.html”
- 到目标文件夹“C:\MyProjects\MyTests\PerformanceRepository”
- 将新跟踪追加到先前记录的时间
- 不指定名称
- 不指定描述
[Test]
[NJC_TestPerformanceRecorder
(
OutputTargetFormat = NJC_TestPerformanceTargetFormat.Html, // Xhtml file output
OutputTargetIdentificationFormat = NJC_TestPerformanceTargetIdentificationFormat.ClassFullNameAndMethodName, // full method name
OutputTarget="C:\MyProjects\MyTests\PerformanceRepository", // in real-world code, a constant would be better
OutputTargetWriteMode = NJC_TestPerformanceTargetWriteMode.Append // append to previous trackings
)]
public void MyTestMethod()
{
/* place here the code to test AND benchmark*/
}
示例 3
跟踪一个测试...
- 到文件系统(默认选项,无需指定属性参数
OutputTargetKind
) - 到 CSV 文件作为视觉报告和跟踪存储库
- 以类和测试方法名称命名“MyTestClass.MyTestMethod.csv”
- 到目标文件夹“C:\MyProjects\MyTests\PerformanceRepository”
- 将新跟踪追加到先前记录的时间
- 指定名称
- 指定描述
[Test]
[NJC_TestPerformanceRecorder
(
Name = "Performance test 1",
Description = "This is a unit test used as performance tracking element",
OutputTargetFormat = NJC_TestPerformanceTargetFormat.Csv, // Csv file output
OutputTargetIdentificationFormat = NJC_TestPerformanceTargetIdentificationFormat.ClassNameAndMethodName, // class and method name
OutputTarget="C:\MyProjects\MyTests\PerformanceRepository", // in real-world code, a constant would be better
OutputTargetWriteMode = NJC_TestPerformanceTargetWriteMode.Append // append to previous trackings
)]
public void MyTestMethod()
{
/* place here the code to test AND benchmark*/
}
输出示例
下面是一些由插件生成的示例。
CSV 文件内容
第一行包含列标题,后续行包含测试执行数据。
start;startTicks;end;endTicks;elapsed;elapsedTicks lun, 06 apr 2009 13:20:49 GMT;633746208497456374;lun, 06 apr 2009 13:20:50 GMT;633746208500267866;00:00:00.2811492;2811492 lun, 06 apr 2009 13:21:14 GMT;633746208741743790;lun, 06 apr 2009 13:21:14 GMT;633746208744555282;00:00:00.2811492;2811492 lun, 06 apr 2009 13:21:20 GMT;633746208806408106;lun, 06 apr 2009 13:21:20 GMT;633746208809688180;00:00:00.3280074;3280074 lun, 06 apr 2009 13:21:27 GMT;633746208878569734;lun, 06 apr 2009 13:21:28 GMT;633746208881693614;00:00:00.3123880;3123880
XML 文件内容testExecutionHistory
节点包含一组 testExecution
子节点。
每个子节点代表一次测试执行。testExecution
节点包含 RFC1123 格式(请参阅 http://www.isi.edu/in-notes/rfc1123.txt 或 http://www.faqs.org/rfcs/rfc1123.html)和滴答数(其中一个滴答是一百纳秒或千分之一秒,如 MSDN 文档所述)的日期和时间间隔。
<?xml version="1.0" encoding="UTF-8"?>
<testExecutionHistory
ns="NinjaCross.Classes.Nunit.TestPerformance"
className="NJC_TestPerformanceRecorderAttributeTest"
methodName="Test_Success_WriteModeAppend_TargetKindFileSystem_TargetFormatCsvXml"
testName="Test 1"
testDescription="Test description">
<testExecution>
<start>lun, 06 apr 2009 13:20:50 GMT</start>
<startTicks>633746208500424060</startTicks>
<end>lun, 06 apr 2009 13:20:50 GMT</end>
<endTicks>633746208504328910</endTicks>
<elapsed>00:00:00.3904850</elapsed>
<elapsedTicks>3904850</elapsedTicks>
</testExecution>
<testExecution>
<start>lun, 06 apr 2009 13:21:14 GMT</start>
<startTicks>633746208744711476</startTicks>
<end>lun, 06 apr 2009 13:21:14 GMT</end>
<endTicks>633746208748460132</endTicks>
<elapsed>00:00:00.3748656</elapsed>
<elapsedTicks>3748656</elapsedTicks>
</testExecution>
<testExecution>
<start>lun, 06 apr 2009 13:21:20 GMT</start>
<startTicks>633746208809844374</startTicks>
<end>lun, 06 apr 2009 13:21:21 GMT</end>
<endTicks>633746208812655866</endTicks>
<elapsed>00:00:00.2811492</elapsed>
<elapsedTicks>2811492</elapsedTicks>
</testExecution>
<testExecution>
<start>lun, 06 apr 2009 13:21:28 GMT</start>
<startTicks>633746208881849808</startTicks>
<end>lun, 06 apr 2009 13:21:28 GMT</end>
<endTicks>633746208885442270</endTicks>
<elapsed>00:00:00.3592462</elapsed>
<elapsedTicks>3592462</elapsedTicks>
</testExecution>
</testExecutionHistory>
HTML 文件内容
这是最重要也是最有趣的报告格式。
它包含与测试执行相关的所有可用信息,包括原始数据和数据分析/趋势。
通过使用基于网格的图形/数据表示执行时间,并结合使用 Google Chart API 生成的折线图,可以直观地评估跟踪方法在其开发生命周期中的性能趋势。
这里有一些关于视觉功能的解释:
- 测试标识和信息
- “测试标识和信息”中的“名称”字段仅在属性声明中提供了
NJC_TestPerformanceRecorderAttribute.TestName
时出现。 - “测试标识和信息”中的“描述”字段仅在提供了
NJC_TestPerformanceRecorderAttribute.TestDescription
时出现。 - 如果 XSL 样式表检测到至少有一个记录的
elapsedTicks==0
,则报告会显示一条警告消息,指出该基准测试无用,因为计时器无法测量有意义的时间跨度。 - 测试统计和测试性能跟踪框
- 红色条形代表性能最差的执行。
- 绿色条形代表性能最好的执行。
- 黄色条形代表性能既非“最佳”也非“最差”的执行。
- 测试性能演变
- 如果 XSL 样式表检测到给定测试只有一个记录,则不会在报告中渲染 Google 折线图,因为它没有意义。
- 折线图还显示执行时间的最小值、最大值和平均值,因此工作范围和特征行为都清晰可见且可衡量。
- X 轴代表测试执行的维度。原点是第一次执行日期,右侧是最后一次执行日期。日期以 RFC1123 格式显示。
- Y 轴表示以秒为单位的执行时间。
- 两个轴都会自动调整大小以容纳该测试的最后 100 次执行。
Visual Studio 2005 解决方案和安装说明
要使用此插件,您必须首先将其编译后的程序集复制到 NUnit 安装路径的“addins”子文件夹中。
可以通过以下两种方式获得编译后的程序集:
- 您可以使用可用的 VS2005 解决方案生成它。
- 您可以从这里下载它。
如果您想自己编译它,可以下载附带的解决方案,其中包含 2 个项目:
- 插件项目,包含上述所有类。该项目有一个生成后事件,该事件将输出 DLL 复制到默认 NUnit 安装文件夹(我的 2.5 版本是“C:\Programmi\NUnit 2.5\bin\net-2.0\addins”)。如果您的 NUnit 安装在其他路径,只需更改它并重新编译项目。如果您想手动管理插件安装(例如,不干扰正在运行的实例),只需删除生成后事件。
- 一个 NUnit 项目,用于测试插件中实现的功能。总覆盖率不是最高的,但包含的测试(51 个)足以确保良好的代码稳定性。
结语和后续开发
该工具完全满足了我的需求,因此即使它不是一个最先进的项目,它也是进一步开发和扩展的一个不错的起点。
我希望将来能增加新功能,例如:
- 与 CruiseControl.NET 的视觉集成。
- 在 NUnit GUI 中的视觉集成。
- 生成一个链接到所有 HTML 报告的索引文档。
- 支持外部 xsl 样式表,能够生成具有不同视觉布局和高级性能评估功能的 HTML 报告。
- 支持条件记录条件,允许指定决定是否在测试执行后记录到目标文件的规则(例如,当测试不成功时不记录性能)。
- 支持与 mbUnit 的集成。
- 支持与 Gallio Framework 的集成。
参考文献
开发历史(日/月/年)
- 2009/05/15 - 版本 1.0.0.0:本项目已迁移至其官方网站 www.firebenchmarks.com
- 2009/05/15 - 版本 1.0.0.0:在教程中扩展了安装说明。
- 2009/05/12 - 版本 1.0.0.0:修复了 VS2005 下载包。
- 2009/04/21 - 版本 1.0.0.0:在教程中添加了代码示例。
- 2009/04/08 - 版本 1.0.0.0:首次发布。