背景

当我们写了一个类库提供给别人使用时,我们可能会对它做一些基准测试来测试一下它的性能指标,好比内存分配等。

在 .NET 的世界中,用 BenchmarkDotNet 来做这件事是非常不错的选择,我们只要写少量的代码就可以在本地运行基准测试然后得到结果。

这个在修改代码的时候,效果可能会更加明显,因为我们想知道我们的修改会不会使这段代码跑的更快,占用的资源更少。

作一个简单的假设,根据测试用例,代码变更之前,某方法在基准测试的分配的内存是 1M,修改之后变成 500K,那么我们可以认为这次的代码变更是有性能提升的,占用的资源更少了,当然这个得在单元测试通过的前提下。

试想一下,如果遇到下面的情况

  1. 想在多个不同配置的机器上面运行基准测试,好比 4c8g 的windows, 4c16g 的 linux
  2. Pull Request/Merge Request 做代码变更时,如何较好的做变更前后的基准测试比较

这个时候就会复杂一点了,要对一份代码在多个环境下面运行,做一些重复性的工作。

那么我们有没有办法让这个变得简单呢?答案是肯定的。

我们可以用 Crank 这个工具来完成这些内容。

什么是 Crank

Crank 是.NET团队用于运行基准测试的基础设施,包括(但不限于)TechEmpower Web Framework基准测试中的场景。 Crank 第一次出现在公众的视野应该是在 .NET Conf 2021, @sebastienros 演讲的 Benchmarking ASP.NET Applications with .NET Crank

Crank 是 client-server (C/S) 的架构,主要有一个控制器 (Controller) 和一个或多个代理 (Agent) 组成。 其中控制器就是 client,负责发送指令;代理就是 server,负责执行 client 发送的指令,也就是执行具体的测试内容。

下面是它的架构图。

可以看到,控制器和代理之间的交互是通过 HTTP 请求来驱动的。然后代理可以执行多个不同类型的作业类型。

我们这篇博客主要讲的是图中的 .NET project Job

先来看看官方仓库一个比较简单的入门示例。

入门示例

首先要安装 crank 相关的两个工具,一个是控制器,一个是代理。

dotnet tool update Microsoft.Crank.Controller --version "0.2.0-*" --global

dotnet tool update Microsoft.Crank.Agent --version "0.2.0-*" --global

然后运行官方仓库上面的 micro 示例,是一个 Md5 和 SHA 256 对比的例子。

public class Md5VsSha256
{
[Params(100, 500)]
public int N { get; set;}
private readonly byte[] data; private readonly SHA256 sha256 = SHA256.Create();
private readonly MD5 md5 = MD5.Create(); public Md5VsSha256()
{
data = new byte[N];
new Random(42).NextBytes(data);
} [Benchmark]
public byte[] Sha256() => sha256.ComputeHash(data); [Benchmark]
public byte[] Md5() => md5.ComputeHash(data);
}

要注意的是 Main 方法,要用 BenchmarkSwitcher 来运行,因为 Crank 是用命令行来执行的,会附加一些参数,也就是代码中的 args。

public static void Main(string[] args)
{
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}

然后是控制器要用到的配置文件,里面就是要执行的基准测试的内容,要告诉代理怎么执行。

# 作业
jobs:
# 作业名,自定义
benchmarks:
# 源相关内容
source:
# 这里是本地文件夹,也可以配置远程 repository 和分支
localFolder: .
# 这个是具体的 csproj
project: micro.csproj
# 一些变量
variables:
filterArg: "*"
jobArg: short
# 参数
arguments: --job {{jobArg}} --filter {{filterArg}} --memory
options:
# 使用 BenchmarkDotNet
benchmarkDotNet: true # 场景
scenarios:
# 场景名,自定义
Md5VsSha256:
application:
# 与前面的定义作业名一致
job: benchmarks # 档案
profiles:
# 档案名,自定义
local:
jobs:
application:
# 代理的地址
endpoints:
- http://localhost:5010

下面先来启动代理,直接运行下面的命令即可。

crank-agent

会看到下面的输出:

[11:42:30 INF] Created temp directory 'C:\Users\catcherwong\AppData\Local\Temp\2\benchmarks-agent\benchmarks-server-8952\2mmqc00i.3b1'
[11:42:30 INF] Agent ready, waiting for jobs...

