作为一个行业,我们正在采用更高透明度和更可预测的构建过程,以降低构建软件的风险。持续交付的核心原则之一是通过反馈循环收集反馈。在 Dev9,我们采用了与 CD 原则一致的“先知道”原则,这意味着我们(开发团队)希望第一个知道什么时候出现故障、性能下降或任何与业务目标不一致的结果。
Maven 和其他构建工具为开发人员提供了一个标准化的工具和生态系统,用于建立和交流反馈。虽然单元测试、功能、构建验收、数据库迁移、性能测试和代码分析工具已成为开发管道中的支柱,但基准测试在很大程度上仍处于流程之外。这可能是由于缺乏开源、低成本的工具或增加最小复杂性的轻量级库。
现有工具通常需要将外部工具与运行时工件集成,并且测试不会保存在同一源存储库中,甚至不会存储在源存储库中,从而使复杂性增加。本地开发人员无法毫不费力地运行基准测试,因此测试很快就会失去价值。除了主流解决方案问题之外,基准测试通常不会在课堂上教授,而且通常在没有必要隔离以收集可靠结果的情况下实施。这使得所有关于基准测试结果的博客或帖子成为喷子的成熟目标。
综上所述,在代码库的关键区域周围放置某种基准测试覆盖范围仍然非常重要。建立关于代码关键部分的历史知识有助于影响优化工作、告知团队技术债务、在提交性能阈值更改时发出警报以及比较以前或新版本的算法。现在的问题应该是,如何找到并轻松地将基准测试添加到我的新项目或现有项目中。在这篇博客中,我们将重点关注 Java 项目 (1.7+)。示例代码将使用 Maven,尽管 Gradle 的工作方式非常相似。我在整个博客中提出了一些建议,它们基于过去项目的经验。
在寻找基于 Java 的代码基准时,有很多不错的选择,但其中大多数都有缺点,包括许可费用、额外的工具、字节代码操作和/或 Java 代理、使用非基于 Java 的代码概述的测试和高度复杂的配置设置。我喜欢让测试尽可能接近被测代码,以减少脆弱性、降低内聚和减少耦合。我认为我以前使用的大多数基准测试解决方案都太麻烦而无法使用,或者运行测试的代码不够隔离(字面上集成在代码中)或包含在远离源代码的辅助解决方案中。
此博客的目的是演示如何将轻量级基准测试工具添加到您的构建管道,因此我不会详细介绍如何使用 JMH,以下博客是学习的极好资源:
关于模式和评分,我想指出少量项目,因为它们在如何设置基本配置方面发挥着重要作用。在基本层面上,JMH 有两种主要的度量类型:吞吐量和基于时间的度量。
吞吐量是单位时间内可以完成的操作量。随着框架增加测试负载量,JMH 会维护成功和失败操作的集合。注意:确保方法或测试被很好地隔离,并且测试对象创建等依赖项在方法之外完成或在设置方法中进行预测试。对于吞吐量,值越高越好,因为它表示每单位时间可以运行更多的操作。
基于时间的测量是吞吐量的对应物。基于时间的测量的目标是确定特定操作每单位时间运行的时间。
平均时间
最常见的基于时间的测量是“AverageTime”,它计算操作的平均时间。 JMH 还将生成“分数错误”,以帮助确定对生成的分数的信心。 “得分误差”通常是置信区间的 1/2,表示结果与平均时间的偏差程度。结果越低越好,因为它表明每个操作的平均运行时间越短。
采样时间
SampleTime 类似于 AverageTime,但 JMH 尝试推动更多负载并寻找故障,从而生成故障百分比矩阵。对于 AverageTime,数字越低越好,百分比有助于确定您对因吞吐量和时间长度导致的故障感到满意的程度。
单发时间
最后一个也是最不常用的模式是 SingleShotTime。此模式实际上是单次运行,可用于冷测试方法或测试测试。如果在运行基准测试时作为参数传入,SingleShotTime 可能会很有用,但会减少运行测试所需的时间(尽管这会降低测试的价值并可能使它们变得无用)。与其他基于时间的测量一样,值越低越好。
目标: 本节将展示如何创建可重复的工具,以允许以最少的开销或代码重复添加新测试。请注意,依赖项位于“测试”范围内,以避免将 JMH 添加到最终工件中。我创建了一个使用 JMH 的 github 存储库,同时致力于微服务 REST 的 Protobuf 替代方案。代码可以在这里找到:https://github.com/mike-ensor/protobuf-serialization
1)首先将依赖项添加到项目中:
<dependencies> <!-- Other libraries left out for brevity --> <!-- jmh.version is the lastest version of JMH. Find by visiting http://search.maven.org --> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-core</artifactId> <version>${jmh.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-generator-annprocess</artifactId> <version>${jmh.version}</version> <scope>test</scope> </dependency> <!-- Other libraries left out for brevity --> </dependencies>
2) JMH 建议将基准测试和工件打包在同一个 uber jar 中。有几种方法可以实现 uber jar,显式使用 maven 的“shade”插件或隐式使用 Spring Boot、Dropwizard 或具有类似结果的某些框架。出于这篇博文的目的,我使用了一个 Spring Boot 应用程序。
3) 添加一个带有主入口类和全局配置的测试工具。在此步骤中,在项目的 test 区域创建一个入口点(用 #1 表示)。目的是避免将基准测试代码与主要工件打包在一起。
3.1) 添加BenchmarkBase文件(如上所示#2)。该文件将作为基准测试的入口点,并包含测试的所有全局配置。我编写的类查找包含配置属性的“benchmark.properties”文件(如上#3 所示)。 JMH 有一个输出文件结果的选项,这个配置是为 JSON 设置的。结果与您的持续集成工具结合使用,并且可以(应该)存储以供历史使用。
此代码段是 Maven 运行的基准测试过程的基础线束和入口点(在下面的第 5 步中设置)此时,该项目应该能够运行基准测试,所以让我们添加一个测试用例。
@SpringBootApplication public class BenchmarkBase { public static void main(String[] args) throws RunnerException, IOException { Properties properties = PropertiesLoaderUtils.loadAllProperties("benchmark.properties"); int warmup = Integer.parseInt(properties.getProperty("benchmark.warmup.iterations", "5")); int iterations = Integer.parseInt(properties.getProperty("benchmark.test.iterations", "5")); int forks = Integer.parseInt(properties.getProperty("benchmark.test.forks", "1")); int threads = Integer.parseInt(properties.getProperty("benchmark.test.threads", "1")); String testClassRegExPattern = properties.getProperty("benchmark.global.testclassregexpattern", ".*Benchmark.*"); String resultFilePrefix = properties.getProperty("benchmark.global.resultfileprefix", "jmh-"); ResultFormatType resultsFileOutputType = ResultFormatType.JSON; Options opt = new OptionsBuilder() .include(testClassRegExPattern) .warmupIterations(warmup) .measurementIterations(iterations) .forks(forks) .threads(threads) .shouldDoGC(true) .shouldFailOnError(true) .resultFormat(resultsFileOutputType) .result(buildResultsFileName(resultFilePrefix, resultsFileOutputType)) .shouldFailOnError(true) .jvmArgs("-server") .build(); new Runner(opt).run(); } private static String buildResultsFileName(String resultFilePrefix, ResultFormatType resultType) { LocalDateTime date = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("mm-dd-yyyy-hh-mm-ss"); String suffix; switch (resultType) { case CSV: suffix = ".csv"; break; case SCSV: // Semi-colon separated values suffix = ".scsv"; break; case LATEX: suffix = ".tex"; break; case JSON: default: suffix = ".json"; break; } return String.format("target/%s%s%s", resultFilePrefix, date.format(formatter), suffix); } }
4) 创建一个类来对操作进行基准测试。请记住,基准测试将针对整个方法主体运行,这包括日志记录、文件读取、外部资源等。请注意您要进行基准测试的内容并减少或删除依赖项以隔离您的主题代码以确保对结果的信心更高。在这个例子中,配置设置在
@State(Scope.Benchmark) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) public class SerializationBenchmark { private RecipeService service; private Recipe recipe; private byte[] protoRecipe; private String recipeAsJSON; @Setup(Level.Trial) public void setup() { IngredientUsed jalepenoUsed = new IngredientUsed(new Ingredient("Jalepeno", "Spicy Pepper"), MeasurementType.ITEM, 1); IngredientUsed cheeseUsed = new IngredientUsed(new Ingredient("Cheese", "Creamy Cheese"), MeasurementType.OUNCE, 4); recipe = RecipeTestUtil.createRecipe("My Recipe", "Some spicy recipe using a few items", ImmutableList.of(jalepenoUsed, cheeseUsed)); service = new RecipeService(new ObjectMapper()); protoRecipe = service.recipeAsProto(recipe).toByteArray(); recipeAsJSON = service.recipeAsJSON(recipe); } @Benchmark public Messages.Recipe serialize_recipe_object_to_protobuf() { return service.recipeAsProto(recipe); } @Benchmark public String serialize_recipe_object_to_JSON() { return service.recipeAsJSON(recipe); } @Benchmark public Recipe deserialize_protobuf_to_recipe_object() { return service.getRecipe(protoRecipe); } @Benchmark public Recipe deserialize_json_to_recipe_object() { return service.getRecipe(recipeAsJSON); } }
标题:此要点是从 Protobuf Serialization 中提取的示例基准测试用例
当您执行测试 jar 时,您所有的 *Benchmark*.java 测试类现在都将运行,但这通常并不理想,因为过程没有隔离,并且对基准测试何时以及如何运行进行一些控制对于保持构建时间很重要向下。
让我们构建一个 Maven 配置文件来控制何时运行基准测试并可能启动应用程序。请注意,为了显示 Maven 集成测试启动/停止服务器,我已将其包含在博客文章中。我会提醒您启动或停止应用程序服务器的必要性,因为您可能会招致资源获取(REST 调用)的成本,这不会是非常孤立的。
5) 概念是创建一个 Maven 配置文件以隔离运行所有基准测试(即没有单元或功能测试)。这将允许基准测试与构建管道的其余部分并行运行。请注意,代码使用“exec”插件并运行 uber jar 以查找主类的完整类路径。此外,可执行范围仅限于“测试”源,以避免将基准代码放入最终工件中。
<profile> <id>benchmark</id> <properties> <maven.test.ITests>true</maven.test.ITests> </properties> <build> <plugins> <!-- Start application for benchmarks to test against --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <id>pre-integration-test</id> <goals> <goal>start</goal> </goals> </execution> <execution> <id>post-integration-test</id> <goals> <goal>stop</goal> </goals> </execution> </executions> </plugin> <!-- Turn off unit tests --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <configuration> <excludes> <exclude>**/*Tests.java</exclude> <exclude>**/*Test.java</exclude> </excludes> </configuration> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>1.5.0</version> <executions> <execution> <goals> <goal>exec</goal> </goals> <phase>integration-test</phase> </execution> </executions> <configuration> <executable>java</executable> <classpathScope>test</classpathScope> <arguments> <argument>-classpath</argument> <classpath /> <argument>com.dev9.benchmark.BenchmarkBase</argument> <argument>.*</argument> </arguments> </configuration> </plugin> </plugins> </build> </profile>
此代码段显示了一个示例 Maven 配置文件,仅运行基准测试。
6) 最后,可选项目是在您的持续集成构建管道中创建一个可运行的构建步骤。为了单独运行基准测试,您或您的 CI 可以运行:
mvn clean verify -Pbenchmark
如果您使用的是基于 Java 的项目,那么将 JMH 添加到您的项目和管道中相对容易。与项目的关键领域相关的历史分类账的好处对于保持较高的质量标准非常有用。将 JMH 添加到您的管道中还遵循持续交付原则,包括反馈循环、自动化、可重复和持续改进。考虑向解决方案的关键区域添加 JMH 线束和一些测试。
标签2: Java教程地址:https://www.cundage.com/article/jcg-adding-microbenchmarking-build-process.html