首页
/ Flutter 框架微基准测试(microbenchmarks):运行方法、参数解析与结果采集机制详解

Flutter 框架微基准测试(microbenchmarks):运行方法、参数解析与结果采集机制详解

2026-09-04 16:05:31作者:伍霜盼Ellen

本篇指南基于 Flutter 仓库中的 dev/benchmarks/microbenchmarks 微基准测试工程,讲解如何在真机上运行全部或部分微基准测试、如何使用 --dart-define 控制测试子集与随机种子、结果日志的 JSON 协议格式,以及测试名称为何不可随意修改。读完本文,你可以独立执行 Flutter 框架核心路径(ChangeNotifier、编解码器、手势、布局、构建管线等)的性能基准,并理解 CI 侧 devicelab 如何从设备日志中解析出机器可读的基准数据。

一、工程定位与基准目录结构

dev/benchmarks/microbenchmarks 是一个标准的 Flutter 应用工程(见其 pubspec.yamldescription: Small benchmarks for very specific parts of the Flutter framework.),它依赖 flutterflutter_test(SDK 包)以及同仓库的路径包 stocks(供 stocks/* 系列基准复用真实业务 UI),并打包了一批 flutter_gallery_assets 图片/视频资源供 ui/image_bench.dart 等基准解码使用。

基准测试按被测的框架子系统分层存放在 lib/ 下:

  • foundation/change_notifier_benchobserver_list_benchstandard_message_codec_benchstandard_method_codec_benchplatform_asset_bundledecode_and_parse_asset_manifesttimeline_benchclampall_elements_bench
  • geometry/:矩阵变换、圆角矩形包含判断等;
  • gestures/:手势检测器、速度跟踪器;
  • language/compute 隔离、同步循环基准(sync_star_bench);
  • layout/:文本 intrinsic 尺寸测量;
  • stocks/:基于 stocks 应用的动画、构建(build)、布局基准;
  • ui/:图片解码基准。

入口文件 benchmark_collection.dart 用一个 Benchmark = (String name, Future<void> Function() value) 记录类型集中登记了全部 22 个基准,每个基准的注册名就是其相对 Dart 文件路径(如 foundation/change_notifier_bench.dart)。入口处的第一行即有防御性检查:

assert(false, "Don't run benchmarks in debug mode! Use 'flutter run --release'.");

由于 assert 只在 JIT(debug)模式下生效,profile/release(AOT)模式下该行被剔除,这正是运行命令要求加 --profile 的原因——基准必须跑在接近真实的 AOT 编译路径上,数据才有意义。

二、在真机上运行基准测试

按照 README 的原始步骤,先在设备连接好后开一个窗口观察日志,再在另一个窗口运行:

flutter logs
flutter run -d $DEVICE_ID --profile lib/benchmark_collection.dart

结果会打印到设备日志中(即 flutter logs 窗口),而不是终端。这一点可以从入口源码得到印证:跑完所有基准后,程序会打印结束标记 ╡ ••• Done ••• ╞,随后显式 await stdout.flush() 确保缓冲区刷出,再延迟 5 秒等待日志通道把输出“排空”后才 exit(0)(见 benchmark_collection.dart)。

三、运行子集:--dart-define=tests=...

微基准不支持给 main 传命令行参数,因此选择测试集完全靠编译期 Dart 变量。README 给出的子集运行命令为:

flutter run -d $DEVICE_ID --profile lib/benchmark_collection.dart --dart-define=tests=foundation/change_notifier_bench.dart,language/sync_star_bench.dart

对应到源码,入口用 args 包构造了一个 ArgParser,把 String.fromEnvironment('tests') 读取到的值转成 --tests 参数再解析(见 benchmark_collection.dart)。关键行为:

  • tests多选addMultiOption)选项,允许值为已登记基准名的列表,逗号分隔多个;
  • 默认值是全部 22 个基准名(defaultsTo: allowed),不传 tests 即全量运行;
  • 传入非法基准名会导致 parser.parse 报错,因为解析器带了 allowed 白名单;
  • 每次编译产物都会固化该选择,改 --dart-define 后需要重新编译运行。

四、随机种子:--dart-define=seed=...

为避免测试顺序引入的系统性偏差(如 CPU 频率爬升、缓存冷热、GC 压力随时间变化),入口在选中基准后会用种子做洗牌:

flutter run -d $DEVICE_ID --profile lib/benchmark_collection.dart --dart-define=seed=12345
final List<Benchmark> tests = benchmarks
    .where((Benchmark e) => selectedTests.contains(e.$1))
    .toList();
tests.shuffle(Random(int.parse(results.option('seed')!)));

(见 benchmark_collection.dart)源码注释说明:seed 默认值为 12345,由基础设施(CI)在一段时间内固定传入同一个种子,以便历史数据可比;换种子即得到不同的运行顺序。每个基准执行前,绑定帧策略会被重置为默认值 fadePointers,再由具体基准自行设置(例如 foundation/all_elements_bench.dart 会切换为 benchmarkLive 并先 runApp(const SizedBox.shrink()) 触发一次 dispose 流程,见 benchmark_collection.dart)。

五、结果输出格式:BenchmarkResultPrinter 的双通道协议

所有基准共用 common.dart 中的 BenchmarkResultPrinter,它是本工程的机器/人类双通道结果协议核心:

BenchmarkResultPrinter printer = BenchmarkResultPrinter();
printer.addResult(
  description: 'Average frame time',
  value: averageFrameTime,
  unit: 'ms',
  name: 'average_frame_time',
);
printer.printToStdout();
  • addResult({description, value, unit, name}):登记单值结果。name 是机器可读键(如 clamp_clampDouble),description 是日志里给人看的描述。
  • addResultStatistics({description, values, unit, name}):登记一组采样值,自动计算均值并额外产出一条 <name>_probability_5pct 结果——按正态分布数值积分算出“均值落在真值 ±5% 区间内”的概率(见 common.dart),用来量化该次测量的可信度。
  • printToStdout() 输出带固定标记的块(见 common.dart):
================ RESULTS ================
:::JSON::: {"clamp_clampDouble": 0.12, "clamp_clampDouble_probability_5pct": 0.98, ...}
================ FORMATTED ==============
clamp - clampDouble: 0.1 us per iteration
clamp - clampDouble - probability margin of error 0.05: 98.0 percent

这三个标记字符串(jsonStartjsonEndjsonPrefix)在源码注释中被明确要求与 dev/devicelab/lib/microbenchmarks.dart 保持同步——因为 CI 侧就是靠它们切分日志。

六、CI 侧如何收割结果:devicelab 的 readJsonResults

仓库中的 devicelab 任务(如 dev/devicelab/bin/tasks/microbenchmarks.dart 的 Android 任务、microbenchmarks_ios.dart 的 iOS 任务)通过 readJsonResults 与设备上的基准进程对接,其流程完整解释了 README 中“结果在设备日志里”这句话的下游去向:

  1. 逐行监听 flutter run 进程的 stdout,stderr 转发到终端;
  2. 见到 ================ RESULTS ================ 进入采集态,把 :::JSON::: 前缀之后的 JSON 累积起来,直到 ================ FORMATTED ============== 收尾;
  3. 见到结束标记 ╡ ••• Done ••• ╞ 后,先向 stdin 写入 q(console runner 的退出指令,对应源码中引用的历史 issue #19208),等待 2 秒后再补发 SIGINT,保证基准进程干净退出;
  4. 把所有采集到的 JSON 块合并为 Map<String, double> 返回,供任务框架写入指标中心。

换言之,微基准 App 与 devicelab 之间是一条纯 stdout 文本协议:标记行定界、JSON 承载数据、Done 行触发收割。如果你要新增自定义基准,只要遵循 printToStdout 的输出约定,devicelab 侧无需任何改动即可解析。

七、一个基准是怎么写的:以 clamp.dart 与 ChangeNotifier 为例

理解 foundation/clamp.dart 可以快速掌握本工程的基准范式:

const int _kBatchSize = 100000;
const int _kNumIterations = 1000;
...
final watch = Stopwatch();
{
  final clampDoubleValues = <double>[];
  for (var j = 0; j < _kNumIterations; ++j) {
    double tally = 0;
    watch.reset();
    watch.start();
    for (var i = 0; i < _kBatchSize; i += 1) {
      tally += clampDouble(-1.0, 0.0, 1.0);
      // ... 更多取值,覆盖边界与 NaN
    }
    watch.stop();
    clampDoubleValues.add(watch.elapsedMicroseconds.toDouble() / _kBatchSize);
    if (tally < 0.0) {
      print("This shouldn't happen.");
    }
  }
  printer.addResultStatistics(
    description: 'clamp - clampDouble',
    values: clampDoubleValues,
    unit: 'us per iteration',
    name: 'clamp_clampDouble',
  );
}

其中体现了几条值得借鉴的度量手法:大批量循环(单批 10 万次)摊薄计时开销;多次独立采样(1000 轮)后交给 addResultStatistics 做统计;tally 累加结果用于“防 DCE(死代码消除)”——保证 AOT 优化器不能把被测调用整个优化掉,同时校验结果合法性;nameclamp_clampDoubleclamp_Double_clamp)保持稳定,形成可跨版本对比的历史序列。

更复杂的 foundation/change_notifier_bench.dart 则展示了预热(warm-up)模式:每个操作(addListener/notify/removeListener/通知中移除)都先以 _kNumWarmUp = 100 迭代跑一遍 addResult: false 的预热轮,再以 65536 次迭代正式采样,并按 1~5 个监听者数量分档输出 add1..add5notify0..notify5 等结果,时间精度用 _kScale = 1000 换算成纳秒/次。该基准还特别在注释中提醒:为单独测量 add/notify 而故意不注销监听,依赖最终 GC 回收,这在真实应用中会造成内存泄漏——基准代码与生产代码的写法边界在此有明确交代。

八、为什么不能改基准名称

README 中独立一节强调“避免修改基准名称”:

每个微基准由名称标识,例如 "catmullrom_transform_iteration"。修改传给 BenchmarkResultPrinter.addResult 的名称,实际上等同于删除旧基准并创建一个新基准,过程中会丢失旧基准关联的历史数据。

结合本文第五、六节可以看出其机制:devicelab 把每个 name 作为 JSON 键写入指标系统,历史曲线就是按这个键累积的;换一个键,采集器只会把它当作一条全新指标,旧序列从此“断档”。因此,重命名基准文件、调整被测逻辑可以,但 name(以及 description/unit 对应的语义)一旦发布就应当保持稳定;确需变更时,更稳妥的做法是保留旧键并新增新键,让新旧序列并行一段时间再下线旧序列。

九、运行前提与限制

  • 必须使用真机 + profile(或 release)模式flutter run ... --profile lib/benchmark_collection.dart;debug 模式会直接命中 assert(false) 终止。
  • --dart-define 是编译期常量tests/seed 通过 String.fromEnvironment 注入,修改后需重新编译;seed 缺省 12345tests 缺省全量。
  • 结果只出现在设备日志:需并行开着 flutter logs(README 原文即如此指导),程序退出前会 stdout.flush() 并等待 5 秒,确保日志通道完整送达。
  • 适用版本前提:本工程 pubspec.yaml 声明 SDK 约束 ^3.11.0-0 且采用 resolution: workspace,属于仓库 monorepo 工作区成员,直接检出本仓库当前版本即可运行;文中基准清单(22 项)与协议标记均以该仓库版本源码为准。

十、小结

dev/benchmarks/microbenchmarks 把 Flutter 框架中 22 个细粒度热点封装成一个可在真机 profile 模式执行的应用:--dart-define=tests 选择子集、--dart-define=seed 固定洗牌顺序,BenchmarkResultPrinter 以“RESULTS 标记 + JSON + FORMATTED 文本”的协议输出结果,devicelab 的 readJsonResults 再按同一组标记收割并入库。掌握这套“注册名即历史序列、stdout 标记即采集协议、warm-up 与防 DCE 即数据可信度”的约定,你就既能手动跑基准验证性能直觉,也能理解 Flutter CI 基准数据是如何从设备日志变成可追踪的回归曲线的。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
docsdocs
暂无描述
Markdown
899
5.83 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
925
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
601
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
525
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
395