默认端口是 5010,可以通过 -u|--url 来指定其他的;如果运行代理的电脑已经安装好 SDK 了,可以指定 --dotnethome 避免因网络问题导致无法正常下载 SDK。

然后是通过控制器向代理发送指令。

crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario  Md5VsSha256 --profile local

上面的命令指定了我们上面的配置文件,同时还指定了 scenario 和 profile。因为配置文件中可以有多个 scenario 和 profile,所以在单次执行是需要指定具体的一个。

如果需要执行多个 scenario 则需要执行多次命令。

在执行命令后,代理里面就可以看到日志输出了:

最开始的是收到作业请求,然后安装对应的 SDK。安装之后就会对指定的项目进行 release 发布。

发布成功后就会执行 BenchmarkDotNet 相关的内容。

运行完成后会输出结果,最后清理这次基准测试的内容。

代理执行完成后,可以在控制器侧看到对应的结果:

一般来说,我们会把控制器得到的结果保存在 JSON 文件里面,便于后续作对比或者要出趋势图。

这里可以加上 --json 文件名.json

crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario  Md5VsSha256 --profile local --json base.json

运行多次,将结果存在不同的 JSON 文件里,尤其代码变更前后的结果。

crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario  Md5VsSha256 --profile local --json head.json

最后是把这两个结果做一个对比,就可以比较清楚的看到代码变更是否有带来提升。

crank compare base.json head.json

上面提到的还是在本地执行,如果要在不同的机器上面执行要怎么配置呢?

我们要做的是在配置文件中的 profiles 节点增加机器的代理地址即可。

下面是简单的示例:

profiles:
local:
jobs:
application:
endpoints:
- http://localhost:5010
remote-win:
jobs:
application:
endpoints:
- http://192.168.1.100:9090
remote-lin:
jobs:
application:
endpoints:
- http://192.168.1.102:9090

这个时候,如果指定 --profile remote-win 就是在 192.168.1.100 这台服务器执行基准测试,如果是 --profile remote-lin 就是在 192.168.1.102

这样就可以很轻松的在不同的机器上面执行基准测试了。

Crank 还有一个比较有用的功能是可以针对 Pull Request 进行基准测试,这对一些需要基准测试的开源项目来说是十分有帮助的。

接下来老黄就着重讲讲这一块。

Pull Request

正常来说,代码变更的肯定是某个小模块,比较少出现多个模块同时更新的情况,如果是有,估计也会被打回拆分!

所以我们不会选择运行所有模块的基准测试,而是运行变更的那个模块的基准测试。

思路上就是有人提交 PR 后,由项目组成员在 PR 上面进行评论来触发基准测试的执行,非项目组成员的话不能触发执行。

下面就用这个 Crank 提供的 Pull Request Bot 来完成后面的演示。

要想用这个 Bot 需要先执行下面的安装命令:

dotnet tool update Microsoft.Crank.PullRequestBot --version "0.2.0-*" --global

安装后会得到一个 crank-pr 的文件,然后执行 crank-pr 的命令就可以了。

可以看到它提供了很多配置选项。

下面是一个简单的例子

crank-pr \
--benchmarks lib-dosomething \
--components lib \
--config ./benchmark/pr-benchmark.yml\
--profiles local \
--pull-request 1 \
--repository "https://github.com/catcherwong/library_with_crank" \
--access-token "${{ secrets.GITHUB_TOKEN }}" \
--publish-results true

这个命令是什么意思呢?

它会对 catcherwong/library_with_crank 这个仓库的 Id 为 1 的 Pull Request 进行两次基准测试,一次是主分支的代码,一次是 PR 合并后的代码;基准测试的内容由 benchmarks,components 和 profiles 三个选项共同决定;最后两个基准测试的结果对比会在 PR 的评论上面。

其中 catcherwong/library_with_crank 是老黄提前准备好的示例仓库。

下面来看看 pr-benchmark.yml 的具体内容

