使用Crank給我們的類庫做基準(zhǔn)測試
什么是 Crank
Crank?是.NET團(tuán)隊(duì)用于運(yùn)行基準(zhǔn)測試的基礎(chǔ)設(shè)施,包括(但不限于)TechEmpower Web Framework基準(zhǔn)測試中的場景。 Crank 第一次出現(xiàn)在公眾的視野應(yīng)該是在 .NET Conf 2021, @sebastienros 演講的?Benchmarking ASP.NET Applications with .NET Crank。
Crank 是 client-server (C/S) 的架構(gòu),主要有一個(gè)控制器 (Controller) 和一個(gè)或多個(gè)代理 (Agent) 組成。 其中控制器就是 client,負(fù)責(zé)發(fā)送指令;代理就是 server,負(fù)責(zé)執(zhí)行 client 發(fā)送的指令,也就是執(zhí)行具體的測試內(nèi)容。
下面是它的架構(gòu)圖。

可以看到,控制器和代理之間的交互是通過 HTTP 請求來驅(qū)動(dòng)的。然后代理可以執(zhí)行多個(gè)不同類型的作業(yè)類型。
我們這篇博客主要講的是圖中的?.NET project Job。
先來看看官方倉庫一個(gè)比較簡單的入門示例。
入門示例
首先要安裝 crank 相關(guān)的兩個(gè)工具,一個(gè)是控制器,一個(gè)是代理。
dotnet tool update Microsoft.Crank.Controller --version "0.2.0-*" --globaldotnet tool update Microsoft.Crank.Agent --version "0.2.0-*" --global
然后運(yùn)行官方倉庫上面的 micro 示例,是一個(gè) Md5 和 SHA 256 對(duì)比的例子。
public class Md5VsSha256{
? ?[ ] ? ?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);
? ?}
? ?[ ] ? ?public byte[] Sha256() => sha256.ComputeHash(data);
? ?[ ] ? ?public byte[] Md5() => md5.ComputeHash(data);
}
要注意的是 Main 方法,要用?BenchmarkSwitcher?來運(yùn)行,因?yàn)?Crank 是用命令行來執(zhí)行的,會(huì)附加一些參數(shù),也就是代碼中的 args。
public static void Main(string[] args){
? ?BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
}
然后是控制器要用到的配置文件,里面就是要執(zhí)行的基準(zhǔn)測試的內(nèi)容,要告訴代理怎么執(zhí)行。
# 作業(yè)jobs:
?# 作業(yè)名,自定義
?benchmarks:
? ?# 源相關(guān)內(nèi)容
? ?source:
? ? ?# 這里是本地文件夾,也可以配置遠(yuǎn)程 repository 和分支
? ? ?localFolder: .
? ? ?# 這個(gè)是具體的 csproj
? ? ?project: micro.csproj
? ?# 一些變量
? ?variables:
? ? ?filterArg: "*"
? ? ?jobArg: short
? ?# 參數(shù)
? ?arguments: --job {{jobArg}} --filter {{filterArg}} --memory
? ?options:
? ? ?# 使用 BenchmarkDotNet
? ? ?benchmarkDotNet: true# 場景 ? ?scenarios:
?# 場景名,自定義
?Md5VsSha256:
? ?application:
? ? ?# 與前面的定義作業(yè)名一致
? ? ?job: benchmarks# 檔案profiles:
?# 檔案名,自定義
?local:
? ?jobs:
? ? ?application:
? ? ? ?# 代理的地址
? ? ? ?endpoints:
? ? ? ? ?- http://localhost:5010
下面先來啟動(dòng)代理,直接運(yùn)行下面的命令即可。
crank-agent
會(huì)看到下面的輸出:
['C:\Users\catcherwong\AppData\Local\Temp\2\benchmarks-agent\benchmarks-server-8952\2mmqc00i.3b1'[ ] Agent ready, waiting for jobs...
] Created temp directory
默認(rèn)端口是 5010,可以通過?
-u|--url
?來指定其他的;如果運(yùn)行代理的電腦已經(jīng)安裝好 SDK 了,可以指定?--dotnethome
?避免因網(wǎng)絡(luò)問題導(dǎo)致無法正常下載 SDK。
然后是通過控制器向代理發(fā)送指令。
crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario Md5VsSha256 --profile local
上面的命令指定了我們上面的配置文件,同時(shí)還指定了 scenario 和 profile。因?yàn)榕渲梦募锌梢杂卸鄠€(gè) scenario 和 profile,所以在單次執(zhí)行是需要指定具體的一個(gè)。
如果需要執(zhí)行多個(gè) scenario 則需要執(zhí)行多次命令。
在執(zhí)行命令后,代理里面就可以看到日志輸出了:

最開始的是收到作業(yè)請求,然后安裝對(duì)應(yīng)的 SDK。安裝之后就會(huì)對(duì)指定的項(xiàng)目進(jìn)行 release 發(fā)布。

