ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

在 .NET MAUI Android 应用中使用 BenchmarkDotNet 运行基准测试:完整实战指南

在 .NET MAUI Android 应用中使用 BenchmarkDotNet 运行基准测试:完整实战指南 在 .NET MAUI Android 应用中使用 BenchmarkDotNet 运行基准测试完整实战指南【免费下载链接】maui.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.项目地址: https://gitcode.com/GitHub_Trending/ma/maui导读本文基于 .NET MAUI 官方仓库maui中 Benchmarks.Droid 的 README 展开完整讲解如何在 Android 真机上通过 BenchmarkDotNet 对 .NET MAUI 核心控件与图片加载链路做可复现的性能测量。读者将掌握一键构建并执行基准测试的命令、背后由自定义 MSBuild Target Android Instrumentation组成的执行机制、基准测试用例的编写方式以及如何读懂 logcat 输出的 BenchmarkDotNet 汇总报告从而在自己的 MAUI 性能工作中复现这套方案。背景为什么要在 Android 上跑 BenchmarkDotNetBenchmarkDotNet 是 .NET 生态最常用的基准测试框架但它的典型运行方式是面向常规 .NET 控制台或桌面宿主。而 .NET MAUI 的 Android 应用运行在单文件trimmed的 AOT/解释执行环境中其控件创建、Handler 映射、布局与图片加载路径与桌面宿主差异很大——尤其是Microsoft.Maui.Controls的Border、ContentView、Label、Entry等控件在 Android 上经由 Handler 桥接到原生视图其开销只有在真实 Android 运行时中测量才有意义。仓库中的 Benchmarks.Droid 项目正是为此而生它把一个可运行的 Android 应用与 BenchmarkDotNet 集成在一起README 明确说明This has a number of hacks to get BenchmarkDotNet running in a .NET 6 Android application——即为了在 Android 宿主中跑通 BenchmarkDotNet 使用了不少技巧其中一部分甚至可以回馈到 BenchmarkDotNet 未来的版本中。核心要点是你不能像普通应用那样直接点开 App 来跑基准测试。README 强调 you cant simply run the app to run the benchmarks而是通过一个 Android instrumentation 类配合自定义 MSBuild target 来驱动整个基准测试流程。一键运行自定义 MSBuild Target 驱动基准测试在仓库根目录执行以下命令即可完成构建 → 安装 APK → 启动 instrumentation → 拉取 logcat 日志的完整流程./bin/dotnet/dotnet build src/Core/tests/Benchmarks.Droid/Benchmarks.Droid.csproj -t:Benchmark -c Release-t:Benchmark指定执行项目内自定义的BenchmarkMSBuild target而不是默认的 Build-c Release使用 Release 构建。README 特别提醒在物理设备上记录最终耗时数据时务必使用 Release 构建Use Release builds when recording your final timing on a physical device因为 Debug 构建未经优化且携带大量诊断信息测出的数据没有参考价值。该命令会先完成编译然后自动把 APK 安装到已连接的设备或模拟器上随后通过adb shell am instrument启动基准测试结束后再抓取 logcat 中过滤后的结果打印到终端。整个基准测试运行耗时较长示例输出中Run time: 00:01:50构建信息与实时进度可以通过adb logcat观察。从命令到落地BenchmarkTarget 与MainInstrumentation的执行链命令中-t:Benchmark背后是 Benchmarks.Droid.csproj 中定义的 MSBuild target它把安装 → 跑测试 → 取日志三步串成了流水线Target NameBenchmark DependsOnTargetsInstall Message TextRunning benchmarks. This might take a while... See adb logcat for realtime progress. ImportanceHigh / !-- 1. 清空 logcat保证日志干净 -- Exec Commandadb shell logcat -c WorkingDirectory$(AndroidSdkDirectory)/platform-tools / !-- 2. 通过 am instrument 启动 instrumentation 执行基准测试 -- Exec Commandadb shell am instrument -w $(ApplicationId)/com.microsoft.maui.MainInstrumentation WorkingDirectory$(AndroidSdkDirectory)/platform-tools / !-- 3. 打印过滤后的日志只保留 DOTNET 与 MAUI 两个 tag -- Exec Commandadb shell logcat -d -v tag -s quot;DOTNET,MAUIquot; IgnoreStandardErrorWarningFormattrue StdErrEncodingutf-8 StdOutEncodingutf-8 WorkingDirectory$(AndroidSdkDirectory)/platform-tools / /Target三个步骤的职责分别是adb shell logcat -c先清空日志缓冲确保接下来抓到的内容全部来自本次基准测试adb shell am instrument -w com.microsoft.maui.benchmarks/com.microsoft.maui.MainInstrumentation以同步-w方式启动 Android instrumentation这是执行基准测试的入口adb shell logcat -d -v tag -s DOTNET,MAUI-d表示 dump 后退出-v tag让每行日志仅保留 tag 信息-s DOTNET,MAUI只显示这两个 tag 的日志从而把 BenchmarkDotNet 的汇总报告与 MAUI 自身的状态日志干净地过滤出来。关键工程决策为什么用 Instrumentation 而不是 Activity普通 Android 应用的主入口是MainActivity而此项目的主 ActivityMainActivity.cs只是一个占位壳——它仅仅SetContentView(Resource.Layout.activity_main)加载布局不承载任何基准测试逻辑。真正的执行入口是 MainInstrumentation.cs 中通过[Instrumentation(Name com.microsoft.maui.MainInstrumentation)]声明的类public class MainInstrumentation : Instrumentation { const string Tag MAUI; public static MainInstrumentation? Instance { get; private set; } public static string ExternalDataDirectory { get; private set; } string.Empty; protected MainInstrumentation(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { } public override void OnCreate(Bundle? arguments) { base.OnCreate(arguments); Instance this; ExternalDataDirectory Context?.GetExternalFilesDir(null)?.ToString() ?? string.Empty; if (string.IsNullOrEmpty(ExternalDataDirectory)) { Log.Error(Tag, ExternalDataDirectory is failed to be set); return; } Log.Debug(Tag, $ExternalDataDirectory: {ExternalDataDirectory}); Start(); } public async override void OnStart() { // ... var success await Task.Factory.StartNew(Run); Log.Debug(Tag, $Benchmark complete, success: {success}); Finish(success ? Result.Ok : Result.Canceled, new Bundle()); } // ... }选择Instrumentation而非Activity的原因很直接instrumentation 由am instrument命令启动、在OnStart中异步执行任务、完成后通过Finish()把成功/失败结果回传给调用方Result.Ok或Result.Canceled。这让基准测试可以完全脱离 UI 生命周期以无界面、可脚本化、可判定成败的方式运行正好适配 CI 与自动化场景。Instance静态实例与ExternalDataDirectory还会暴露给基准测试类使用例如ImageBenchmark的GlobalSetup就是通过MainInstrumentation.Instance!.Context!获取上下文的。BenchmarkDotNet 配置进程内执行、内存诊断与结果排序MainInstrumentation.cs 的Run()方法集中体现了为 Android 量身定制的 BenchmarkDotNet 配置static bool Run() { bool success false; try { var config ManualConfig.CreateMinimumViable() .AddJob(Job.Default.WithToolchain(new InProcessEmitToolchain(TimeSpan.FromMinutes(10), logOutput: true))) .AddDiagnoser(MemoryDiagnoser.Default) .WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Alphabetical)); // ImageBenchmark class is hardcoded here for now BenchmarkRunner.RunImageBenchmark(config); BenchmarkRunner.RunViewHandlerBenchmark(config); success true; } catch (Exception ex) { Log.Error(Tag, $Error: {ex}); } return success; }各配置项的作用与背后的设计意图ManualConfig.CreateMinimumViable()关闭 BenchmarkDotNet 默认的完整配置导出器、分析器等只保留最小可用集合减少 Android 上的额外开销Job.Default.WithToolchain(new InProcessEmitToolchain(...))这是 Android 上的关键 hack。默认情况下 BenchmarkDotNet 会生成一个独立的基准进程来隔离测量这在 Android 的进程模型下不可行InProcessEmitToolchain让基准测试在当前进程内通过 Emit 生成的代码执行TimeSpan.FromMinutes(10)设置超时上限logOutput: true让 BenchmarkDotNet 的输出直接打到 logcat。这也解释了示例输出中的ToolchainInProcessEmitToolchain一行.AddDiagnoser(MemoryDiagnoser.Default)启用内存分配诊断输出中Gen 0与Allocated两列即来源于此.WithOrderer(new DefaultOrderer(SummaryOrderPolicy.FastestToSlowest, MethodOrderPolicy.Alphabetical))结果按耗时从快到慢排序方法名按字母序排列方便快速定位性能热点异常被捕获并记录到MAUItag最终以布尔值决定Finish的结果码保证 CI 能识别失败。另外OnStart中有一段#if PERFLAB_INLAB条件编译块用于在 Perflab微软内部性能实验室环境预置PERFLAB_*、DOTNET_VERSION、HELIX_*等环境变量并创建外部数据目录说明这套工程在仓库之外还被接入到自动化性能采集流水线中PERFLAB_BUILDNUM、PERFLAB_HASH等以REPLACE_*占位符形式出现供构建时替换。基准测试用例解读Handler 创建与图片加载两条链路当前工程硬编码了两个基准类注释 ImageBenchmark class is hardcoded here for nowViewHandlerBenchmark控件 Handler 创建链路ViewHandlerBenchmark.cs 衡量的是创建 MAUI 控件并连接 Handler这一核心路径的成本。每个[Benchmark]方法的结构一致先new一个具体 Handler如BorderHandler调用SetMauiContext(_context)注入轻量级MauiContext再创建对应控件并赋值给Handler属性从而触发虚拟视图到原生视图的桥接[Benchmark] public void Border() { var handler new BorderHandler(); handler.SetMauiContext(_context); new Border { Handler handler, }; }覆盖的控件包括Border、ContentView、ActivityIndicator、Label、Entry另有被注释掉的BoxView源码注释// FIXME: BoxView hits an exception on API 31 in Maui.Graphics表明该用例在 API 31 上触发过异常而被停用这正是设备端基准测试能暴露真实问题的例证。该文件的MauiContext与ServiceProvider是刻意裁剪的最小实现ServiceProvider只实现了IFontManager的服务解析内部为FontManager(new FontRegistrar(new EmbeddedFontLoader()))其余GetHandler/GetHandlerType等直接抛出NotImplementedException。这说明基准聚焦于控件 Handler 创建本身不引入完整的 MAUI DI 容器以隔离被测变量。ImageBenchmark图片解码与加载链路ImageBenchmarks.cs 比较三种 Android 图片加载路径SetImageResource直接从资源 ID 同步设置SetImageDrawable先GetDrawable再设置ImageHelperFromFile/ImageHelperFromFont走Microsoft.Maui.PlatformInterop.LoadImageFromFile/LoadImageFromFont的异步加载并通过自实现Callback实现IImageLoaderCallback内部用TaskCompletionSource把原生回调转成Task等待异步完成——因此这两个用例的返回类型是async Task。[GlobalSetup]阶段把 Assets 里的dotnet_bot.png复制到FileSystem.CacheDirectory通过MainInstrumentation.Instance.Context与GetExternalFilesDir[GlobalCleanup]释放ImageView。这些用例直接体现了真机图片 I/O 与解码开销与MAUI 图片辅助 API 相对原生路径的开销差。读懂输出Summary、Legends 与结束标记运行结束后MSBuild target 会把 logcat 中DOTNET与MAUI两个 tag 的日志完整打印出来典型输出形如I/DOTNET : // * Summary * I/DOTNET : I/DOTNET : BenchmarkDotNetv0.13.1, OSUnknown I/DOTNET : Unknown processor I/DOTNET : [Host] : .NET 6.0.0 (6.0.21.52210), Arm64 RyuJIT I/DOTNET : I/DOTNET : ToolchainInProcessEmitToolchain I/DOTNET : I/DOTNET : | Method | Mean | Error | StdDev | Gen 0 | Allocated | I/DOTNET : |------------------ |-----------:|---------:|---------:|-------:|----------:| I/DOTNET : | Border | 242.3 µs | 1.34 µs | 1.25 µs | 0.9766 | 5 KB | I/DOTNET : | ContentView | 258.3 µs | 0.49 µs | 0.43 µs | 1.4648 | 6 KB | I/DOTNET : | ActivityIndicator | 380.3 µs | 1.40 µs | 1.31 µs | 0.9766 | 5 KB | I/DOTNET : | Label | 542.2 µs | 2.56 µs | 2.40 µs | 0.9766 | 5 KB | I/DOTNET : | Entry | 1,998.2 µs | 17.01 µs | 15.07 µs | - | 12 KB | I/DOTNET : I/DOTNET : // * Legends * I/DOTNET : Mean : Arithmetic mean of all measurements I/DOTNET : Error : Half of 99.9% confidence interval I/DOTNET : StdDev : Standard deviation of all measurements I/DOTNET : Gen 0 : GC Generation 0 collects per 1000 operations I/DOTNET : Allocated : Allocated memory per single operation (managed only, inclusive, 1KB 1024B) I/DOTNET : 1 µs : 1 Microsecond (0.000001 sec) I/DOTNET : I/DOTNET : // * Diagnostic Output - MemoryDiagnoser * I/DOTNET : I/DOTNET : // ***** BenchmarkRunner: End ***** I/DOTNET : // ** Remained 0 benchmark(s) to run ** I/DOTNET : Run time: 00:01:50 (110.92 sec), executed benchmarks: 5 I/DOTNET : I/DOTNET : Global total time: 00:01:51 (111.12 sec), executed benchmarks: 5 I/DOTNET : // * Artifacts cleanup * I/DOTNET : // * Artifacts cleanup * D/MAUI : Benchmark complete, success: True这份报告的关键信息解读环境头BenchmarkDotNetv0.13.1、OSUnknown、[Host] : .NET 6.0.0 ... Arm64 RyuJIT、ToolchainInProcessEmitToolchain——因运行在 Android 上OS 无法像桌面那样被探测故显示 UnknownArm64表明当前设备/镜像为 arm64项目 csproj 中默认RuntimeIdentifier为android-arm64。结果表Mean算术平均耗时、Error99.9% 置信区间的一半、StdDev标准差、Gen 0每 1000 次操作触发的 Gen 0 GC 次数、Allocated单次操作托管内存分配含 1KB1024B 换算。示例数据中Entry明显是最贵的控件约 2ms、12KB 分配Border与ContentView最轻这与 Handler 桥接的复杂度直接相关。请注意这些数字是仓库 README 记录的当时某台设备的结果仅供理解输出格式不应视为跨设备、跨版本的性能结论。收尾标记Run time给出纯基准执行耗时Global total time含预热等全部开销最后D/MAUI : Benchmark complete, success: True是 instrumentation 结束时的状态日志对应Log.Debug(Tag, $Benchmark complete, success: {success})可作为自动化判断是否成功的关键行。与桌面版 Benchmarks 的关系同一套 BenchmarkDotNet 的两处落地仓库中还有一套纯 .NET 控制台版基准测试 src/Core/tests/Benchmarks入口 Program.cs 通过BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args)支持按命令行参数筛选基准。它包含约 25 个更细粒度的用例如BindableObjectBenchmarker、BindingBenchmarker、PropertyMapperBenchmarker、SourceGeneratedBindingBenchmarker、VisualTreeBenchmarker、GridLayoutManagerBenchMarker、ShellBenchmarker等并配有Stubs目录ApplicationStub、HandlersContextStub、WindowStub、TestServices在纯托管环境中模拟 MAUI 运行时。两者形成互补桌面控制台版覆盖深层框架机制绑定、映射器、布局算法且可在开发机快速迭代而 Benchmarks.Droid 版本把重点放在真实 Android 运行时下控件 Handler 创建与图片加载这条端到端路径用本文介绍的命令在真机上验证。注意事项与最佳实践基于 README 与工程源码在实际使用时建议遵守以下几点务必用 Release 构建README 明确要求最终计时使用 ReleaseDebug 构建的计时无意义优先真机项目 csproj 注释Physical device is recommended默认RuntimeIdentifier为android-arm64PublishTrimmedfalse、RunAOTCompilationfalse真机能反映真实 CPU 频率、内存带宽与 GPU/解码硬件行为模拟器数据只能用于冒烟验证保持设备状态一致基准对频率缩放、后台进程敏感测量前应让设备冷却、关闭无关应用并在相同条件下对比不同分支关注日志过滤最终结果只依赖DOTNET与MAUI两个 tag若需实时观察可另开终端执行adb logcat DOTNET:V MAUI:V *:S新增基准类需修改源码当前Run()中硬编码了BenchmarkRunner.RunImageBenchmark(config)与BenchmarkRunner.RunViewHandlerBenchmark(config)代码注释 hardcoded here for now新增基准类需要同步在 MainInstrumentation.cs 中注册联网需求首次构建会从 NuGet 拉取 BenchmarkDotNet 0.13.10见 Benchmarks.Droid.csproj 的PackageReference IncludeBenchmarkDotNet Version0.13.10以及Xamarin.Android.Glide、Controls.Core.csproj、Core.csproj等项目引用需要正常的包源配置。总结Benchmarks.Droid 的 README 篇幅虽短却浓缩了在 Android 上跑 BenchmarkDotNet的一套完整工程方案用自定义 MSBuildBenchmarktarget 把安装、instrumentation 启动、日志采集串成单命令用MainInstrumentation避开 Activity 生命周期、以可脚本化的方式驱动InProcessEmitToolchain进程内执行用ViewHandlerBenchmark与ImageBenchmark两个用例覆盖 MAUI 最有代表性的开销路径。这套模式对任何需要在 Android/iOS 等受限运行时中测量 .NET 代码性能的团队都具有直接参考价值——本文中给出的命令、配置与输出解读可以直接迁移到自己的项目中使用。【免费下载链接】maui.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.项目地址: https://gitcode.com/GitHub_Trending/ma/maui创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表