components:
lib:
script: |
echo lib
arguments:
# crank arguments
"--application.selfContained false" # default arguments that are always used on crank commands
defaults: "" # the first value is the default if none is specified
profiles:
local:
description: Local
arguments: --profile local
remote-win:
description: windows
arguments: --profile remote-win
remote-lin:
description: linux
arguments: --profile remote-lin benchmarks:
lib-dosomething:
description: DoSomething
arguments: --config ./benchmark/library.benchmark.yml --scenario dosomething lib-getsomething:
description: GetSomething
arguments: --config ./benchmark/library.benchmark.yml --scenario getsomething lib-another:
description: Another
arguments: --config ./benchmark/library.benchmark.yml --scenario another

基本上可以说是把 crank 的参数拆分了到了不同的配置选项上面去了,运行的时候就是把这些进行组合。

再来看看 library.benchmark.yml

jobs:
lib:
source:
localFolder: ../src
project: BenchmarkLibrary/BenchmarkLibrary.csproj
variables:
filter: "*"
jobArg: short
arguments: --job {{jobArg}} --filter {{filter}} --memory
options:
benchmarkDotNet: true scenarios:
dosomething:
application:
job: lib
variables:
filter: "*DoSomething*" getsomething:
application:
job: lib
variables:
filter: "*GetSomething*" another:
application:
job: lib
variables:
filter: "*Method*" profiles:
local:
jobs:
application:
endpoints:
- http://localhost:9999 remote-lin:
jobs:
application:
endpoints:
- http://remote-lin.com remote-win:
jobs:
application:
endpoints:
- http://remote-win.com

和前面入门的例子有点不一样,我们在 scenarios 节点 里面加了一个 variables,这个和 jobs 里面定义的 variables 和 arguments 是相对应的。

如果指定 --scenario dosomething,那么最后得到的 arguments 就是

--job short --filter *DoSomething* --memory

后面就是来看看效果了。

这里省略了评论内容的解析,也就是评论什么内容的时候会触发执行,因为这一块不是重点,有兴趣可以看 workflow 的脚本即可。

具体的执行过程可以参考

https://github.com/catcherwong/library_with_crank/actions/runs/4598397510/jobs/8122376959

当然,如果条件允许的话,也可以用自己的服务器资源来跑基准测试,不用 Github Action 提供的资源。

这样的好处是相对稳定,可以自己根据场景指定不同配置的服务器。不过对一些没那么复杂类库,用 Github Action 的资源也是无伤大雅的。

下面这个截图就是在提交到外部服务器上面执行的。

如果仓库不是在 Github,是在自建 Gitlab 或者其他的,就可以根据这个思路来自定义流水线从而去完成这些基准测试的操作。

总结

Crank 还是一个挺不错的工具,可以结合 BenchmarkDotNet 来做类库的基准测试,也可以结合 wrk/wrk2/bombardier/h2load 等压测工具进行 api/grpc 框架和应用的测试。

这里只介绍了其中一个小块的内容,还有挺多内容可以挖掘一下的。

最后是本文的示例代码:

https://github.com/catcherwong/library_with_crank

参考资料

