将性能分析集成到构建过程中
探讨如何将性能分析集成到构建过程中,作为自动化测试套件的一部分。
引言
许多开发人员都明白在发布前使用性能分析器的重要性,但发布前几天才发现问题可能会让人头疼。您可能需要花几周时间来梳理工作,以便更改导致程序运行缓慢的方法,并且在问题解决后,可能没有时间像平常那样仔细地测试解决方案。在此期间,您还承受着压力,这增加了犯错的风险。如果在整个开发过程中对程序进行分析,无疑会更好,但很少有团队有足够的闲暇时间定期进行此操作。
如果您已采用持续集成方法,那么您可能已经在构建系统中部分地整合了自动化测试。如果您可以使用现有的测试工具来自动检查应用程序的性能,每次测试运行时,岂不是很好?这样做的好处是可以及时发现提交的代码中的问题,帮助您更快地找到问题及其解决方案。甚至可能在您的老板发现问题之前就找到问题!
在本教程中,我将演示如何使用 Red Gate 的 ANTS Performance Profiler 在 NUnit 测试中比较每种方法与已知基线的 CPU 滴答数。如果滴答数超过 33% 的阈值,NUnit 测试将失败。
自动化性能分析
背景
ANTS Performance Profiler 6 引入了命令行界面,允许在没有图形用户界面的情况下运行分析会话。然后可以将结果导出到 XML 文件。此处描述的程序依赖于比较两个 XML 结果文件中的值,一个用于已知基线,另一个用于您刚刚构建的版本。
为简单起见,我假设您的应用程序可以完全从命令行运行,无需交互;如果不是这种情况,您可能需要单独测试应用程序的不同部分。
请注意,本教程使用参数化 NUnit 测试,这需要 NUnit 2.5 或更高版本。
步骤 1:记录基线结果集
第一步自然是记录一个已知性能良好的应用程序现有构建的结果集。在命令行启动 ANTS Performance Profiler,并将结果保存到 XML 文件中。
确保将基线结果保存在您的测试工具可以读取的位置。
步骤 2:记录新构建的结果
将相同的命令添加到构建服务器完成新构建后运行的批处理文件中。同样,请确保将结果保存在您的测试工具可以读取的位置。
请注意,该命令还会以 APP6 结果格式保存一份结果副本。如果遇到性能问题,您无需再次分析应用程序即可在 ANTS Performance Profiler 中打开结果。
步骤 3:编写程序以读取两个结果文件的数据,并将其作为参数化 NUnit 测试提供
该解决方案包含两个类库项目:一个用于读取 ANTS Performance Profiler 创建的 XML 结果文件,另一个用于执行测试。
从结果文件中读取数据
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Xml;
namespace RedGate.NUnitProfilingSample.ReadXml
{
public class ReadXml
{
public Dictionary<string, long> XmlRead(string filename)
{
List<string> hierarchy = new List<string>();
Dictionary<string, long> readResults = new Dictionary<string, long>();
using(XmlTextReader textReader = new XmlTextReader(filename))
{
while (textReader.Read())
{
if (textReader.Name== "Method")
{
if (textReader.HasAttributes)
{
// The current element is a method element
textReader.MoveToNextAttribute();
if (textReader.Name== "class")
{
string className = textReader.Value
textReader.MoveToNextAttribute();
hierarchy.Add(className + "." + textReader.Value);
}
else
{
// If the first attribute isn't a class name, this is normally
// because it's unmanaged
hierarchy.Add(textReader.Value);
}
}
else
{
// It's an end tag
hierarchy.RemoveAt((hierarchy.Count - 1));
}
}
else if (textReader.Name == "CPU")
{
textReader.MoveToNextAttribute();
if (textReader.Name == "ticks")
{
long cpuTicks = Int64.Parse(textReader.Value);
string hierarchyName = String.Join(":", hierarchy);
readResults[hierarchyName] = cpuTicks;
}
}
}
}
return readResults;
}
}
}
创建 NUnit 测试
此项目包含两个单独的 C# 文件。
第一个文件仅使用我们刚刚创建的 XML 读取器来读取结果文件。
using System.Collections.Generic;
namespace RedGate.NUnitProfilingSample
{
class DataSource
{
Public static Dictionary<string, long> Data()
{
ReadXml.ReadXml c = new ReadXml.ReadXml();
return c.XmlRead(@"..\..\..\ProfilerResults\testresults.xml");
}
}
}
另一个 C# 文件设置并运行 NUnit 测试。首先,读取基线结果,然后对从结果文件中返回的字典运行参数化测试。参数化测试检查每个方法是否在允许的容差范围内。
using System;
using System.Collections.Generic;
using NUnit.Framework;
namespace RedGate.NUnitProfilingSample
{
[TestFixture]
public class ComparisonTests
{
Dictionary<string, long> m_expectedResults;
[TestFixtureSetUp]
public void LoadExpectedResults()
{
// Reads the expected (baseline) results into a dictionary
m_expectedResults = new Dictionary<string, long>();
ReadXml.ReadXml c = new ReadXml.ReadXml();
m_expectedResults = c.XmlRead(
@"..\..\..\ProfilerResults\baselineResults.xml");
}
[Test]
public void TestPerformance([ValueSource(typeof(DataSource),
"Data")] KeyValuePair<string, long> data)
{
// Set this to the % tolerance permitted
const int tolerance = 33;
// Ensures the test doesn't fail if the baseline does not contain a
// method that is in the code being tested
if (!m_expectedResults.ContainsKey(data.Key))
{
return;
}
long expectedValue = m_expectedResults[data.Key];
long result = data.Value;
Assert.True(IsWithinPercentage(expectedValue, result, tolerance),
"Value from test ({0}) is not within {1}% of expected value ({2})",
result, tolerance, expectedValue);
}
// Checks that the value (y) is within +/- tolerance% (percentage) of the
// baseline value (x)
private static bool IsWithinPercentage(long x, long y, int percentage)
{
double percentageAsFraction = (double) percentage/100;
return y <= x*(1.0 + percentageAsFraction) && y >= x*(1.0 - percentageAsFraction);
}
}
}
步骤 4:将测试添加到现有的 NUnit 测试中
构建这两个项目代表的两个 DLL 后,将包含 NUnit 测试的 DLL 添加到构建时进行的其他测试中。
当测试运行时,NUnit 会检查是否有任何方法的运行时间比基线结果长 33% 以上(或短 33% 以上!)。我们使用 33%,因为反复试验表明这种容差提供了最有用的结果。当然,在运行测试框架的计算机上运行的其他任务会影响结果,因此即使没有更改代码,您也不会期望两次单独的测试完全相同。因此,我们建议您根据自己的实验,为您的应用程序确定最佳容差。
在下面的示例中,开发人员在 `GenerateReport()` 方法中添加了一行 `SpinWait()`,这会导致 CPU 空闲 90,000 个滴答。
static void GenerateReport(PermutationGenerator p)
{
string reportText = String.Empty;
foreach (var permutation in p.Permutations)
{
reportText += permutation + Environment.NewLine;
// waste time here
Thread.SpinWait(90000);
if (reportText.Length > 500000)
{
Console.WriteLine(reportText);
reportText = String.Empty;
}
}
Console.WriteLine(reportText);
}
当将此方法与不包含 `SpinWait()` 的基线进行比较时,`GenerateReport()` 的 NUnit 测试失败。此测试失败表明该更改导致方法的执行速度比先前版本慢至少 33%。
您可以 下载此示例中使用的文件 进行尝试。
结论
自动化测试长期以来一直用于持续集成,以确保尽可能快地发现错误。在本文中,我们展示了如何使用 ANTS Performance Profiler 扩展现有的 NUnit 测试以包含性能测试。
要试用 ANTS Performance Profiler,请 下载 14 天免费试用版。