65.9K
CodeProject 正在变化。 阅读更多。
Home

将性能分析集成到构建过程中

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011年4月12日

CPOL

4分钟阅读

viewsIcon

21872

探讨如何将性能分析集成到构建过程中,作为自动化测试套件的一部分。

引言

许多开发人员都明白在发布前使用性能分析器的重要性,但发布前几天才发现问题可能会让人头疼。您可能需要花几周时间来梳理工作,以便更改导致程序运行缓慢的方法,并且在问题解决后,可能没有时间像平常那样仔细地测试解决方案。在此期间,您还承受着压力,这增加了犯错的风险。如果在整个开发过程中对程序进行分析,无疑会更好,但很少有团队有足够的闲暇时间定期进行此操作。

如果您已采用持续集成方法,那么您可能已经在构建系统中部分地整合了自动化测试。如果您可以使用现有的测试工具来自动检查应用程序的性能,每次测试运行时,岂不是很好?这样做的好处是可以及时发现提交的代码中的问题,帮助您更快地找到问题及其解决方案。甚至可能在您的老板发现问题之前就找到问题!

在本教程中,我将演示如何使用 Red Gate 的 ANTS Performance Profiler 在 NUnit 测试中比较每种方法与已知基线的 CPU 滴答数。如果滴答数超过 33% 的阈值,NUnit 测试将失败。

自动化性能分析

背景

ANTS Performance Profiler 6 引入了命令行界面,允许在没有图形用户界面的情况下运行分析会话。然后可以将结果导出到 XML 文件。此处描述的程序依赖于比较两个 XML 结果文件中的值,一个用于已知基线,另一个用于您刚刚构建的版本。

为简单起见,我假设您的应用程序可以完全从命令行运行,无需交互;如果不是这种情况,您可能需要单独测试应用程序的不同部分。

请注意,本教程使用参数化 NUnit 测试,这需要 NUnit 2.5 或更高版本。

步骤 1:记录基线结果集

第一步自然是记录一个已知性能良好的应用程序现有构建的结果集。在命令行启动 ANTS Performance Profiler,并将结果保存到 XML 文件中。

image001.jpg

确保将基线结果保存在您的测试工具可以读取的位置。

步骤 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%。

image002.png

image003.png

您可以 下载此示例中使用的文件 进行尝试。

结论

自动化测试长期以来一直用于持续集成,以确保尽可能快地发现错误。在本文中,我们展示了如何使用 ANTS Performance Profiler 扩展现有的 NUnit 测试以包含性能测试。

要试用 ANTS Performance Profiler,请 下载 14 天免费试用版

© . All rights reserved.