聊一聊如何使用Crank给我们的类库做基准测试的更多相关文章

  1. C++使用RabbitMQ类库做客户端与RabbitMQ Server通讯,生成C++可调用的rabbimq.*.dll的过程

    Step: download the latest rabbitmq-c via: https://github.com/alanxz/rabbitmq-c follow the document, ...

  2. 编写Javascript类库(jQuery版) - 进阶者系列 - 学习者系列文章

    这些年主要关注于项目管理方面的工作,编码就比较少了.这几天比较空闲,就想把原来的经验沉淀下来,一个是做好记录,以后如果忘记了还能尽快找回来,第二个是写写博文,算是练练手笔吧. 言归正传,这次写的是Ja ...

  3. iOS平台XML解析类库对比和安装说明

    在iPhone开发中,XML的解析有很多选择,iOS SDK提供了NSXMLParser和libxml2两个类库,另外还有很多第三方类库可选,例如TBXML.TouchXML.KissXML.Tiny ...

  4. 优雅地使用CodeIgniter 3之Session类库(1)(转)

    相信无数人在使用CI2的Session类库时,遇到各种的坑,各种抱怨,各种不解.在CI中国论坛能搜到大量关于Session类库的提问,说明要想用 好session类库还是得下一番功夫.本文将先从CI2 ...

  5. Windows.Andy.Code4App.dll Win8.1/WP8.1通用类库@ver1.0.1

    在上篇 Windows.Andy.Code4App.dll  Win8.1/WP8.1通用类库@ver1.0.0 已经对Win8.1和WP8.1部分扩展通用类库做了说明,这篇继续对通用类库做扩展.写的 ...

  6. 编写Javascript类库(jQuery版

    编写Javascript类库(jQuery版) - 进阶者系列 - 学习者系列文章 Posted on 2014-11-13 09:29 lzhdim 阅读(653) 评论(1) 编辑 收藏 本系列文 ...

  7. Java——容器类库框架浅析

    前言 通常,我们总是在程序运行过程中才获得一些条件去创建对象,这些动态创建的对象就需要使用一些方式去保存.我们可以使用数组去存储,但是需要注意数组的尺寸一旦定义便不可修改,而我们并不知道程序在运行过程 ...

  8. CamStar insitexmlclient重新封装为.net Core类库

    工作原因经常使用camstar的 InsiteXMLClient类库做二次开发,但是只能在4.X环境下使用,对于日益繁荣的.net core生态,花费了些时间把原有的类库重新封装为.net core ...

  9. Xamarin+Prism小试牛刀:定制跨平台Outlook邮箱应用(后续)

    在[Xamarin+Prism小试牛刀:定制跨平台Outlook邮箱应用]里面提到了Microsoft 身份认证,其实这也是一大块需要注意的地方,特作为后续补充这些知识点.上章是使用了Microsof ...

  10. ABP理论之CSRF

    返回总目录 本篇目录 介绍 ASP.NET MVC ASP.NET WEB API ASP.NET Core[以后补上] 客户端类库 内部原理 介绍 CSRF[Cross-Site Request F ...

随机推荐

  1. Jetpack compose学习笔记之列表(布局)

    一,简介 Jetpack compose中的布局主要分为Column,Row,Box. 二,Column创建的列表 Column创建list时,不管内容是在屏幕内还是屏幕外,都会将list的内容全部创 ...

  2. Mysql 查询最近一年的数量sql

    按月查询分月数量 , 某个月份没有的显示为0 SELECT@s := @s + 1 AS `index`,DATE_FORMAT( DATE_SUB( CURDATE(), INTERVAL @s M ...

  3. 对象和类复习题(c++ prime plus )

    第一题: 为复习题5描述的类提供方法定义,并编写一个小程序来演示所有的特性: bank.h #ifndef BACK_H_ #define BACK_H_ #include <string> ...

  4. uni-app之返回上⼀个页⾯并传递参数(从页⾯1跳转到页⾯2,然后页⾯2返回页⾯1并且带回参数。 列如新增地址,带回经纬度和地址名称)

    uni-app之返回上⼀个页⾯并传递参数 1 reBack: function() { 2 let pages = getCurrentPages(); 3 if (pages.length > ...

  5. nodejs批量重命名

    const fs = require("fs"); // directory path let config = {   affix: null,   src: null, }; ...

  6. c# iis网站发布

    c# iis网站发布 问题一: 有很多人在用服务器发布网站的时候,一直出现 "HTTP 错误 403.14 - Forbidden Web 服务器被配置为不列出此目录的内容",那么 ...

  7. 备份linux系统日志脚本

    #!/bin/bash#script_name bkup_log.sh#7 0 * * 1 cd /home/tools/;./bkup_log.sh >& /dev/nullproce ...

  8. 自定义组件中使用half-screen-dailog 无法舒勇外部样式

    为了将弹窗更方便的调用,封装了组件,但是发现一个问题, 外部样式类在组件中是无法正常使用的. 也就说在组件的wxss中是无法正常使用.weui-half-screen-dialog 这个外部样式类,然 ...

  9. 整数划分问题(Java递归)

    整数划分问题(Java递归) 文章目录 整数划分问题(Java递归) 0. 问题描述 1.递归式 2.代码 3.参考 0. 问题描述 整数划分问题 将正整数n表示成一系列正整数之和:n=n1+n2+- ...

  10. 使用*打印出2*n-1行的菱形

    /** * @Author * @Description //TODO 2*n+1菱形 * @Date * @Param int * @return **/ public static void pr ...