發(fā)布成功后就會(huì)執(zhí)行 BenchmarkDotNet 相關(guān)的內(nèi)容。

運(yùn)行完成后會(huì)輸出結(jié)果,最后清理這次基準(zhǔn)測試的內(nèi)容。
代理執(zhí)行完成后,可以在控制器側(cè)看到對(duì)應(yīng)的結(jié)果:

一般來說,我們會(huì)把控制器得到的結(jié)果保存在 JSON 文件里面,便于后續(xù)作對(duì)比或者要出趨勢圖。
這里可以加上?--json 文件名.json
。
crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario Md5VsSha256 --profile local --json base.json
運(yùn)行多次,將結(jié)果存在不同的 JSON 文件里,尤其代碼變更前后的結(jié)果。
crank --config C:\code\crank\samples\micro\micro.benchmarks.yml --scenario Md5VsSha256 --profile local --json head.json
最后是把這兩個(gè)結(jié)果做一個(gè)對(duì)比,就可以比較清楚的看到代碼變更是否有帶來提升。
crank compare base.json head.json

上面提到的還是在本地執(zhí)行,如果要在不同的機(jī)器上面執(zhí)行要怎么配置呢?
我們要做的是在配置文件中的 profiles 節(jié)點(diǎn)增加機(jī)器的代理地址即可。
下面是簡單的示例:
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 ? ? ?
這個(gè)時(shí)候,如果指定?--profile remote-win
?就是在?192.168.1.100
?這臺(tái)服務(wù)器執(zhí)行基準(zhǔn)測試,如果是?--profile remote-lin
?就是在?192.168.1.102
。
這樣就可以很輕松的在不同的機(jī)器上面執(zhí)行基準(zhǔn)測試了。
Crank 還有一個(gè)比較有用的功能是可以針對(duì) Pull Request 進(jìn)行基準(zhǔn)測試,這對(duì)一些需要基準(zhǔn)測試的開源項(xiàng)目來說是十分有幫助的。
接下來老黃就著重講講這一塊。
Pull Request
正常來說,代碼變更的肯定是某個(gè)小模塊,比較少出現(xiàn)多個(gè)模塊同時(shí)更新的情況,如果是有,估計(jì)也會(huì)被打回拆分!
所以我們不會(huì)選擇運(yùn)行所有模塊的基準(zhǔn)測試,而是運(yùn)行變更的那個(gè)模塊的基準(zhǔn)測試。
思路上就是有人提交 PR 后,由項(xiàng)目組成員在 PR 上面進(jìn)行評(píng)論來觸發(fā)基準(zhǔn)測試的執(zhí)行,非項(xiàng)目組成員的話不能觸發(fā)執(zhí)行。
下面就用這個(gè) Crank 提供的 Pull Request Bot 來完成后面的演示。
要想用這個(gè) Bot 需要先執(zhí)行下面的安裝命令:
dotnet tool update Microsoft.Crank.PullRequestBot --version "0.2.0-*" --global
安裝后會(huì)得到一個(gè)?crank-pr
?的文件,然后執(zhí)行?crank-pr
?的命令就可以了。

可以看到它提供了很多配置選項(xiàng)。
下面是一個(gè)簡單的例子
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
這個(gè)命令是什么意思呢?
它會(huì)對(duì)?catcherwong/library_with_crank?這個(gè)倉庫的 Id 為 1 的 Pull Request 進(jìn)行兩次基準(zhǔn)測試,一次是主分支的代碼,一次是 PR 合并后的代碼;基準(zhǔn)測試的內(nèi)容由 benchmarks,components 和 profiles 三個(gè)選項(xiàng)共同決定;最后兩個(gè)基準(zhǔn)測試的結(jié)果對(duì)比會(huì)在 PR 的評(píng)論上面。
其中 catcherwong/library_with_crank 是老黃提前準(zhǔn)備好的示例倉庫。
下面來看看?pr-benchmark.yml?的具體內(nèi)容
components:
? ?lib:
? ? ? ?script: |
? ? ? ? ? ?echo lib ? ? ? ?arguments:
? ? ? ? ? ?# crank arguments
? ? ? ? ? ?"--application.selfContained false"# default arguments that are always used on crank commandsdefaults: ""# the first value is the default if none is specifiedprofiles:
? ?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
?的參數(shù)拆分了到了不同的配置選項(xiàng)上面去了,運(yùn)行的時(shí)候就是把這些進(jìn)行組合。
再來看看?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
和前面入門的例子有點(diǎn)不一樣,我們在?scenarios?節(jié)點(diǎn) 里面加了一個(gè)?variables,這個(gè)和 jobs 里面定義的 variables 和 arguments 是相對(duì)應(yīng)的。
如果指定?--scenario dosomething
,那么最后得到的 arguments 就是
--job short --filter *DoSomething* --memory
后面就是來看看效果了。